# dAppBooster
> A modern starter kit built with React to quickly get started with your next web3 project.
## Building your first dApp \[Wrapping and Unwrapping WETH]
### Introduction
:::warning
**Initial setup**
Before continuing, be sure to have followed the steps in [getting started](/introduction/installation)
:::
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](https://github.com/BootNodeDev/dAppBooster/blob/develop/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:
:::steps
#### ABI download
Download the [ABI](https://sepolia.etherscan.io/address/0xa45f5716cab1a49b107a5de96ec26da26d1eba9e#code) and save it in `src/constants/contracts/abis/WETH.ts` using the following format:
```tsx [WETH.ts] showLineNumbers
export const WETH_ABI = [
// ...abi code
] as const
```
#### Contracts update
Update `src/constants/contracts/contracts.ts` with the WETH contract address and ABI:
```tsx [contracts.ts] showLineNumbers
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](https://github.com/BootNodeDev/dAppBooster/blob/develop/src/lib/wagmi/config.ts) wagmi to generate hooks with both suspense and promised hooks.
```bash [Terminal]
pnpm wagmi-generate
```
### Create the page
:::steps
#### Add a new route
Create a new file `/src/routes/weth.lazy.tsx` with the following content:
```tsx [weth.lazy.tsx] showLineNumbers
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:
```tsx [index.tsx] showLineNumbers
import { Card } from '@chakra-ui/react'
export const Weth = () => {
return (Hello User!
}
```
:::
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](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](https://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:
:::steps
#### Create the tokens file
Create `public/tokens.json` with the following content:
```json [tokens.json] showLineNumbers
{
"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:
```tsx [tokenLists.ts] showLineNumbers
{
// ...
CUSTOM: '/tokens.json',
}
```
:::
Now, all the token related utilities will make use of this new list.
:::info
**Get the tokens**
Use the [useTokenLists](https://github.com/BootNodeDev/dAppBooster/blob/develop/src/hooks/useTokenLists.ts) hook it to obtain all the tokens configured.
:::
### The TokenInput component
We will use the [TokenInput](https://github.com/BootNodeDev/dAppBooster/blob/develop/src/components/sharedComponents/TokenInput/index.tsx) 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:
```tsx [index.tsx] showLineNumbers
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 needs.
const tokenInput = useTokenInput(ethToken)
return (
WETH EXAMPLE
)
}
```
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](https://react.dev/reference/react/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:
```tsx [index.tsx] showLineNumbers
import { withSuspenseAndRetry } from '@/src/utils/suspenseWrapper'
export const SuspendedWeth = withSuspenseAndRetry(() => {
// ...
})
export const Weth = () => (
Loading...} />
)
```
### 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()](https://github.com/BootNodeDev/dAppBooster/blob/develop/src/hooks/useWeb3Status.tsx) provided by dappBooster. This hook will return important information of the context of the connected wallet and selected chain.
```tsx [index.tsx] showLineNumbers
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 { // [!code focus]
useReadWethAllowance, // [!code focus]
useWriteWethApprove, // [!code focus]
useWriteWethDeposit, // [!code focus]
useWriteWethWithdraw, // [!code focus]
} from '@/src/hooks/generated' // [!code focus]
import { useTokenLists } from '@/src/hooks/useTokenLists'
import { useWeb3Status } from '@/src/hooks/useWeb3Status' // [!code focus]
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 needs.
const tokenInput = useTokenInput(ethToken)
// status of the Web3 connection. // [!code focus]
const { address } = useWeb3Status() // [!code focus]
// contracts calls // [!code focus]
const { writeContractAsync: wethDeposit } = useWriteWethDeposit() // [!code focus]
const { writeContractAsync: wethApprove } = useWriteWethApprove() // [!code focus]
const { writeContractAsync: wethWithdraw } = useWriteWethWithdraw() // [!code focus]
// contracts reads // [!code focus]
const wethAllowance = useReadWethAllowance({ // [!code focus]
args: [address as Address, weth.address], // [!code focus]
query: { // [!code focus]
enabled: !!address, // [!code focus]
}, // [!code focus]
}) // [!code focus]
return (
WETH EXAMPLE
)
})
export const Weth = () => (
Loading...} />
)
```
### 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](https://github.com/BootNodeDev/dAppBooster/blob/develop/src/components/sharedComponents/TransactionButton.tsx) 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.
```tsx [index.tsx] showLineNumbers
import { ChangeEvent, useState } from 'react' // [!code focus]
import { Card } from '@chakra-ui/react'
import { Address } from 'viem' // [!code focus]
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' // [!code focus]
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' // [!code focus]
export const SuspendedWeth = withSuspenseAndRetry(() => {
const [operation, setOperation] = useState('Deposit') // [!code focus]
// 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 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 () => { // [!code focus]
if (operation == 'Deposit') { // [!code focus]
const res = await wethDeposit({ value: tokenInput.amount }) // [!code focus]
return res // [!code focus]
} else if (tokenInput.amount > (wethAllowance.data || 0n)) { // [!code focus]
const res = await wethApprove({ args: [weth.address, tokenInput.amount] }) // [!code focus]
return res // [!code focus]
} else { // [!code focus]
const res = await wethWithdraw({ args: [tokenInput.amount] }) // [!code focus]
return res // [!code focus]
} // [!code focus]
} // [!code focus]
// giving context to the toast // [!code focus]
sendOperation.methodId = `WETH:${operation}` // [!code focus]
if (operation === 'Withdraw' && tokenInput.amount > (wethAllowance.data || 0n)) { // [!code focus]
sendOperation.methodId = `WETH:Approve` // [!code focus]
} // [!code focus]
const getActionText = () => { // [!code focus]
if (operation === 'Withdraw' && tokenInput.amount > (wethAllowance.data || 0n)) { // [!code focus]
return 'Approve' // [!code focus]
} // [!code focus]
return operation // [!code focus]
}
const handleOperation = (e: ChangeEvent) => { // [!code focus]
const newValue = e.target.value as Operation // [!code focus]
tokenInput.setTokenSelected(newValue === 'Deposit' ? ethToken : wethToken) // [!code focus]
setOperation(e.target.value as Operation) // [!code focus]
} // [!code focus]
return (