3. Fetch and display shareholders - PolymathNetwork/whitelist-standalone GitHub Wiki

One of the most used modules in Polymath protocol is the GeneralTransferManager module or GTM. It's a free module that gets attached to all newly created tokens by default. It allows token owner to define a white list of token holders (called shareholders by the SDK). Here's the model of a token holder recorded on the GTM:

- address: Ethereum address of this token holder.
- canTransferAfter (boolean): The date after which they can transfer their holdings.
- canReceiveAfter (Date): The date after which they can receive tokens.
- kycExpiry (Date): The date when this record expires.
- canBuyFromSto (boolean): Whether they can buy from STO directly (versus receiving tokens through minting).
- isAccredited (boolean): Whether they're accredited investors or not. 

Checkout this document for more information about the GTM: General-Transfer-Manager.

Like previous sections, we're going to use the SDK to fetch token shareholders, then we'll display them in a simple Ant Design Table component.

Components

// Whitelist.js

function formatDate(input) {
  // Format a date using momentjs.
  return moment(input).format('YYYY-MM-DD')
}

// Return boolean values as an Icon.
function formatBool(input) {
  return input ?
    <Fragment><Icon type="check-circle" theme="filled"/><span>Yes</span></Fragment> :
    <Fragment><Icon type="close-circle" theme="filled"/><span>No</span></Fragment>
}

export WhitelistTable = ({tokenholders}) => {
  return (
    <Table dataSource={tokenholders} rowKey="address">
      <Column
        title='Address'
        dataIndex='address'
        render={(text) => <Text>{text}</Text>}
      />
      <Column
        title='Can send after'
        dataIndex='canSendAfter'
        render={(text) => formatDate(text)}
      />
      <Column
        title='Can receive after'
        dataIndex='canReceiveAfter'
        render={(text) => formatDate(text)}
      />
      <Column
        title='KYC expiry'
        dataIndex='kycExpiry'
        render={(text) => formatDate(text)}
      />
      <Column
        title='Can buy from STO'
        dataIndex='canBuyFromSto'
        render={(text) => formatBool(text)}
      />
      <Column
        title='Is accredited'
        dataIndex='isAccredited'
        render={(text) => formatBool(text)}
      />
      <Column render={(text, record) => {
        return (
          <Fragment>
            // We'll create openForm() in a later section
            <Button onClick={() => openForm(record.address)}>
              <Icon type="edit" theme="filled" />
            </Button>
            // We'll create removeShareholders() in a later section
            <Button onClick={() => removeShareholders([record.address])}>
              <Icon type="delete" theme="filled" />
            </Button>
          </Fragment>
        )
      }}/>
    </Table>
  )
}

The table above receives shareholders array as a property. Each object is structures as we layed it out earlier. We format some values before rendering, eg boolean values will be rendered as icons, and Date type values will be formatted. Additionally, we've added two buttons that we'll use later to edit or delete a record.

Now that we have this component declared in a separate file (Whitelist.js), we're going to import it into App.,js, and then append it to the component as shown below.

// App.js
function App() {
  const [state, dispatch] = useReducer(reducer,initialState)
  const { tokenholders } = state;

  return (
    <div className="App">
      ...
      ...
      { selectedToken !== undefined &&
        <WhitelistTable tokenholders={tokenholders} />
      }
    </div>
  )
}

Actions

We haven't fetched shareholders array yet, so let's use the SDK for that purpose. As always, we run our async code in a useEffect() hook. Note that the effect won't run unless user has selected a token, or if we're reloading shareholders list deliberately (via relaodShareholders). We're going to use the later flag to signal that we need shareholders list refreshed, say after editing or deleting a shareholder.

// App.js
function App() {
  ...
  useEffect(() => {
    // Async action to fetch shareholders.
    async function fetchShareholders(dispatch, st) {
      let shareholders = await st.shareholders.getShareholders()
      dispatch({ type: a.SHAREHOLDERS_FETCHED, shareholders })
    }
    if ( reloadShareholders === true | selectedToken !== undefined ) {
      fetchShareholders(dispatch, tokens[selectedToken])
    }
  }, [tokens, selectedToken, reloadShareholders])
}

Note that contrary to fetching tokens, fetching shareholders is a method of SecurityToken object rather than a top-level method:

st.shareholders.getShareholders()

vs

polyClient.getSecurityTokens({ walletAddress })

You might have notices that it's under .shareholders namespace too, that's where all shareholder-related method are grouped.

Fortunately, app state contains the tokens array we had fetched earlier. So we access the currently selected tokens by tokens[selectedToken].

Reducers

Finally, we'll add a few more cases to the reducer() function we created earlier. We need to handle two actions:

  • SHAREHOLDERS_FETCHED, which is dispatched once getShareholders has resolved. In this case we'll stop the spinner and add the shareholders array to state.
  • RELOAD_SHAREHOLDERS, this action will set reloadShareholders flag to true. As we saw in the effect function above, that flag will force reloading shareholders.
// App.js
function reducer() {
  ...
  case a.SHAREHOLDERS_FETCHED:
    let { shareholders } = action
    return {
      ...state,
      shareholders,
      fetching: false,
      tip: '',
      reloadShareholders: false,
    }
  
  case a.RELOAD_SHAREHOLDERS:
    return {
      ...state,
      fetching: true,
      tip: 'Reloading tokenholders...',
      reloadShareholders: true,
    }
  } 
}

That's it. In the next section we will know how to add/edit/delete shareholders to token's whitelist.

⚠️ **GitHub.com Fallback** ⚠️