Building Your First dApp
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).
Hands-on
Before continuing, be sure to have followed the steps in getting started
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
We have already deployed a smart contract to the Sepolia network. Now it'ss time to let the dApp know about it existence:
- Download the ABI and save it in
src/constants/contracts/abis/WETH.ts
using the following format:
export const WETH_ABI = [
// ...abi code
] as const
- Update
src/constants/contracts/contracts.ts
with the WETH contract address and ABI:
import { WETH_ABI } from '@/src/constants/contracts/abis/WETH'
export const contracts = {
// ...
{
abi: WETH_ABI,
name: 'WETH',
address: {
[sepolia.id]: '0xa45f5716cab1a49b107a5de96ec26da26d1eba9e',
},
},
}
Generate WETH 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.
pnpm wagmi-generate
Create the weth page
- Add a new file
/src/routes/weth.lazy.tsx
with the following content:
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 a new folder
src/components/pageComponents/weth
. -
Create a new file
src/components/pageComponents/weth/index.tsx
with this content:
import { Card } from 'db-ui-toolkit'
export const Weth = () => {
return <Card style={{ margin: 'auto' }}>Hello Annon!</Card>
}
This is just a simple component that we will use to add all the WETH logic.
Now, you can open this link http://localhost:5173/weth to see it working.
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 are 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 code:
- Create
public/tokens.json
with the following content:
{
"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 new token list to the following constant
src/constants/tokenLists.ts
like so:
{
// ...
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 will provide us with the following 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
file as follows:
import { Card } from 'db-ui-toolkit'
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 style={{ margin: 'auto' }}>
<p>WETH EXAMPLE</p>
<TokenInput singleToken tokenInput={tokenInput} />
</Card>
)
}
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 loading 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:
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.
import { Card } from 'db-ui-toolkit'
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 style={{ margin: 'auto' }}>
<p>WETH EXAMPLE</p>
<TokenInput singleToken tokenInput={tokenInput} />
</Card>
)
})
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.
import { ChangeEvent, useState } from 'react'
import { Card } from 'db-ui-toolkit'
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 style={{ margin: 'auto' }}>
<p>WETH EXAMPLE</p>
<br />
<div style={{ display: 'flex', width: '100%' }}>
<div>
Operation{' '}
<select onChange={handleOperation} value={operation}>
<option value="Deposit">Deposit</option>
<option value="Withdraw">Withdraw</option>
</select>
</div>
</div>
<TokenInput singleToken tokenInput={tokenInput} />
<br />
<TransactionButton
disabled={tokenInput.amountError !== undefined || tokenInput.amount == 0n}
onMined={() => wethAllowance.refetch()}
transaction={sendOperation}
>
{getActionText()}
</TransactionButton>
</Card>
)
})
export const Weth = () => (
<SuspendedWeth suspenseFallback={<div style={{ margin: 'auto' }}>Loading...</div>} />
)