Best Practices for Ethereum developers on NEAR
In this example, we will create an Ethereum dApp on NEAR that functions as a portfolio manager, displaying the current balances for a list of tokens. Additionally, we will display current market value of each asset in the portfolio.
We will be using several technologies:
- NEAR Components for the user interface (UI).
- Ethers.js for retrieving balance data from the blockchain.
- CoinGecko API for fetching static content with information about tokens and their current prices.
- Social-DB for storing the list of tokens to be tracked.
- GitHub Actions for caching static content, speeding up loading, and circumventing rate limits.
Step 1: Load balances from chain
Let's start with a simple example and consider an application where we want to display a user's balances for multiple tokens.
Source code
// Load current sender address if it was not loaded yet
if (state.sender == undefined && Ethers.provider()) {
Ethers.provider()
.send("eth_requestAccounts", [])
.then((accounts) => {
if (accounts.length) {
// save sender address to the state
State.update({ sender: accounts[0] });
}
});
}
// Load ERC20 ABI JSON
const erc20Abi = fetch(
"https://ipfs.near.social/ipfs/bafkreifgw34kutqcnusv4yyv7gjscshc5jhrzw7up7pdabsuoxfhlnckrq"
);
if (!erc20Abi.ok) {
return "Loading";
}
// Create contract interface
const iface = new ethers.utils.Interface(erc20Abi.body);
// specify list of tokens
const tokens = [
"0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", // WBTC
"0x6b175474e89094c44da98b954eedeac495271d0f", // DAI
"0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", // UNI
];
// load receiver's balance for a giver token
const getTokenBalance = (receiver, tokenId) => {
// encode `balanceOf` request
const encodedData = iface.encodeFunctionData("balanceOf", [receiver]);
// send request to the network
return Ethers.provider()
.call({
to: tokenId,
data: encodedData,
})
.then((rawBalance) => {
// decode response
const receiverBalanceHex = iface.decodeFunctionResult(
"balanceOf",
rawBalance
);
return Big(receiverBalanceHex).toFixed(0);
});
};
const loadTokensData = () => {
// load balances of all tokens
tokens.map((tokenId) => {
getTokenBalance(state.sender, tokenId).then((value) => {
// save balance of every token to the state
State.update({ [tokenId]: { balance: value, ...state[tokenId] } });
});
});
};
const renderToken = (tokenId) => (
<li>
{tokenId}: {state[tokenId].balance}
</li>
);
if (state.sender) {
loadTokensData();
return (
<>
<ul>{tokens.map((tokenId) => renderToken(tokenId))}</ul>
<p>Your account: {state.sender} </p>
</>
);
} else {
// output connect button for anon user
return <Web3Connect />;
}
You can see how it works here: step_1.
Once the web3 connection is enabled, the output will appear as follows:
0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599: 726220
0x6b175474e89094c44da98b954eedeac495271d0f: 140325040242585301886
0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984: 127732731780832810
When developing NEAR components, it's recommended to always present some content even if the user hasn't connected their wallet yet. In this example, the component uses the <Web3Connect>
button to prompt the user to connect their wallet if they haven't already.
Step 2: Load static data
To format the list, we must determine the decimal precision for each asset. While it's possible to retrieve this information from the ERC-20 contract for each token, it's important to note that the ERC-20 contract lacks certain valuable data such as the token icon and description. As a solution, we can leverage the CoinGecko API to retrieve token details, including the current market price.
Let's add a function to load token data for a given token from the Coingecko:
const loadCoingeckData = (tokenId) => {
let dataUrl = `https://api.coingecko.com/api/v3/coins/ethereum/contract/${tokenId}`;
const data = fetch(dataUrl);
if (data.ok) {
return {
name: data.body.name,
icon: data.body.image.small,
decimals: data.body.detail_platforms["ethereum"].decimal_place,
price: Number(data.body.market_data.current_price.usd),
};
}
};
Other available API methods are listed in the Coingecko API documentation.
Now that we have the data, let's modify the loadTokensData function to save the token information in the state:
const loadTokensData = () => {
// load balances of all tokens
tokens.map((tokenId) => {
getTokenBalance(state.sender, tokenId).then((value) => {
// save balance of every token to the state
State.update({ [tokenId]: { balance: value, ...state[tokenId] } });
});
});
tokens.map((tokenId) => {
const tokenData = loadCoingeckData(tokenId);
// save balance of every token to the state
State.update({ [tokenId]: { ...tokenData, ...state[tokenId] } });
});
};
And lets update the renderToken
function to display data we just got:
const renderToken = (tokenId) => {
const tokenBalance = Big(state[tokenId].balance ?? 0)
.div(new Big(10).pow(state[tokenId].decimals ?? 1))
.toFixed(4);
const tokenBalanceUSD = (tokenBalance * state[tokenId].price).toFixed(2);
return (
<li>
{state[tokenId].name}: {tokenBalance}{" "}
<img src={state[tokenId].icon} width="16" alt={state[tokenId].symbol} />
{`(${tokenBalanceUSD} USD)`}
</li>
);
};