A-Z of building dApps on ICE — Part 5: Using Polkadot.js to build dApps.

ICONOsphere
7 min readApr 5, 2022

In the part 4 of this series, we discussed writing and deploying ERC20 token using Ink! smart contract on the Arctic network (ICE/Snow testnet), using the Polkadot.js app.

Now, we will build a client-side dApp that interacts with the ERC20 token contract, that we deployed in the part 4 of the series, using Polkadot.js and ReactJS library.

Polkadot.js library allows us to create client-side dApps for smart contracts deployed on substrate-based blockchain.

What you will learn

Prerequisite

This browser extension handles accounts and facilitates the signature of transactions with those accounts. It does not inject providers for use by dApps at this early point, nor does it execute wallet services, e.g transmit funds.

  • Accounts with funds

To get funds into your account, you can refer to ice testnet faucet documentation.

Getting started

You can find full implementation of code on this github repo.

The client-side implementation has the following structure and content:

Tree structure of the frontend folder

The frontend is created using create-react-app template. The main logic is written in the App.js file. Polkadot.js library is used to communicate with deployed Ink! Smart contract on the arctic testnet.

Running the frontend

To get started quickly, you can clone the frontend repo and the deploy ERC20 contract (in case you have not done it already).

Next, grab the ERC20 contract address from the Polkadot.js app and paste it into the CONTRACT_ADDRESS variable which is defined in App.js.

For eg:

CONTRACT_ADDRESS=‘5FZ4bns2YnroQYjGXPCTG2CxitiZeYK9LeU5KnkLEeSC49jz’;

Also, replace the content of src/metadata.json with your own compiled erc20 metadata.json (ABI).

Now that the frontend is configured, you can navigate to root path of the project and run the app in the browser by using the following commands:

npm install
npm start

The ERC20 token dApp will launch and will be accessible via any browser at http://localhost:3000.

UI for the dApp

Note: Since the UI/UX is not the main focus of this article, the UI is kept simple

The UI contains all the methods of the contract which we can interact with from the frontend.

You should have Polkadot.js extension installed on your browser and have at-least one wallet created on it

Understanding the code

Now let’s go through understanding important sections of the frontend code. The main logic is inside the src/App.js file.

In the src/App.js file, you will find the following snippets.

// Snippet 1
const PROVIDER_URL = ‘wss://arctic-rpc.icenetwork.io:9944’;
const setup = async () => {
try {
const wsProvider = new WsProvider(PROVIDER_URL);
const _api = await ApiPromise.create({provider: wsProvider});
if(_api) {
console.log('Connection Success');
setApi(_api);
}
} catch (error) {
console.log(error);
}
}

// Snippet 2
const extensionSetup = async () => {
const extension = await web3Enable('polkadot-client-app');
if(extension.length === 0) {
console.log('No extension Found');
return;
}
let acc = await web3Accounts();
// initialize contract
const contractAddress = CONTRACT_ADDRESS;
const contract = new ContractPromise(api, abi, contractAddress);
setContract(contract);
setAccounts(acc);

}

In snippet 1, we have created a provider using WsProvider by passing arctic testnet WebSocket url in it as a parameter.

Then an instance of the _api is created using ApiPromise.create which will eventually be used to create an instance of a contract in our code.

Both WsProvider and ApiPromise are imported from the @polkadot/api library.

The Polkadot.js library uses a WebSocket endpoint to interact with the arctic testnet.

Note: In all cases, the _api will handle reconnecting automatically. This means that when you connect and the endpoint is not (yet) ready, the promise will not resolve immediately, but rather when connected. The same applies to when a connection is lost, the API will manage re-connections.

Similarly, snippet 2 is used to invoke the Polkadot extension to get the wallet addresses and also for creating an instance of the contract.

Here, web3Enable() retrieves the list of all injected extensions. In the absence of an extension, it logs the message informing that extension is not found.

In presence of the extension, Polkadot wallet pops up to ask for permission for connecting the Polkdot wallet extension to the dApp.

web3Accounts() returns a list of all the accounts available in a Polkadot wallet extension.

web3Enable and web3Accounts are imported from @polkadot/extension-dapp.

ContractPromise() is called to create the instance of a contract by passing the abi(metadata.json), api(initialized in snippet 1), and contractAddress as parameters. This instance is stored in a variable named contract. The contract variable can be used to invoke the read and state-changing methods from the deployed ERC20 contract.

ContractPromise is imported from @polkadot/api-contract library.

Let us now look at calling some functions using this contract instance.

Note: All the following code snippets are from App.js file.

Read Only Methods

To read a value from the contract, we can do the following:

// Read from the contract via an RPC callconst value = 0; // only useful on isPayable messages

// NOTE the apps UI specified these in mega units
const gasLimit = 3000n * 1000000n;

const {result,output} = await contract.query.name(userWallet,{ value, gasLimit });
console.log(output.toHuman()) // → myToken

Note: toHuman() — returns Human-parsable JSON structure with values formatted as per the settings

Here, contract.query.name is used to retrieve the name of token.

So we can call any read-only function by using query.<methodName>

Its format is always of the form .methodName(<account address to use>, <value>, <gasLimit>, <…additional params>)

Let us now look at an example of how to query the balance of a specific account.

const {result, output} = await contract.query.balanceOf(userWallet,{value, gasLimit}, targetAddress);console.log(output.toHuman()) //returns balance of `targetAddress`

Here, userWallet (the logged in account) is the address, that we are using to call the contract method and targetAddress is the address, whose balance we are querying.

Next we will look at some state-changing (write) methods that we are invoking from our frontend app.

Transfer Token

In addition to using the .query.<methodName> on a contract, the .tx.<methodName> is also provided to send an actual encoded transaction to the contract. This allows for execution and have the transaction applied to a block.

Example of transferring tokens in the dApp:

const fromAcc = ownerAccount; // account to send tx from
const injector = await web3FromSource(fromAcc.meta.source);
await contract.tx.transfer({value, gasLimit}, targetAddress,amount)
.signAndSend(fromAcc.address, {signer: injector.signer}, ({status}) => {
if(status.isFinalized) {
console.log(`Finalized Block hash : ${status.asFinalized.toString()}`);
} else {
console.log(`Current transaction status: ${status.type}`);
}
})

Here tx.transfer() method is called to transfer tokens by passing targetAddress and amount as parameters.

Note: Here, fromAcc is the account returned directly by web3Accounts() api which is an object
For example, the value of fromAcc will look like the following

fromAcc = {
address:"5CXMFdfLwN9Qr3bB9Gj8pTDzy39EXCVxUMxy5QYo2LAknSfL",
meta:{
genesisHash:null,
name:"Test",
source:"polkadot-js"
},
type:"sr25519"
}

To sign and send our transaction, the address of the account (as retrieved injector) is passed through as the param to the `signAndSend`, the API then calls the Polkadot extension to present it to the user and get it signed. Once complete, the API sends the transaction via the normal process.

So the transaction will pop up the Polkadot extension for confirming the transaction and once the transaction is completed we can see block hash as logs in the browser’s console.

Approve

The approve method allows the caller of the contract method to authorize or approve the spender’s account to withdraw instances of the token from the owner’s account.

const fromAcc = ownerAccount
const injector = await web3FromSource(fromAcc.meta.source);
await contract.tx.approve({value, gasLimit}, spenderAddress, amount)
.signAndSend(fromAcc.address, {signer: injector.signer}, ({status}) => {
if(status.isFinalized) {
console.log(`Finalized. Block hash: ${status.asFinalized.toString()}`);
setLoading('');
} else {
console.log(`Current transaction status: ${status.type}`);
}
})

Here we are passing spenderAddress, which is the the account we want to approve, to spend tokens on owner’s behalf and amount is the amount of tokens upto which spenderAddress can spent.

You can also check how much token the owner has approved to the spender’s address by calling the read-only function allowance().

const {result,output} = await contract.query.allowance(userWallet, { value, gasLimit}, ownerAddress, spenderAddress);console.log(output.toHuman()) // –> Return the amount of token // previously approved

Here ownerAddress is the address who is allowing spenderAddress to spend on his behalf, spenderAddress is the address whose account we approved to spend on owner’s behalf, and userWallet is the wallet that we are using to perform the query.

Third-party transfer

transferFrom() function allows the calling account (spender) to transfer tokens to a receiving account on behalf of token owner account (actual token holder).

The spender must be allowed by the owner to spend this particular amount of tokens (being transferred) in order to execute this function successfully.

const fromAcc = spenderAccount; // logged in from spender account
const injector = await web3FromSource(fromAcc.meta.source);
await contract.tx.transferFrom({value, gasLimit},fromAddress,toAddress,amount)
.signAndSend(fromAcc.address, {signer: injector.signer}, ({status}) => {
if(status.isFinalized) {
console.log(`Finalized. Block hash: ${status.asFinalized.toString()}`);
setLoading('');
} else {
console.log(`Current transaction status: ${status.type}`);
}
})

In the transferFrom function:

fromAddress: is the owner’s address on whose behalf we are spending the funds.
toAddress: is the target address where we are sending the funds

Once the transaction is completed, block hash is logged on the browser’s console, which can also be queried in the explorer on the Polkadot.js app.

Conclusion

In this Part 5 of the A-Z of building dApps on ICE series, we have learned about building a dApp using Polkadot.js to interact with ERC20 Ink! smart contract deployed on the Arctic network. We should now, be able to build client side dApps for any other Ink! smart contracts deployed on ICE network.

--

--

ICONOsphere

With a deep history of commitment to building projects that foster community growth & personal empowerment we work to create a nation built on trust with icon.