1. Using Polymath SDK to connect to smart contracts - PolymathNetwork/whitelist-standalone GitHub Wiki
First, we're going to establish a connection to the Polymath protocol, via Polymath SDK. Connection is just a matter of instantiating the SDK, then configuring it with the PolymathRegistry contract address on each supported network (namely Mainnet and Kovan). To learn more, please check out the SDK getting started guide
The SDK is accompanied by browserUtils
helper library, which will let us query some data from the injected web3 object, such as current ethereum network and current user account. Returned information will be displayed in the app header, as follows:
Once connected, the app renders a simple app header, which displays two pieces of data from Metamask: the current ethereum network (eg Mainnet or Kovan), and the currently selected account.
Also, we need to display any errors that occur during the connection. Errors could occur if you lose internet connection, user refuses to use Metamask with the app, or user has selected an unsupported network. Polymath smart contracts are deployed to Mainnet and Kovan only.
// This Context provider will provide 'dispatch' function to children components
const DispatchContext = React.createContext(null)
export default DispatchContext
const initialState = {
walletAddress: '',
polyClient: undefined, // It's completely unnecessary to declare a state var with an undefined value, but we're listing all variables here for sake of completeness.
connected: false,
error: '',
networkId: 0,
tip: ''
}
function Network({networkId}) {
const networks = {
0: 'Disconnected',
1: 'Mainnet',
42: 'Kovan'
}
return (
<Fragment>
<Icon type="global"/>
<Typography.Text>{networks[networkId]}</Typography.Text>
</Fragment>
)
}
function User({walletAddress}) {
if (walletAddress)
return (
<Fragment>
<Icon type="user"/>
<Typography.Text>{walletAddress}</Typography.Text>
</Fragment>
)
return null
}
function App() {
const [state, dispatch] = useReducer(reducer, initialState)
const {
tip,
walletAddress,
connecting,
error,
networkId,
} = state
return (
<div className="App">
{/* This Context provider will provide 'dispatch' function to children components */}
<DispatchContext.Provider value={dispatch}>
<Spin spinning={connecting} tip={tip} size="large">
<Layout>
<Header>
<Network networkId={networkId} />
<User walletAddress={walletAddress} />
</Header>
<Content>
{ error &&
<Alert
message={error}
type="error"
/> }
<Content />
</Layout>
</Spin>
</DispatchContext.Provider>
</div>
)
}
Note about code snippets: style props have been omitted for sake of brevity. But feel free to check the reference app source any time.
As you can see, the component above displays a handful of state variables:
-
connecting
which is a flag that determines whether the<Spin />
component is visible and spinning. -
tip
the message to display on the spinner above. -
networkId
this is the integer ID of the currently selected Metamask network. We will use the SDK to retrieve that info. -
walletAddress
the address of currently selected Metamask account. -
error
is an error message that, if populated, will be displayed in an<Alert />
component. We'll use it to display any exceptions thrown while connecting.
Now that we have a dumb component that serves as a scaffold for our data, we need to carry on with the actual SDK connection, which will manipulate component state accordingly. We're going to accomplish that using a combination of useReducer
and useEffect
hooks in the next section.
If you're familiar with React, you know that state can be updated using setState()
function, eg setState({connecting: false})
. We've elected to use useReducer()
hook in this guide. It brings basic Redux features such as centralized state management, without the extra dependency. Additionally, it will help us maintain a purely functional paradigm without resorting to React class components.
From Redux docs, actions are:
payloads of information that send data from your application to your store. They are the only source of information for the store.
We're using two types of actions in this guide:
-
Synchronous actions: those are plain JS objects. They're comprised of a
type
property as well as the payload we're sending to the store. For example,dispatch({ type: actions.CONNECTION_ERROR, error: error.message})
, which updates theerror
state variable in the store. -
Asynchronous actions which won't send data to the store directly. They're rather regular JS functions that conduct async operations (eg querying Metamask for the current user account), and eventually, they dispatch one of the synchronous actions above in order to update the state. The only async action we have here is
connect()
function.
On page load, we need to call connect()
. That can be accomplished by calling the function in componentDidMount
life-cycle method. But as mentioned before, we will stick with React hooks, ie useEffect()
. Effects are invoked every time React renders or re-renders your components, so we'll need to add a safeguard to make sure it run only when we're not connected.
You'll notice that the async action connect()
, is defined inside the effect where it's being called. You can find the rational for this decision here.
const actions = {
CONNECTING: 'connecting',
CONNECTED: 'connected',
CONNECTION_ERROR: 'connection_error'
}
// A. Connect to Polymath ecosystem
useEffect(() => {
async function connect(dispatch) {
// A1. Start the spinner!
dispatch({
type: actions.CONNECTING
})
try {
// A2. Get the current network and make sure it's either Mainnet or Kovan.
const networkId = await browserUtils.getNetworkId()
const walletAddress = await browserUtils.getCurrentAddress()
if (![-1, 1, 42].includes(networkId)) {
dispatch({
type: actions.CONNECTION_ERROR,
error: 'Please switch to either Main or Kovan network'
})
return
}
// A3. Instantiate and configure the SDK. Then, dispatch CONNECTED action with the necessary state variables. This should also stop the snipper.
export const networkConfigs = {
1: {
polymathRegistryAddress: '0xdfabf3e4793cd30affb47ab6fa4cf4eef26bbc27'
},
42: {
polymathRegistryAddress: '0x5b215a7d39ee305ad28da29bf2f0425c6c2a00b3'
},
};
const config = networkConfigs[networkId]
const polyClient = new Polymath()
await polyClient.connect(config)
dispatch({
type: actions.CONNECTED,
networkId,
polyClient,
walletAddress: walletAddress,
})
}
catch(error) {
// A4. Dispatch ERROR action in order to display any errors thrown in the process.
dispatch({
type: actions.CONNECTION_ERROR,
error: error.message
})
}
}
// Attempt to connect but only if we haven't connected yet.
if (!connected) {
connect(dispatch)
}
}, [connected]) // we're passing this dependency so that the effect is invoked every time `connected` value has changed.
Here's the flow of the actions above:
- On page load, the effect above is called.
- The effect will determine if we're connected by checking the state variable
connected
. If not, it's going to call the async actionconnect
. -
connect
will dispatchCONNECTING
sync action, signalling that an async connection operation is in progress. -
connect
will usebrowserUtils
lib to retrieve the current metamask user account and network, orwalletAddress
andnetworkId
, respectively. - Polymath smart contracts are deployed to both Main and Kovan networks. If the currently selected network is neither of them, we dispatch
CONNECTION_ERROR
action to signal an error. Also we dispatch that action for any exceptions caught during the process. - Once a connection has established (ie
await polyClient.connect(config)
resolved), we sendnetworkId
,walletAddress
as well as the SDK client object viaCONNECTED
action.
From Redux docs:
Reducers specify how the application's state changes in response to actions sent to the store. Remember that actions only describe what happened, but don't describe how the application's state changes.
So far we've built a component to display app state, and defined the actions responsible for sending the state vars we need to the state store. What's missing, is a reducer function that changes app state in response to actions. For that purpose we're going to declare a new pure function reducer()
. Note that we've already passed a function with that name to the useReducer()
call above.
function reducer(state, action) {
switch (action.type) {
case actions.CONNECTING:
return {
...state,
connecting: true, // Spinner will keep on spinning until connection has established.
tip: 'Connecting...', // Message to display while connecting.
error: undefined // Clear previous error, if any.
}
case actions.CONNECTED:
// Update state with action payload variables.
const { polyClient, networkId, walletAddress } = action
return {
...state,
polyClient,
networkId,
walletAddress,
connecting: false,
tip: '',
error: undefined,
}
case actions.CONNECTION_ERROR:
const { error } = action
return {
...state,
error,
connecting: false,
tip: '',
}
default:
throw new Error(`Unrecognized action "${action.type}"`)
}
}
In the snippet above, we're handling the three types of sync actions we've got:
- On
CONNECTING
, make spinner display the messagetip
. - On
CONNECTED
, hide spinner, and update state with action's payload. - On
CONNECTION_ERROR
, hide spinner and display errors.
Once you paste all aforementioned sections, the resulting src/App.js
file should look like this. Don't worry about remaining components, actions and reducers. We're going to build them in the next few guides.
Now run your app via yarn start
, and open localhost:3000
in your browser. If all is good, you should see a Metamask popup that prompts to grant the current app, the permission to access Metamask account. Go ahead and click on "Connect".
Once, a spinner will appear until async action connect()
resolves. Finally, you'll see an almost blank page with a header that looks like this:
Shall an error occur, for example if user denies granting the app a permission to use Metamask, or if they have selected a unsupported network, an error will be displayed as follows: