Cronos developer series: build a simple Dapp with React, Crypto.com DeFi Wallet and MetaMask
Cronos is EVM compatible, which means that you can port any Dapp from another Ethereum-compatible chain to Cronos. This is excellent news…
Update: While this blog post and associated repository may now be slightly outdated, you can always refer to the Cronos Wallet Connections repository for up to date code and explanations.
Cronos is EVM compatible, which means that you can port any Dapp from another Ethereum-compatible chain to Cronos. This is excellent news for you, Solidity developer!
The Dapp must be configured to connect to Cronos and allow users to log in and sign transactions. Dapps may use two of the world’s most widely used crypto wallets, MetaMask and Crypto.com’s DeFi Wallet.
In this blog post, we will review a simple Dapp that connects to Cronos. You can play with the code available at this Github repository to create a user experience that works for you.
Please note that this is a guest post. It it is not endorsed by Cronos, MetaMask or Crypto.com.
Goals of this tutorial
As part of this tutorial, we are going to build a very simple Dapp that shows information and balances from the Cronos chain.
The Dapp will showcase 3 possible ways that the end-user can log in, for demonstration purposes. The following options differ by device type and login user experience, and you can select those that feel right for your Dapp:
1] Log in with MetaMask (in desktop browser, or within the in-app browser in the mobile app).
2] Log in with Crypto.com Wallet Extension in desktop browser (the connection approval takes place in the Crypto.com DeFi Wallet mobile app).
3] Log in with Crypto.com DeFi Wallet via Wallet Connect. This works both in the browser and in the DeFi Wallet’s mobile in-app browser.
Note: More login options are under development by wallet providers, for example native login in the Crypto.org desktop wallet and in the Crypto.com DeFi Wallet. They will be covered by future posts.
Pre-requisites
You need to have node version 14+ installed on your machine, for example using nvm. We will use React, Redux, and Typescript; therefore, it helps if you are familiar with these libraries and languages.
To play with the Dapp as a user, you need to install:
Crypto.com DeFi Wallet on your mobile device
Crypto.com Wallet Extension on your Chrome, Brave or Edge browser. Click on the extension and scan the QR code to connect the extension to your Crypto.com DeFi Wallet app.
MetaMask browser extension (on Chrome, Brave, or Edge) and mobile app
If you’d like, you can create a wallet in the DeFi Wallet app first, and then import your seed phrase to the MetaMask wallet so that you can use the same account in both wallets. Or vice versa. But remember that you should never share your seed phrase with anyone to keep your accounts secure!
To create test transactions on the Cronos Testnet, you will need some Test CRO tokens. Visit the Cronos Testnet Faucet, and enter your public account address, to receive the Test CROs.
Let’s run the Dapp
First things first, let’s run the Dapp on your computer !
You must clone the repository to your machine and open a Terminal in the root folder of that repository. Enter the command npm install
and then npm start
. Then, open your browser at URL https://127.0.0.1:3000.
Make sure that your wallet apps are connected to Cronos Testnet
. If Cronos Testnet
is not yet set-up in your MetaMask wallet, don’t worry, the Dapp will configure it automatically.
The Dapp’s Connect Wallet
button allows you to connect to the Dapp using various methods, as shown below:
Once you are connected, the Dapp shows some basic information regarding the chain that we are connected to, and the tokens that you own, as shown below:
CTOK is actually a dummy ERC20 token deployed to the Cronos Testnet, that anyone can mint in order to increase their balance whenever they want.
In the Dapp, you can click Mint 1 CTOK for Myself
and then click the Refresh Balance
button after a few seconds in order to see your updated balance.
If you want to test the Dapp on your mobile device, you need a way to tunnel the local port 127.0.0.1:3000 to an external web address that you can visit from the in-app browser of your wallet app. For tunneling we use ngrok, but a detailed explanation of ngrok would make this post too long, so we will leave this to more experienced users. Also, note that DeFi Wallet does not support the in-app browser on Tesnet, so you’ll need to use Cronos Mainnet to test the mobile experience once you have completed the rest of this tutorial.
Cronos network endpoints
Crypto.org exposes public JSON-RPC endpoints to access Cronos Mainnet and Cronos Testnet. The configuration values are shown in the config/config.ts
file.
Here, we will be using Cronos Testnet. Hence, the rpcNetwork_mainnet
values are provided only for reference.
// config/config.ts
export const configVars = {
rpcNetwork: {
rpcUrl: "https://evm-t3.cronos.org/",
chainId: 338,
chainIdHex: "0x152",
chainName: "Cronos Testnet",
chainType: "mainnet",
nativeCurrency: {
name: "CRO",
symbol: "CRO",
decimals: 18,
},
blockExplorerUrl: "https://cronos.crypto.org/explorer/testnet3/",
},
rpcNetwork_mainnet: {
rpcUrl: "https://evm.cronos.org/",
chainId: 25,
chainIdHex: "0x19",
chainName: "Cronos Mainnet Beta",
chainType: "mainnet",
nativeCurrency: {
name: "CRO",
symbol: "CRO",
decimals: 18,
},
blockExplorerUrl: "https://cronoscan.com/",
},
If you expect high transaction volume in your Dapp, you will probably want to run your own full Cronos node instead of the publicly available one, to avoid rate limits.
Code review: wallet connection
In the world of Dapps, a Web3 provider is an object that can be called by the application to read the blockchain or send transactions to it.
Our goal is to create a Web3 provider. We could use one of the two widely used Javascript libraries, web3.js or ethers.js. Here, we use ethers.
We will store the wallet connection in the Redux store of the React application:
// Interfaces.tsx
export interface IWallet {
walletProviderName: string; // for example, "metamask" or "defiwallet"
address: string; // 0x address of the user
browserWeb3Provider: any; // Web3 provider connected to the wallet's browser extension
serverWeb3Provider: ethers.providers.JsonRpcProvider | null; // cloud based Web3 provider for read-only
wcConnector: any; // connector object provided by some wallet connection methods, stored if relevant
wcProvider: any; // provider object provided by some wallet connection methods, stored if relevant
connected: boolean; // is the wallet connected to the Dapp, or not?
chainId: number; // for example, 25 for Cronos mainnet, and 338 for Cronos testnet
}
The general approach, when connecting to a wallet, is to access a provider
object that is exposed by the wallet’s SDK, and then to use ethers.providers.JsonRpcProvider(provider)
in order to generate a Web3 provider that will be called by the Dapp for blockchain read
and write
.
MetaMask handles read
and write
blockchain calls slightly differently. For example, on Ethereum Mainnet, MetaMask exposes its own provider for write
transactions, but if the Dapp needs to read large amounts of blockchain data, it needs to call its own Infura or Alchemy account. That is the reason why we store two distinct Web3 providers in the IWallet object: a browser-based Web3 provider browserWeb3Provider
for write
transactions, and a server-based Web3 provider serverWeb3Provider
that connects directly to the blockchain network’s RPC endpoint for read
calls.
All of this will be shown with more details below.
The Connect Wallet
button is located in the Header component of the React application. The following code excerpt shows that the login flow is executed by distinct helpers depending on the wallet selected by the user. See the repository for the full code.
// Header.tsx
// These are the wallet SDK helpers
import * as walletMetamask from "../helpers/wallet-metamask";
import * as walletDefiwallet from "../helpers/wallet-defiwallet";
import * as walletConnect from "../helpers/wallet-connect";
...
const handleClickConnect = async (option: string) => {
let newWallet: any;
switch (option) {
// Wallet injected within browser (MetaMask)
case "metamask-injected":
newWallet = await walletMetamask.connect();
break;
// Crypto.com DeFi Wallet Extension (browser)
case "defiwallet":
newWallet = await walletDefiwallet.connect();
break;
// Crypto.com DeFi Wallet mobile app (via Wallet Connect)
case "wallet-connect":
newWallet = await walletConnect.connect();
break;
}
...
<MenuItem
onClick={() => {
handleClickConnect("metamask-injected");
}}
disableRipple
>
MetaMask (browser / mobile)
</MenuItem>
<MenuItem
onClick={() => {
handleClickConnect("defiwallet");
}}
disableRipple
>
Crypto.com Wallet Extension (browser)
</MenuItem>
<MenuItem
onClick={() => {
handleClickConnect("wallet-connect");
}}
disableRipple
>
Wallet Connect (browser / mobile)
</MenuItem>tine9YONDER_signal
1] MetaMask (browser / mobile)
For MetaMask, the login flow is located in the helpers/wallet-metamask.ts
helper:
// wallet-metamask.ts
// Injected wallet
// Works with MetaMask in browser or in in-app browser
import { ethers } from "ethers"; // npm install ethers
import { IWallet, defaultWallet } from "../store/interfaces";
import * as utils from "./utils";
import * as config from "../config/config";
// One feature of MetaMask is that the Dapp developer
// can programmatically
// change the network that the browser
// extension is connected to.
// This feature is implemented below,
// to automatically set - up Cronos
export const switchNetwork = async () => {
try {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: config.configVars.rpcNetwork.chainIdHex }],
});
} catch (e) {
console.log(e);
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [
{
chainId: config.configVars.rpcNetwork.chainIdHex,
chainName: config.configVars.rpcNetwork.chainName,
rpcUrls: [config.configVars.rpcNetwork.rpcUrl],
nativeCurrency: config.configVars.rpcNetwork.nativeCurrency,
blockExplorerUrls: [config.configVars.rpcNetwork.blockExplorerUrl],
},
],
});
}
};
// Main login flow for injected wallet like MetaMask
export const connect = async (): Promise<IWallet> => {
try {
let chainId = await window.ethereum.request({ method: "eth_chainId" });
if (!(chainId === config.configVars.rpcNetwork.chainIdHex)) {
await switchNetwork();
await utils.delay(2000);
return defaultWallet;
}
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
// It is possible to subscribe to events chainChanged,
// accountsChanged or disconnect,
// and reload the Dapp whenever one of these events occurs
window.ethereum.on("chainChanged", utils.reloadApp);
window.ethereum.on("accountsChanged", utils.reloadApp);
window.ethereum.on("disconnect", utils.reloadApp);
return {
...defaultWallet,
walletProviderName: "metamask",
address: accounts[0],
browserWeb3Provider: new ethers.providers.Web3Provider(window.ethereum),
serverWeb3Provider: new ethers.providers.JsonRpcProvider(
config.configVars.rpcNetwork.rpcUrl
),
connected: true,
chainId: utils.hexToInt(
await window.ethereum.request({ method: "eth_chainId" })
),
};
} catch (e) {
window.alert(e);
return defaultWallet;
}
};
2] Crypto.com DeFi Wallet Extension (browser)
For Crypto.com DeFi Wallet using the Wallet Extension on browser, the login flow is located in the helpers/wallet-defiwallet
helper:
// wallet-defiwallet.ts
import { ethers } from "ethers"; // npm install ethers
// This is the SDK provided by Crypto.com DeFi Wallet
import { DeFiWeb3Connector } from "deficonnect"; // npm install deficonnect
import * as config from "../config/config";
import * as utils from "./utils";
import { IWallet, defaultWallet } from "../store/interfaces";
// Main login flow for Crypto.com DeFi Wallet with Wallet Extension
// The connector must be activated, then it exposes a provider
// that is used by the ethers Web3Provider constructor.
export const connect = async (): Promise<IWallet> => {
try {
const connector = new DeFiWeb3Connector({
supportedChainIds: [config.configVars.rpcNetwork.chainId],
rpc: {
[config.configVars.rpcNetwork.chainId]:
config.configVars.rpcNetwork.rpcUrl,
},
pollingInterval: 15000,
});
await connector.activate();
const provider = await connector.getProvider();
const web3Provider = new ethers.providers.Web3Provider(provider);
if (
!(parseInt(provider.chainId) === config.configVars.rpcNetwork.chainId)
) {
window.alert(
"Switch your Wallet to blockchain network " +
config.configVars.rpcNetwork.chainName
);
return defaultWallet;
}
connector.on("session_update", utils.reloadApp);
connector.on("Web3ReactDeactivate", utils.reloadApp);
connector.on("Web3ReactUpdate", utils.reloadApp);
return {
...defaultWallet,
walletProviderName: "defiwallet",
address: (await web3Provider.listAccounts())[0],
browserWeb3Provider: web3Provider,
serverWeb3Provider: new ethers.providers.JsonRpcProvider(
config.configVars.rpcNetwork.rpcUrl
),
wcProvider: provider,
wcConnector: connector,
connected: true,
chainId: provider.chainId,
};
} catch (e) {
window.alert(e);
return defaultWallet;
}
};
3] Wallet Connect (browser / mobile)
For Crypto.com DeFi Wallet using Wallet Connect, the login flow is located in the helpers/wallet-connect
helper. It works both in the browser and on mobile.
// wallet-connect.ts
import { ethers } from "ethers"; // npm install ethers
// This is the SDK provided by Wallet Connect
import WalletConnectProvider from "@walletconnect/web3-provider";
import * as config from "../config/config";
import * as utils from "./utils";
import { IWallet, defaultWallet } from "../store/interfaces";
// Main login flow for Crypto.com DeFi Wallet with Wallet Extension
// The connector must be activated, then it exposes a provider
// that is used by the ethers Web3Provider constructor.
export const connect = async (): Promise<IWallet> => {
try {
// Reset cache
localStorage.clear();
const provider = new WalletConnectProvider({
rpc: {
[config.configVars.rpcNetwork.chainId]:
config.configVars.rpcNetwork.rpcUrl,
},
// This chainId parameter is not mentioned
// in the WalletConnect documentation,
// But is necessary otherwise
// WalletConnect will connect to Ethereum mainnet
chainId: config.configVars.rpcNetwork.chainId,
});
await provider.enable();
const ethersProvider = new ethers.providers.Web3Provider(provider);
if (!(provider.chainId === config.configVars.rpcNetwork.chainId)) {
window.alert(
"Switch your Wallet to blockchain network " +
config.configVars.rpcNetwork.chainName
);
return defaultWallet;
}
// Subscribe to events that reload the app
provider.on("accountsChanged", utils.reloadApp);
provider.on("chainChanged", utils.reloadApp);
provider.on("disconnect", utils.reloadApp);
return {
...defaultWallet,
walletProviderName: "walletconnect",
address: (await ethersProvider.listAccounts())[0],
browserWeb3Provider: ethersProvider,
serverWeb3Provider: new ethers.providers.JsonRpcProvider(
config.configVars.rpcNetwork.rpcUrl
),
wcProvider: provider,
connected: true,
chainId: provider.chainId,
};
} catch (e) {
window.alert(e);
return defaultWallet;
}
};
Reading the blockchain
We can easily read the blockchain thanks to the serverWeb3Provider
that is connected directly to the blockchain network’s RPC endpoint.
The serverWeb3Provider
does not use the wallet connector, since there is no transaction signature required. If the Web3 Provider needs to know the user’s address, it can simply take it from the wallet.address
field in the Redux store, which was updated at the time of login.
The following code excerpt shows how the Dapp reads the latest block’s number and the CRO and CTOK balances of the user using the standard ethers.js methods:
// helper/utils.ts
import { ethers } from "ethers"; // npm install ethers
import * as config from "../config/config";
import * as ERC20Json from "../config/contracts/MyERC20MintableByAnyone.json";
// Get the last block number
export const getLastBlockNumber = async (ethersProvider: any): Promise<any> => {
return ethersProvider.getBlockNumber();
};
// Get the CRO balance of address
export const getCroBalance = async (
serverWeb3Provider: ethers.providers.JsonRpcProvider,
address: string
): Promise<number> => {
const balance = await serverWeb3Provider.getBalance(address);
// Balance is rounded at 2 decimals instead of 18, to simplify the UI
return (
ethers.BigNumber.from(balance)
.div(ethers.BigNumber.from("10000000000000000"))
.toNumber() / 100
);
};
// Get the CTOK token balance of address
// The CTOK is a ERC20 smart contract, its address is retrieved from
// the config/config.ts file
// and the ABI from config/contracts/MyERC20MintableByAnyone.json
export const getBalance = async (
serverWeb3Provider: ethers.providers.JsonRpcProvider,
address: string
): Promise<number> => {
const contractAbi = ERC20Json.abi;
// Create ethers.Contract object using the smart contract's ABI
const readContractInstance = new ethers.Contract(
config.configVars.erc20.address,
contractAbi,
serverWeb3Provider
);
const contractResponse = await readContractInstance["balanceOf"](address);
// Balance is rounded at 2 decimals instead of 18, to simplify UI
return (
ethers.BigNumber.from(contractResponse)
.div(ethers.BigNumber.from("10000000000000000"))
.toNumber() / 100
);
};
The above read
helpers are called by the Dapp at the time of login, as well as when the user clicks the Refresh Balance
button. For example, you can see how the Refresh Balance
button works here:
// Welcome.tsx
const refreshQueryResults = async () => {
updateRefreshingAction(dispatch, {
status: true,
message: "Querying chain data...",
});
const lastBlockNumber = await utils.getLastBlockNumber(
state.wallet.serverWeb3Provider
);
const croBalance = await utils.getCroBalance(
state.wallet.serverWeb3Provider,
state.wallet.address
);
const erc20Balance = await utils.getBalance(
state.wallet.serverWeb3Provider,
state.wallet.address
);
updateRefreshingAction(dispatch, {
status: false,
message: "Complete",
});
updateQueryResultsAction(dispatch, {
...state.queryResults,
lastBlockNumber: lastBlockNumber,
croBalance: croBalance,
erc20Balance: erc20Balance,
});
};
Sending transactions
In order to sign a blockchain transaction, we must use the browserWeb3Provider
that is connected directly to the wallet extension (MetaMask of DeFi Wallet).
First, we use browserWeb3Provider
to create a ethers.Contract
object that is connected to the ERC20 smart contract and to the wallet’s signer:
// helper/utils.ts
// Generate a ethers.Contract instance of the contract object
// together with a signer that will trigger a transaction
// approval in the wallet whenever it is called by the Dapp
export const getWriteContractInstance = async (
browserWeb3Provider: any
): Promise<ethers.Contract> => {
const ethersProvider = browserWeb3Provider;
const contractAbi = ERC20Json.abi;
// Create ethers.Contract object using the smart contract's ABI
const readContractInstance = new ethers.Contract(
config.configVars.erc20.address,
contractAbi,
ethersProvider
);
// Add a signer to make the ethers.Contract object able to
// craft transactions
const fromSigner = ethersProvider.getSigner();
return readContractInstance.connect(fromSigner);
};
Then we use the mint
function that is exposed by the smart contract (via ethers.js) in order to request the minting of 1 CTOK for the user. The smart contract balances have 18 decimals here, hence we are minting 1,000,000,000,000,000,000 units of the token.
// Welcome.tsx
const sendTransaction = async () => {
updateRefreshingAction(dispatch, {
status: true,
message: "Sending transaction...",
});
const erc20WriteContractInstance = await utils.getWriteContractInstance(
state.wallet.browserWeb3Provider
);
const tx = await erc20WriteContractInstance["mint"](
state.wallet.address,
"1000000000000000000"
);
updateRefreshingAction(dispatch, {
status: false,
message: "Complete",
});
updateQueryResultsAction(dispatch, {
...state.queryResults,
lastTxHash: tx.hash,
});
};
Next steps
Go ahead and clone the entire Github repository. You can review it in more details to fully understand the login and transaction signing flows!
You can also test the Dapp on the Cronos Mainnet, simply by switching the configuration details in the config/config.ts file (network and contract address). For testing, do not forget to switch your DeFi Wallet to Mainnet in the browser extension as well as the mobile app.
Of course, this Dapp is a basic demo. Your Dapp will require more design and fine tuning if you want to offer a top-notch user experience. Check the repositories of some apps of the Cronos ecosystem like VVS Finance and Beefy Finance to find out how they have addressed similar challenges!
Finally, there are some great frameworks that developers can leverage in order to re-use frequently used connection methods, for example web3modal and web3-react. You can customize these frameworks to connect to the Cronos custom JSON-RPC network endpoints.
Feel free to post a pull request or an issue in Github if you would like to suggest improvements regarding this demo.