Tokenholder Specifications

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:

  1. Fetch security tokens that belong to the current user.

  2. Fetch Tokenholders.

  3. 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.js
function App() {
  ...
  // b. Fetch tokens
  useEffect(() => {
    // An async action to retrieve relevant tokens.
    async function fetchTokens(dispatch, polyClient, walletAddress) {
      // An auxiliary action to start a spinner.
      dispatch({type: a.FETCHING_TOKENS})

      // Use the SDK to fetch tokens.
      const tokens = await polyClient.getSecurityTokens({ walletAddress })

      const tokenSelectOpts = 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.

const tokens = await polyClient.getSecurityTokens({ 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).

// App.js
function App() {
  ...
  useEffect(() => {
    // Async action to fetch tokenholders.
    async function fetchTokenholders(dispatch, st) {
      let tokenholders = await st.tokenholders.getTokenholders()
      dispatch({ type: a.TOKENHOLDERS_FETCHED, tokenholders })
    }
    if ( reloadTokenholders === true | selectedToken !== undefined ) {
      fetchTokenholders(dispatch, tokens[selectedToken])
    }
  }, [tokens, selectedToken, reloadTokenholders])
}

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.js
function App() {
  ...
  // Used for both adding a new tokenholder and modifying an existing one.
  async function modifyWhitelist(data) {
    const queue = await tokens[selectedToken].tokenholders.modifyData({
      tokenholderData: data
    })
    await queue.run()
    dispatch({type:a.RELOAD_TOKENHOLDERS})
  }

  async function removeTokenholders(addresses) {
    dispatch({type: a.DELETING_TOKENHOLDER})
    const queue = await tokens[selectedToken].tokenholders.revokeKyc({
      tokenholderAddresses: addresses
    })
    await queue.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:

const queue = await tokens[selectedToken].tokenholders.modifyData({
  // data is an array of tokenholder objects.
  tokenholderData: data
})
await queue.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.js
export default ({toknholders, modifyWhitelist, removeTokenholders}) => {
  const form = useForm()
  // These functions are provided by rc-form-hooks library. See https://github.com/mushan0x0/rc-form-hooks
  const { 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 } = state

  const closeForm = () => {
    dispatch({type: 'CLOSE_FORM'})
    resetFields()
  }
  
  // Passing an index means that we're editing an existing item. Otherwise it's a create form.
  const openForm = (index = '') => {
    dispatch({ type: 'OPEN_FORM', payload: { editIndex: index } })
  }

  const submitForm = async () => {
    const fields = ['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.
          await modifyWhitelist([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 || defaultTokenholderValues
    setFieldsValue(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())
                            !== undefined
                        return 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.

Last updated