How to Apply Transfer Restrictions for Compliant Security Tokens
This tutorial application will teach you how to add transfer restrictions to token holders in 3 simple steps:
Fetch security tokens that belong to the current user.
Fetch Tokenholders.
Add, update and remove Tokenholders.
Once we've established an SDK connection, we can fetch the security tokens owned by the current address in Metamask.
// App.jsfunctionApp() {...// b. Fetch tokensuseEffect(() => {// An async action to retrieve relevant tokens.asyncfunctionfetchTokens(dispatch, polyClient, walletAddress) {// An auxiliary action to start a spinner.dispatch({type:a.FETCHING_TOKENS})// Use the SDK to fetch tokens.consttokens=awaitpolyClient.getSecurityTokens({ walletAddress })consttokenSelectOpts=tokens.map((token, i) =><Option value={i} key={i}>{token.symbol}</Option>)// Send fetched tokens to be saved in app state. Also stop the spinner.dispatch({type:a.FETCHED_TOKENS, tokens, tokenSelectOpts}) }if (polyClient && walletAddress &&!tokens) {fetchTokens(dispatch, polyClient, walletAddress) } }, [walletAddress, polyClient, tokens])...return (...}
The SDK will query the SecurityTokenRegistry for tokens owned by walletAddress.
Use the SDK to fetch an array of tokenholders. Start by running your async code in a useEffect() hook. Note that the effect won't run unless the user has selected a token, or if you're reloading the tokenholders list deliberately (via relaodTokenholders).
Note that contrary to fetching tokens, fetching tokenholders is a method of the SecurityToken entity objet rather than a top-level method:
st.tokenholders.getTokenholders()
vs
polyClient.getSecurityTokens({ walletAddress })
Tokenholder-related methods are grouped under the .tokenholders namespace.
The app state contains the tokens array fetched earlier. You can access the currently selected tokens by tokens[selectedToken].
First, add event handlers to the App component. You can add these functions anywhere in the App function:
// App.jsfunctionApp() {...// Used for both adding a new tokenholder and modifying an existing one.asyncfunctionmodifyWhitelist(data) {constqueue=await tokens[selectedToken].tokenholders.modifyData({ tokenholderData: data })awaitqueue.run()dispatch({type:a.RELOAD_TOKENHOLDERS}) }asyncfunctionremoveTokenholders(addresses) {dispatch({type:a.DELETING_TOKENHOLDER})constqueue=await tokens[selectedToken].tokenholders.revokeKyc({ tokenholderAddresses: addresses })awaitqueue.run()dispatch({type:a.TOKENHOLDER_DELETED})dispatch({type:a.RELOAD_TOKENHOLDERS}) }...return (... { selectedToken !== undefined && <Whitelist tokenholders={tokenholders}// Note these additional properties modifyWhitelist={modifyWhitelist} removeTokenholders={removeTokenholders} /> }... )
modifyWhitelist: A submit handler for the tokenholder form. The handler receives an array of tokenholder data objects (which include the tokenholder Ethereum addresses, buy and sell lockup dates and their KYC expiry dates). Eventually, the function uses the SDK's modifyData as follows:
constqueue=await tokens[selectedToken].tokenholders.modifyData({// data is an array of tokenholder objects. tokenholderData: data})awaitqueue.run()
removeTokenholders: A handler for the delete button. It will call the SDK's revokeKyc to revoke KYC for passed addresses.
Now, add the add/edit tokenholder form. First, create a wrapper component Whitelist, which will wrap the form as well as the tokenholders table we've created in previous steps.
// Whitelist.jsexportdefault ({toknholders, modifyWhitelist, removeTokenholders}) => {constform=useForm()// These functions are provided by rc-form-hooks library. See https://github.com/mushan0x0/rc-form-hooksconst { getFieldDecorator,setFieldsValue,resetFields,validateFields } = form // Note this is a different reducer function from the one we created earlier. We're using this reducer particularly for form state.
const [state,dispatch] =useReducer(reducer, initialState)const { visible,editIndex,ongoingTx } = stateconstcloseForm= () => {dispatch({type:'CLOSE_FORM'})resetFields() }// Passing an index means that we're editing an existing item. Otherwise it's a create form.constopenForm= (index ='') => {dispatch({ type:'OPEN_FORM', payload: { editIndex: index } }) }constsubmitForm=async () => {constfields= ['address','canSendAfter','canReceiveAfter','kycExpiry','canBuyFromSto','isAccredited']validateFields(fields, { force:true }).then(async (values) => {// The values below are instances of momentjs. Convert them to JS Date objects as the SDK expects.values.canSendAfter =values.canSendAfter.toDate()values.canReceiveAfter =values.canReceiveAfter.toDate()values.kycExpiry =values.kycExpiry.toDate()try {dispatch({type:'TX_SEND'})// Call the helper function, which will the SDK in its turn.awaitmodifyWhitelist([values])dispatch({ type:'TX_RECEIPT'})resetFields() }catch (error) {// This could be transaction error, or a user error e.g user rejected transaction.dispatch({ type:'TX_ERROR', payload: {error:error.message} })message.error(error.message) } }) }let editedRecord =tokenholders.filter(tokenholder =>tokenholder.address === editIndex)[0]// This effect sets form initial values. In "edit" mode, initial values reflect the currently edited record.useEffect(() => {let initialValues = editedRecord || defaultTokenholderValuessetFieldsValue(initialValues) }, [editedRecord, setFieldsValue])return (<div style={{display:'flex', flexDirection:'column'}}><Button onClick={openForm}>Add new</Button> {/* This is the component from the previous guide */}<TokenholdersTable tokenholders={tokenholders} removeTokenholders={removeTokenholders} openForm={openForm} /><Modal title={editedRecord ? 'Edit token holder':'Add a new token holder'} closable={false} visible={visible} footer={null}><Spin spinning={ongoingTx} size="large"><Form {...formItemLayout}><Item name="address" label="Address">// here we add a couple of address validators// - The first make sure that the address is a valid ethereum address// - The second make sure we're not adding an existing tokenholder. {getFieldDecorator('address', { rules: [ { required: true }, { validator: (rule, value, callback) => {if (!editedRecord && !web3Utils.isAddress(value)) {callback('Address is invalid') return }callback() return } }, { validator: (rule, value, callback) => { const tokenholderExists= (address) => { const ret = tokenholders.find((element) => element.address.toUpperCase() === address.toUpperCase())
!==undefinedreturn ret }if (!editedRecord && tokenholderExists(value)) {callback('Tokenholder is already present in the whitelist') return }callback() return } } ],// Disable address field in case of editing. })(<Input disabled={!!editedRecord}/>)}</Item><Item name="canSendAfter" label="Can Send after"> {getFieldDecorator('canSendAfter', { rules: [{ required: true }], })(<DatePicker />)}</Item><Item name="canReceiveAfter" label="Can Receive After"> {getFieldDecorator('canReceiveAfter', { rules: [{ required: true }], })(<DatePicker />)}</Item><Item name="kycExpiry" label="KYC Expiry"> {getFieldDecorator('kycExpiry', { rules: [{ required: true }], })(<DatePicker />)}</Item><Item name="canBuyFromSto" label="Can Buy from STO"> {getFieldDecorator('canBuyFromSto', { valuePropName: 'checked', })(<Switch />)}</Item><Item name="isAccredited" label="Accredited"> {getFieldDecorator('isAccredited', { valuePropName: 'checked', })(<Switch />)}</Item> <Item><Button onClick={closeForm}>cancel</Button><Button type="primary" onClick={submitForm}>save</Button></Item></Form></Spin></Modal></div> )}
Feel free to substitute the Ant Design library used here with any other library you're familiar with.