DEV Community

Uigla
Uigla

Posted on • Edited on

web3 - 3D Web3 serie

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Add to App.jsx

import Web3ContextProvider from './web3/Web3ContextProvider';
import ConnectWallet from './web3/ConnectWallet';

return (
...
<Web3ContextProvider style={{ height: '15vh' }} className='header'>
                    <ConnectWallet />
</Web3ContextProvider>
...
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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/"],
  },
};

Enter fullscreen mode Exit fullscreen mode

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>
    )
}

Enter fullscreen mode Exit fullscreen mode

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;
};

Enter fullscreen mode Exit fullscreen mode

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 };

Enter fullscreen mode Exit fullscreen mode

Now, fork git repo and try localy.

npm install

// add to .env.local
// VITE_APP_INFURA_KEY
// VITE_APP_ALCHEMY_KEY

npm run dev
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
joelbonetr profile image
JoelBonetR πŸ₯‡

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 😁

Collapse
 
uigla profile image
Uigla

Check previous post about @react-three/cannon

Collapse
 
joelbonetr profile image
JoelBonetR πŸ₯‡

I'll sure do, thank you! 😁

Collapse
 
uigla profile image
Uigla

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.

Collapse
 
joelbonetr profile image
JoelBonetR πŸ₯‡ • Edited

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?

Thread Thread
 
uigla profile image
Uigla

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 :)

Thread Thread
 
joelbonetr profile image
JoelBonetR πŸ₯‡

Thank you so much! 😁

Collapse
 
uigla profile image
Uigla

Thanks, I'll check it!