Skip to content

Building your first dApp

Wrapping and Unwrapping WETH

Introduction

This guide will walk you through developing your first dApp using dappBooster. You'll learn how to create a simple dApp that enables users to wrap and unwrap WETH (in less than five minutes).

Network configuration

The Sepolia network is pre-configured in src/lib/networks.config.ts. Review this file if you're interested in adding more networks by following the existing pattern.

Contract configuration

For this example, we have already deployed a smart contract to the Sepolia network. Now it's time to let the dApp know about it existence:

ABI download

Download the ABI and save it in src/constants/contracts/abis/WETH.ts using the following format:

WETH.ts
export const WETH_ABI = [
  // ...abi code
] as const

Contracts update

Update src/constants/contracts/contracts.ts with the WETH contract address and ABI:

contracts.ts
import { WETH_ABI } from '@/src/constants/contracts/abis/WETH'
 
export const contracts = {
  // ...
  {
    abi: WETH_ABI,
    name: 'WETH',
    address: {
      [sepolia.id]: '0xa45f5716cab1a49b107a5de96ec26da26d1eba9e',
    },
  },
}

Generate the hooks

Use the wagmi utility to generate React hooks for the WETH contract. We have configured wagmi to generate hooks with both suspense and promised hooks.

Terminal
pnpm wagmi-generate

Create the page

Add a new route

Create a new file /src/routes/weth.lazy.tsx with the following content:

weth.lazy.tsx
import { createLazyFileRoute } from '@tanstack/react-router'
import { Weth } from '@/src/components/pageComponents/weth'
 
export const Route = createLazyFileRoute('/weth')({
  component: Weth,
})

With this file we are registering a new page under the path /weth.

Create the component

  • Create a new folder src/components/pageComponents/weth
  • Create src/components/pageComponents/weth/index.tsx with this content:
index.tsx
import { Card } from '@chakra-ui/react'
 
export const Weth = () => {
  return (<Card.Root display="flex" flexDirection="column" m="auto">Hello User!</Card.Root>
}

This is just a simple component that we will use to add all the WETH logic.

You can see the new page and component in action in http://localhost:5173/weth

Add a custom token list

dappBooster has a set of components and utilities designed to simplify the integration of tokens while following the standard defined by tokenlists.org. As we're working with a home-made WETH token, we will create a custom token link where the only token it holds is our WETH token.

Let's add it to our dApp:

Create the tokens file

Create public/tokens.json with the following content:

tokens.json
{
  "name": "custom",
  "tokens": [
    {
      "address": "0xa45f5716cab1a49b107a5de96ec26da26d1eba9e",
      "chainId": 11155111,
      "name": "WETH",
      "symbol": "WETH",
      "decimals": 18,
      "logoURI": "https://assets.coingecko.com/coins/images/2518/thumb/weth.png?1696503332"
    }
  ]
}

Add the tokens list

Add the new tokens list to src/constants/tokenLists.ts like so:

tokenLists.ts
{
  // ...
  CUSTOM: '/tokens.json',
}

Now, all the token related utilities will make use of this new list.

The TokenInput component

We will use the TokenInput component. This component has this useful features:

  • Show the user the token they are interacting with (ETH when wrapping and WETH when unwrapping).
  • It will allow the user to enter the token amount in decimal format and will return the parsed amount in wei.
  • It will fetch the selected token's balance.
  • It will notify the user of any issues found with the input value entered.

Let's modify the /src/components/pageComponents/weth/index.tsx file as follows:

index.tsx
import { Card } from '@chakra-ui/react'
import { sepolia } from 'viem/chains'
import TokenInput from '@/src/components/sharedComponents/TokenInput'
import { useTokenInput } from '@/src/components/sharedComponents/TokenInput/useTokenInput'
import { getContract } from '@/src/constants/contracts/contracts'
import { env } from '@/src/env'
import { useTokenLists } from '@/src/hooks/useTokenLists'
 
export const Weth = () => {
  // get the list of tokens.
  const { tokensByChainId } = useTokenLists()
 
  // helper that give us information given a contract name.
  const weth = getContract('WETH', sepolia.id)
 
  // get the tokens from the token list we need to use.
  const wethToken = tokensByChainId[sepolia.id].find((token) => token.address === weth.address)
  const ethToken = tokensByChainId[sepolia.id].find(
    (token) => token.address === env.PUBLIC_NATIVE_TOKEN_ADDRESS,
  )
  if (!wethToken) throw new Error('WETH token not found')
  if (!ethToken) throw new Error('ETH token not found')
 
  // hook that gives us the state that <TokenInput /> needs.
  const tokenInput = useTokenInput(ethToken)
 
  return (
    <Card.Root display="flex" flexDirection="column" m="auto">
      <p>WETH EXAMPLE</p>
      <TokenInput singleToken tokenInput={tokenInput} />
    </Card.Root>
  )
}

Now, you should see the TokenInput component in action. Connect a wallet with a Sepolia ETH balance, and the component will display it.

Suspense loading

As you might have noticed, after the page is loaded the component will take some time before appearing. This is happening because dappBooster makes use of Suspense for some internal requests.

To improve the UX we can show a spinner or skeleton loadin component until the component is ready to be rendered. We'll use a utility function called withSuspenseAndRetry to wrap our Weth component, and indicate that while the component is fetching data, we want to render a "loading..." text:

index.tsx
import { withSuspenseAndRetry } from '@/src/utils/suspenseWrapper'
 
export const SuspendedWeth = withSuspenseAndRetry(() => {
  // ...
})
 
export const Weth = () => (
  <SuspendedWeth suspenseFallback={<div style={{ margin: 'auto' }}>Loading...</div>} />
)

Wallet status & contract interaction

Now it's time to develop the functionality for deposit and withdrawal.

We will consume different hooks that will take care of triggering the transactions for deposit, approve and withdraw.

Also, we will make use of another special hook useWeb3Status() provided by dappBooster. This hook will return important information of the context of the connected wallet and selected chain.

index.tsx
import { Card } from '@chakra-ui/react'
import { Address } from 'viem'
import { sepolia } from 'viem/chains'
import TokenInput from '@/src/components/sharedComponents/TokenInput'
import { useTokenInput } from '@/src/components/sharedComponents/TokenInput/useTokenInput'
import { getContract } from '@/src/constants/contracts/contracts'
import { env } from '@/src/env'
import { 
  useReadWethAllowance, 
  useWriteWethApprove, 
  useWriteWethDeposit, 
  useWriteWethWithdraw, 
} from '@/src/hooks/generated'
import { useTokenLists } from '@/src/hooks/useTokenLists'
import { useWeb3Status } from '@/src/hooks/useWeb3Status'
import { withSuspenseAndRetry } from '@/src/utils/suspenseWrapper'
 
export const SuspendedWeth = withSuspenseAndRetry(() => {
  // get the list of tokens.
  const { tokensByChainId } = useTokenLists()
 
  // helper that give us information given a contract name.
  const weth = getContract('WETH', sepolia.id)
 
  // get the tokens from the token list we need to use.
  const wethToken = tokensByChainId[sepolia.id].find((token) => token.address === weth.address)
  const ethToken = tokensByChainId[sepolia.id].find(
    (token) => token.address === env.PUBLIC_NATIVE_TOKEN_ADDRESS,
  )
  if (!wethToken) throw new Error('WETH token not found')
  if (!ethToken) throw new Error('ETH token not found')
 
  // hook that gives us the state that <TokenInput /> needs.
  const tokenInput = useTokenInput(ethToken)
 
  // status of the Web3 connection. 
  const { address } = useWeb3Status() 
 
  // contracts calls 
  const { writeContractAsync: wethDeposit } = useWriteWethDeposit() 
  const { writeContractAsync: wethApprove } = useWriteWethApprove() 
  const { writeContractAsync: wethWithdraw } = useWriteWethWithdraw() 
 
  // contracts reads 
  const wethAllowance = useReadWethAllowance({ 
    args: [address as Address, weth.address], 
    query: { 
      enabled: !!address, 
    }, 
  }) 
 
  return (
    <Card.Root display="flex" flexDirection="column" m="auto">
      <p>WETH EXAMPLE</p>
      <TokenInput singleToken tokenInput={tokenInput} />
    </Card.Root>
  )
})
 
export const Weth = () => (
  <SuspendedWeth suspenseFallback={<div style={{ margin: 'auto' }}>Loading...</div>} />
)

Deposit, approve and withdraw

After the user enters a valid value, we need a button to trigger one of the possible actions. We will make use of the TransactionButton component to handle the transaction lifecycle. We will only pass two properties transaction, which receives a function returning a promise with a transaction hash, and onMined where we will refetch the user balance once the transaction has been mined.

index.tsx
import { ChangeEvent, useState } from 'react'
import { Card } from '@chakra-ui/react'
import { Address } from 'viem'
import { sepolia } from 'viem/chains'
import TokenInput from '@/src/components/sharedComponents/TokenInput'
import { useTokenInput } from '@/src/components/sharedComponents/TokenInput/useTokenInput'
import TransactionButton from '@/src/components/sharedComponents/TransactionButton'
import { getContract } from '@/src/constants/contracts/contracts'
import { env } from '@/src/env'
import {
  useReadWethAllowance,
  useWriteWethApprove,
  useWriteWethDeposit,
  useWriteWethWithdraw,
} from '@/src/hooks/generated'
import { useTokenLists } from '@/src/hooks/useTokenLists'
import { useWeb3Status } from '@/src/hooks/useWeb3Status'
import { withSuspenseAndRetry } from '@/src/utils/suspenseWrapper'
 
type Operation = 'Deposit' | 'Withdraw'
 
export const SuspendedWeth = withSuspenseAndRetry(() => {
  const [operation, setOperation] = useState<Operation>('Deposit') 
 
  // get the list of tokens.
  const { tokensByChainId } = useTokenLists()
 
  // helper that give us information given a contract name.
  const weth = getContract('WETH', sepolia.id)
 
  // get the tokens from the token list we need to use.
  const wethToken = tokensByChainId[sepolia.id].find((token) => token.address === weth.address)
  const ethToken = tokensByChainId[sepolia.id].find(
    (token) => token.address === env.PUBLIC_NATIVE_TOKEN_ADDRESS,
  )
  if (!wethToken) throw new Error('WETH token not found')
  if (!ethToken) throw new Error('ETH token not found')
 
  // hook that gives us the state that <TokenInput /> needs.
  const tokenInput = useTokenInput(ethToken)
 
  // status of the Web3 connection.
  const { address } = useWeb3Status()
 
  // contracts calls
  const { writeContractAsync: wethDeposit } = useWriteWethDeposit()
  const { writeContractAsync: wethApprove } = useWriteWethApprove()
  const { writeContractAsync: wethWithdraw } = useWriteWethWithdraw()
 
  // contracts reads
  const wethAllowance = useReadWethAllowance({
    args: [address as Address, weth.address],
    query: {
      enabled: !!address,
    },
  })
 
  const sendOperation = async () => { 
    if (operation == 'Deposit') { 
      const res = await wethDeposit({ value: tokenInput.amount }) 
      return res 
    } else if (tokenInput.amount > (wethAllowance.data || 0n)) { 
      const res = await wethApprove({ args: [weth.address, tokenInput.amount] }) 
      return res 
    } else { 
      const res = await wethWithdraw({ args: [tokenInput.amount] }) 
      return res 
    } 
  } 
 
  // giving context to the toast 
  sendOperation.methodId = `WETH:${operation}`
  if (operation === 'Withdraw' && tokenInput.amount > (wethAllowance.data || 0n)) { 
    sendOperation.methodId = `WETH:Approve`
  } 
 
  const getActionText = () => { 
    if (operation === 'Withdraw' && tokenInput.amount > (wethAllowance.data || 0n)) { 
      return 'Approve'
    } 
    return operation 
  }
 
  const handleOperation = (e: ChangeEvent<HTMLSelectElement>) => { 
    const newValue = e.target.value as Operation
    tokenInput.setTokenSelected(newValue === 'Deposit' ? ethToken : wethToken) 
    setOperation(e.target.value as Operation) 
  } 
 
  return (
    <Card.Root display="flex" flexDirection="column" m="auto">
      <p>WETH EXAMPLE</p>
      <br /> // [!code focus]
      <div style={{ display: 'flex', width: '100%' }}> // [!code focus]
        <div> // [!code focus]
          Operation{' '}
          <select onChange={handleOperation} value={operation}> // [!code focus]
            <option value="Deposit">Deposit</option> // [!code focus]
            <option value="Withdraw">Withdraw</option> // [!code focus]
          </select> // [!code focus]
        </div> // [!code focus]
      </div> // [!code focus]
 
      <TokenInput singleToken tokenInput={tokenInput} />
 
      <br /> // [!code focus]
      <TransactionButton
        disabled={tokenInput.amountError !== undefined || tokenInput.amount == 0n}
        onMined={() => wethAllowance.refetch()}
        transaction={sendOperation}
      > // [!code focus]
        {getActionText()}
      </TransactionButton> // [!code focus]
    </Card.Root> 
  )
})
 
export const Weth = () => (
  <SuspendedWeth suspenseFallback={<div style={{ margin: 'auto' }}>Loading...</div>} />
)
Released under the MIT License.
© 2025 - BootNode