3D web3 Series
This is the last post of 3D-web3 Series.
1 - Vite config and basic three.js
2 - Three.js (fiber & drei)
3 - Cannon physics
4 - 3D web - react-web3
The stable version is v6, and currently v8 is in beta.
Is developed by Uniswap Engineering Lead Noah Zinsmeister.
At a high level, web3-react is a state machine which ensures that certain key pieces of data (the user's current account, for example) relevant to your dApp are kept up-to-date. To this end, web3-react uses Context to efficiently store this data, and inject it wherever you need it in your application.
Web3-react v6 uses Context to efficiently store this data, and inject it wherever you need it in your application.
Useful links:
Uniswap/web3-react source code
Uniswap/web3-react documents
How to Use Web3React in Your Next Project
There are other several libraries to create a web3 enviroment as:
Rainbowkit || Wagmi || Scaffold-eth || useDApp || web3modal || Web3-UI
We are using Ethers.js behind the scene. In order to maintain context and connect with blockchain easily in our DApp. Also to connect different kind of blockchain provider, wallet or chain and to query blockchain more efficiently.
Anyway, It is possible to build directly entire DApp using ethers.
We'll be using:
MaskMask wallet on the user side
Ethereum web client libraries - "@web3-react", "ethersproject"
- From "@web3-react": Web3ReactProvider, context, useWeb3React, hooks, InjectedConnector. To connect wallet and get data from blockchain. (built in top of "ethers")
- From "@ethersproject": Contract, Web3Provider. To send transactions to the blockchain.
- To listen broadcasted events from provider we're using "events" library.
npm i @web3-react/core @web3-react/injected-connector
npm i @ethersproject/contracts @ethersproject/providers
npm i events
In this demo, we are deploying same token to BSC testnet and Mumbai (Polygon testnet).
First, we need to have metamask, TrustWallet (WalletConnect)or coinbase extension installed in our browser
@Notice WalletConnect establishes an encrypted connection between your wallet and the DApp. Use it with Ex. with "Trust Wallet"
You can check all css details in git. This article focuses on the web3 connection.
Step 1_ Create a web3 context to all component childs
Add context provider
web3-react relies on the existence of a Web3ReactProvider at the root of your application.
It requires a single getLibrary prop which is responsible for instantiating a web3 convenience library object from a low-level provider.
import React, { useEffect } from 'react';
import { Web3ReactProvider } from '@web3-react/core'
import { Web3Provider } from '@ethersproject/providers'
function getLibrary(provider) {
const library = new Web3Provider(provider)
library.pollingInterval = 12000
return library
}
function Web3ContextProvider({ children }) {
return (
<Web3ReactProvider getLibrary={getLibrary}>
{children}
</Web3ReactProvider>
)
}
export default Web3ContextProvider
Add to App.jsx
import Web3ContextProvider from './web3/Web3ContextProvider';
import ConnectWallet from './web3/ConnectWallet';
return (
...
<Web3ContextProvider style={{ height: '15vh' }} className='header'>
<ConnectWallet />
</Web3ContextProvider>
...
)
Step 2_ Instantiate Web3 object and define methods
Create a ConnectWallet.jsx
Use "useWeb3React" to connect to blockchain using "InjectedConnector".
With MetaMask you can reach a provider by "windows.ethereum".
Provide a button to connect and disconnect wallet and another one to change current chain.
import { useEffect } from 'react'
import { useWeb3React } from '@web3-react/core'
import { InjectedConnector } from '@web3-react/injected-connector'
import "../App.css"
import "../Button.css"
import "../Select.css"
import { changeChainById } from "./transaction/chains"
import ClaimToken from "./ClaimToken"
const ConnectWallet = () => {
const injectedConnector = new InjectedConnector({
supportedChainIds: [1, 97, 80001],
})
const { chainId, account, activate, active, library, deactivate, connector } = useWeb3React()
const activateWallet = () => {
activate(injectedConnector)
}
const deactivateWallet = () => {
deactivate(injectedConnector)
}
const changeChain = (_chainID) => {
changeChainById(_chainID)
}
useEffect(() => {
if (!chainId) return
document.getElementById('select-form').value = chainId
}, [chainId])
return (
<main className="web3-navbar">
<h2 >Welcome to 3D web3 series</h2>
<div className='connect-box'>
<b>ChainId: {chainId}</b>
<div>Account: {account}</div>
{active ? (
<button type="button" className='button-4' onClick={deactivateWallet}>
Disconnect
</button>
) : (
<button type="button" className='button-3' onClick={activateWallet}>
Connect Wallet
</button>
)}
</div>
<div className='box'>
<select id='select-form' onChange={e => {
let _chainID = e.target.value
changeChain(_chainID)
}}>
<option key={1} value={1}>Ethereum Chain</option>
<option key={97} value={97}>BSC testnet</option>
<option key={80001} value={80001}>Mumbai testnet</option>
</select>
</div>
<div>
<ClaimToken
account={account}
chainId={chainId}
/>
</div>
</main>
)
}
export default ConnectWallet
Step 3_ Methods to add and change between Chains.
To switch between diferent blockchains we're using metamask RPC API's has built methods.
Call "wallet_switchEthereumChain" method to request user to change chain.
In case user hasn't that specific chain configured, we catch and call "wallet_addEthereumChain" method to request user to add selected chain.
@Notice. Use tryCatch. There are some recurrent errors to handle it here
In order to change/add chain, we have to provide chain info. Check in code.
export const changeChainById = async (chainID) => {
if (!window.ethereum)
return alert("install metamask extension in your browser");
try {
await ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: chains[chainID].chainId }],
});
} catch (switchError) {
// This error code indicates that the chain has not been added to MetaMask.
if (switchError.code === 4902) {
try {
await ethereum.request({
method: "wallet_addEthereumChain",
params: [chains[chainID]],
});
} catch (addError) {
console.log("error: ", addError);
if (ex.code === 32002)
return alert("already pending request from user in metamask");
else
return alert(
"Disconnect wallet from metamask configuration and try again!"
);
}
}
// handle other "switch" errors
}
return;
};
const ETH = {
name: "Ether",
symbol: "ETH",
decimals: 18,
};
const MATIC = {
name: "Matic",
symbol: "MATIC",
decimals: 18,
};
const BNB = {
name: "Binance",
symbol: "BNB",
decimals: 18,
};
const chains = {
1: {
chainId: "0x1",
chainName: "Ethereum mainnet",
nativeCurrency: ETH,
rpcUrls: [
import.meta.env.VITE_APP_INFURA_KEY
? `https://mainnet.infura.io/v3/${import.meta.env.VITE_APP_INFURA_KEY}`
: undefined,
import.meta.env.VITE_APP_ALCHEMY_KEY
? `https://eth-mainnet.alchemyapi.io/v2/${
import.meta.env.VITE_APP_ALCHEMY_KEY
}`
: undefined,
"https://cloudflare-eth.com",
].filter((url) => url !== undefined),
blockExplorerUrls: ["https://etherscan.com/"],
},
97: {
chainId: "0x61",
chainName: "Binance Testnet",
nativeCurrency: BNB,
rpcUrls: [
"https://data-seed-prebsc-1-s1.binance.org:8545/",
"https://data-seed-prebsc-2-s1.binance.org:8545/",
"http://data-seed-prebsc-1-s2.binance.org:8545/",
"https://data-seed-prebsc-2-s3.binance.org:8545/",
],
// rpcUrls: 'https://data-seed-prebsc-1-s1.binance.org:8545',
blockExplorerUrls: ["https://testnet.bscscan.com/"],
},
80001: {
chainId: "0x13881",
chainName: "Polygon Mumbai",
nativeCurrency: MATIC,
rpcUrls: [
import.meta.env.VITE_APP_INFURA_KEY
? `https://polygon-mumbai.infura.io/v3/${
import.meta.env.VITE_APP_INFURA_KEY
}`
: undefined,
].filter((url) => url !== undefined),
blockExplorerUrls: ["https://mumbai.polygonscan.com/"],
},
};
Step 4_ Define transactions
Create a ClaimToken.jsx component to define UI.
import { burnToken, claimToken } from './transaction/transaction'
import "../App.css"
export default function TransactionMetaMask(props) {
const claimTokenTx = () => {
if (props.chainId === 97 || props.chainId === 80001) {
claimToken(props.provider, props.account, props.chainId, 1)
} else {
scrollTo(0, 0)
alert('Tokens are only available in BSC and Polygon testnets')
}
}
const burnTokenTx = () => {
if (props.chainId === 97 || props.chainId === 80001) {
burnToken(props.provider, props.account, props.chainId, 1)
} else {
scrollTo(0, 0)
alert('Tokens are only available in BSC and Polygon testnets')
}
}
return (
<div className='token-buttons'>
<button type="button" className='button-3' onClick={claimTokenTx}>
Claim Token
</button>
<button type="button" className='button-3' onClick={burnTokenTx}>
Burn Token
</button>
</div>
)
}
To be able to send a transaction to modify blockchain data, import a provider directly from "@ethersproject/providers" to be able to create "signer" object.
Now, using smart contract address, ABI and signer, create "Contract" object (ready to interact with contract methods)
import { Contract } from "@ethersproject/contracts";
import { Web3Provider } from "@ethersproject/providers";
// Same ABI for all SC living in EVM compatible networks
export const contractAbi = [...];
const contractsAddress = {
80001: "0x41e6913ce749018910e45980996dac1f99012c96", // MUMBAI
97: "0x6ec4c5ce6cc67729d89785f715e103e5981c9780", // BSC Test
};
// TODO
export const getContract = (chainId) => {
// using ethersproject to set signer using default provider
const provider = new Web3Provider(window.ethereum);
const signer = provider.getSigner();
const contractAddress = contractsAddress[chainId];
const contract = new Contract(contractAddress, contractAbi, signer);
return contract;
};
Last, send asyncronous RPC and catch all the errors.
import { getContract } from "./contract";
// writeToContractUsingWeb3React
const claimToken = async (account, chainId, amount) => {
try {
const myContract = getContract(chainId);
// Metamask calculates gas, but, for walletConnect and coinbase we need to set gas limit
const overrides = {
gasLimit: 230000,
};
const txResponse = await myContract.mint(account, amount, overrides);
const txReceipt = await txResponse.wait();
console.log(txReceipt);
// alert(txReceipt);
} catch (ex) {
console.log(ex);
if (ex.code === 32002)
return alert("already pending request from user in metamask");
if (ex.code === 4001) return alert("User denied transaction signature");
return alert('"Connect / Disconnect" your wallet and try again.');
}
};
const burnToken = async (chainId, amount) => {
try {
const myContract = getContract(chainId);
// Metamask calculates gas, but, for walletConnect and coinbase we need to set gas limit
const overrides = {
gasLimit: 230000,
};
const txResponse = await myContract.burn(amount, overrides);
const txReceipt = await txResponse.wait();
console.log(txReceipt);
// alert(txReceipt);
} catch (ex) {
console.log(ex);
if (ex.code === 32002)
return alert("already pending request from user in metamask");
if (ex.code === 4001) return alert("User denied transaction signature");
return alert('"Connect / Disconnect" your wallet and try again.');
}
};
export { claimToken, burnToken };
Now, fork git repo and try localy.
npm install
// add to .env.local
// VITE_APP_INFURA_KEY
// VITE_APP_ALCHEMY_KEY
npm run dev
Or check Code Sand Box demo.
RPC provider could be disabled time to time.
In this case, provide your own free RPC API from infura or alchemy
BSC testnet token address, bscscan
Mumbai token address, polygonscan
I hope it has been helpful.
Top comments (8)
Thank you for sharing!
On the past I did some videogames with visuals and sounds for the daughter of a friend and I had fun doing it.
I've been recently interested in Three.js to develop some silly videogame and get the experience for fun, any advice on this topic would be appreciated π
Check previous post about @react-three/cannon
I'll sure do, thank you! π
Sure! I recomend you to check cannon docs to size which possibilities you have to develop game mechanics. Also, don't use too many animated (using useFrame hook) geometries due to performance.
Thank you for the info! π
I was planning not to use React but bare bones three.js for this topic, is it a good or bad idea?
Depends on how complex you want to do it. Using react, you can use "fiber", "drei" and "cannon" libraries built in top of Threejs. It makes it much easier but is not as versatile as three (for some mechanics you'll have to use three). If you have enough time I recommend first to structure your ideas to see if it's possible to build it using mentioned high level libraries. I hope it helps you :)
Thank you so much! π
Thanks, I'll check it!