DApp example
TIP
Task
Implement a DApp that can send a transaction with a specified nonce to a designated address.
Requirements
- Customizable input for the target address
- Customizable input for nonce (defaults to displaying the next nonce)
- Use EIP-1559 for miner fees
- Implement using React Hooks, do not use Class Components
- Implement using TypeScript
- Available for online preview
Bonus Points
- Support for switching network nodes (EIP-3326, EIP-3085)
References
- ethers.js: Complete Ethereum library and wallet implementation in JavaScript.
- What's an Ethereum transaction?
- EIP-1193: Ethereum Provider JavaScript API
- EIP-1559: Fee market change for ETH 1.0 chain
- EIP-3326: Wallet Switch Ethereum Chain RPC Method
- EIP-3085: Wallet Add Ethereum Chain RPC Method
ETH
https://online-judge-with-wuchendi.vercel.app/
ts
const connectWallet = async () => {
try {
if (!window.ethereum) {
// eslint-disable-next-line no-alert
window.alert('Ethereum not found. Please install MetaMask or a similar extension.')
return
}
setIsLoading(true)
await window.ethereum.enable()
const provider = new ethers.providers.Web3Provider(window.ethereum)
const signer = provider.getSigner()
const [address, network, gasPrice, nextNonce] = await Promise.all([
signer.getAddress(),
provider.getNetwork(),
provider.getGasPrice(),
provider.getTransactionCount(signer.getAddress()),
])
setWallet(signer)
setUserAddress(address)
nextNonce && setNonce(nextNonce)
setGasPrice(gasPrice.toNumber())
setNetworkName(network.name)
await updateAccountInfo(signer, address)
// eslint-disable-next-line no-console
console.log(
`Current network: ${
network.name
}, nextNonce: ${nextNonce.toString()}, gasPrice: ${gasPrice.toString()}`
)
} catch (error: any) {
if (error.code === 4001) {
// eslint-disable-next-line no-alert
window.alert(error.message || 'User rejected the request.')
} else {
console.error(error)
}
} finally {
setIsLoading(false)
}
}
ts
const sendTransaction = async () => {
try {
if (!wallet) {
throw new Error('Please connect your wallet')
}
txHash && setTxHash('')
setIsLoading(true)
// const network = await wallet.provider?.getNetwork()
const currentNonce = nonce !== undefined ? nonce : await wallet.getTransactionCount()
if (!ethers.utils.isAddress(targetAddress)) {
// eslint-disable-next-line no-alert
window.alert('Invalid target address')
return
}
if (tokenAmount.trim() === '') {
// eslint-disable-next-line no-alert
window.alert('Token amount is required')
return
}
const parsedTokenAmount = ethers.utils.parseEther(tokenAmount)
if (parsedTokenAmount.isZero()) {
// eslint-disable-next-line no-alert
window.alert('Invalid target amount')
return
}
const feeData = await wallet.getFeeData()
const tx: ethers.providers.TransactionRequest = {
type: 2, // type 2 which is EIP-1559 transaction
to: targetAddress,
nonce: currentNonce || 0,
value: parsedTokenAmount,
// gasPrice,
maxFeePerGas: feeData.maxFeePerGas || ethers.utils.parseUnits('2'),
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas || ethers.utils.parseUnits('1'),
gasLimit: '21000',
// chainId: network?.chainId,
}
// const estimatedGasLimit = await wallet.estimateGas(tx)
// tx.gasLimit = estimatedGasLimit
// eslint-disable-next-line no-console
console.log('🚀 ~ file: page.tsx ~ sendTransaction ~ tx:', tx)
try {
const result = await wallet.sendTransaction(tx)
// TODO!: That's not a good idea
await wait(15 * 1000)
await updateAccountInfo(wallet, userAddress)
setTxHash(result.hash)
} catch (error: any) {
// eslint-disable-next-line no-alert
error?.message && window.alert(error.message)
// eslint-disable-next-line no-console
console.log('🚀 ~ file: page.tsx ~ sendTransaction ~ error:', error)
}
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
}
}
ts
const updateAccountInfo = async (signer: ethers.Signer, address: string) => {
const provider = signer.provider!
const [_balance] = await Promise.all([provider.getBalance(address)])
const formattedBalance = ethers.utils.formatEther(_balance)
setBalance(formattedBalance)
const nextNonce = await provider.getTransactionCount(address)
setNonce(nextNonce)
}
LINK
https://online-judge-with-wuchendi-git-feature-link-wuchendi.vercel.app/
ts
const connectWallet = async () => {
try {
if (!window.ethereum) {
// eslint-disable-next-line no-alert
window.alert('Ethereum not found. Please install MetaMask or a similar extension.')
return
}
setIsLoading(true)
await window.ethereum.enable()
const provider = new ethers.providers.Web3Provider(window.ethereum)
const signer = provider.getSigner()
const [address, network, gasPrice, nextNonce] = await Promise.all([
signer.getAddress(),
provider.getNetwork(),
provider.getGasPrice(),
provider.getTransactionCount(signer.getAddress()),
])
setWallet(signer)
setUserAddress(address)
nextNonce && setNonce(nextNonce)
setGasPrice(gasPrice.toNumber())
setNetworkName(network.name)
await updateAccountInfo(signer, address)
// eslint-disable-next-line no-console
console.log(
`Current network: ${
network.name
}, nextNonce: ${nextNonce.toString()}, gasPrice: ${gasPrice.toString()}`
)
} catch (error: any) {
if (error.code === 4001) {
// eslint-disable-next-line no-alert
window.alert(error.message || 'User rejected the request.')
} else {
console.error(error)
}
} finally {
setIsLoading(false)
}
}
ts
const sendTransaction = async () => {
try {
if (!wallet) {
throw new Error('Please connect your wallet')
}
txHash && setTxHash('')
setIsLoading(true)
// const network = await wallet.provider?.getNetwork()
const currentNonce = nonce !== undefined ? nonce : await wallet.getTransactionCount()
if (!ethers.utils.isAddress(targetAddress)) {
// eslint-disable-next-line no-alert
window.alert('Invalid target address')
return
}
if (tokenAmount.trim() === '') {
// eslint-disable-next-line no-alert
window.alert('Token amount is required')
return
}
const parsedTokenAmount = ethers.utils.parseEther(tokenAmount)
if (parsedTokenAmount.isZero()) {
// eslint-disable-next-line no-alert
window.alert('Invalid target amount')
return
}
const feeData = await wallet.getFeeData()
const linkContract = new ethers.Contract(LINKCONTRACTADDRESS, linkAbi, wallet)
try {
const result = await linkContract.transfer(targetAddress, parsedTokenAmount, {
type: 2, // type 2 which is EIP-1559 transaction
maxFeePerGas: feeData.maxFeePerGas || ethers.utils.parseUnits('2'),
maxPriorityFeePerGas:
feeData.maxPriorityFeePerGas || ethers.utils.parseUnits('1'),
nonce: currentNonce || undefined,
})
// TODO!: That's not a good idea
await wait(15 * 1000)
await updateAccountInfo(wallet, userAddress)
setTxHash(result.hash)
} catch (error: any) {
// eslint-disable-next-line no-alert
error?.message && window.alert(error.message)
// eslint-disable-next-line no-console
console.log('🚀 ~ file: page.tsx ~ sendTransaction ~ error:', error)
}
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
}
}
ts
const updateAccountInfo = async (signer: ethers.Signer, address: string) => {
try {
let localLinkAbi = linkAbi
if (localLinkAbi.length <= 0) {
const response = await fetch(
'https://cdn.jsdelivr.net/gh/cdLab996/picture-lib/cdLab996/abi/LINK.json'
// 'https://raw.githubusercontent.com/cdLab996/picture-lib/main/cdLab996/abi/LINK.json'
)
const result = await response.json()
localLinkAbi = result
setLinkAbi(result)
}
const provider = signer.provider!
const linkContract = new ethers.Contract(LINKCONTRACTADDRESS, localLinkAbi, provider)
const [linkBalance, symbol] = await Promise.all([
linkContract.balanceOf(address),
linkContract.symbol(),
])
const formattedLinkBalance = ethers.utils.formatUnits(linkBalance, 18)
setBalance(formattedLinkBalance + ' ' + symbol)
const nextNonce = await provider.getTransactionCount(address)
setNonce(nextNonce)
} catch (error) {
console.error('Error updating account info:', error)
}
}