diff --git a/advanced/dapps/chain-abstraction-demo/app/hooks/useGiftDonut.tsx b/advanced/dapps/chain-abstraction-demo/app/hooks/useGiftDonut.tsx index 85da06be0..22b696774 100644 --- a/advanced/dapps/chain-abstraction-demo/app/hooks/useGiftDonut.tsx +++ b/advanced/dapps/chain-abstraction-demo/app/hooks/useGiftDonut.tsx @@ -2,13 +2,15 @@ import { config } from "@/config"; import { tokenAddresses } from "@/consts/tokens"; import { Network, Token } from "@/data/EIP155Data"; import { toast } from "sonner"; -import { erc20Abi, Hex, PublicClient } from "viem"; +import { erc20Abi, Hex, parseEther, PublicClient } from "viem"; import { getAccount, getWalletClient, getPublicClient } from "wagmi/actions"; import { useState } from "react"; import { TransactionToast } from "@/components/TransactionToast"; type TransactionStatus = "waiting-approval" | "pending" | "success" | "error"; +const ETH_USD_RATE = 0.0005; // 1 USD = 0.0005 ETH + export default function useGiftDonut() { const [isPending, setIsPending] = useState(false); @@ -36,7 +38,7 @@ export default function useGiftDonut() { ); }; - const validateTransaction = async (network: Network) => { + const getClients = async (network: Network) => { const client = await getWalletClient(config, { chainId: network.chainId }); const publicClient = getPublicClient(config); if (!publicClient) throw new Error("Failed to get public client"); @@ -64,6 +66,12 @@ export default function useGiftDonut() { return contract; }; + const convertDonutCountToEth = (donutCount: number) => { + // consider 1 donut = 1 USD, 1 USD = 0.0005 ETH + return donutCount * ETH_USD_RATE; + } + + const giftDonutAsync = async ( to: Hex, donutCount: number, @@ -77,14 +85,9 @@ export default function useGiftDonut() { try { // Validate chain and get clients - const { client, publicClient } = await validateTransaction(network); + const { client, publicClient } = await getClients(network); const chainId = getAccount(config).chain?.id!; - // Get token contract - const contract = getTokenContract(token, chainId); - - // Calculate token amount using token's decimals - const tokenAmount = donutCount * 10 ** token.decimals; // Start tracking elapsed time updateInterval = setInterval(() => { updateToast(toastId, "waiting-approval", { @@ -92,13 +95,28 @@ export default function useGiftDonut() { }); }, 1000); - // Send transaction - const tx = await client.writeContract({ - abi: erc20Abi, - address: contract, - functionName: "transfer", - args: [to, BigInt(tokenAmount)], - }); + + let tx: Hex; + if (token.name === "ETH") { + // Calculate ETH amount using the conversion rate + const ethAmount = convertDonutCountToEth(donutCount); + tx = await client.sendTransaction({ + to, + value: parseEther(ethAmount.toString()), + chainId, + }); + } else { + // Get token contract + const contract = getTokenContract(token, chainId); + const tokenAmount = donutCount * 10 ** token.decimals; + // Send transaction + tx = await client.writeContract({ + abi: erc20Abi, + address: contract, + functionName: "transfer", + args: [to, BigInt(tokenAmount)], + }); + } // Update to pending status updateToast(toastId, "pending", { hash: tx, networkName: network.name }); diff --git a/advanced/dapps/chain-abstraction-demo/components/gift-donut-modal-views/CheckoutView.tsx b/advanced/dapps/chain-abstraction-demo/components/gift-donut-modal-views/CheckoutView.tsx index 6c715bf3f..4cd95eca7 100644 --- a/advanced/dapps/chain-abstraction-demo/components/gift-donut-modal-views/CheckoutView.tsx +++ b/advanced/dapps/chain-abstraction-demo/components/gift-donut-modal-views/CheckoutView.tsx @@ -238,7 +238,7 @@ function GiftDonutForm({ type="button" variant="secondary" className="flex flex-1 gap-1" - disabled={!giftDonutModalManager.isTokenNetworkCompatible()} + disabled={!giftDonutModalManager.isTokenNetworkCompatible() || count === 0} >

setSelectedToken(tokenItem)} /> {availableTokens.length - 1 !== index && } @@ -122,7 +122,7 @@ function TokenItem({

- Balance: ${formattedBalance} + Balance: ${formattedBalance} { token.type === "native" && token.name === "ETH" &&'(0.0005 ETH = $1)'}
{selected && } diff --git a/advanced/dapps/chain-abstraction-demo/consts/tokens.ts b/advanced/dapps/chain-abstraction-demo/consts/tokens.ts index 4f827d18c..898a4cef2 100644 --- a/advanced/dapps/chain-abstraction-demo/consts/tokens.ts +++ b/advanced/dapps/chain-abstraction-demo/consts/tokens.ts @@ -17,10 +17,17 @@ export const usdsTokenAddresses: Record = { 8453: "0x820c137fa70c8691f0e44dc420a5e53c168921dc", // Base }; +export const ethTokenAddresses: Record = { + 42161: "0x0000000000000000000000000000000000000000", // Arbitrum + 10: "0x0000000000000000000000000000000000000000", // Optimism + 8453: "0x0000000000000000000000000000000000000000", // Base +}; + export const tokenAddresses: Record> = { USDC: usdcTokenAddresses, USDT: usdtTokenAddresses, USDS: usdsTokenAddresses, + ETH: ethTokenAddresses, }; export const getSupportedNetworks = (token: string): number[] => { diff --git a/advanced/dapps/chain-abstraction-demo/controllers/GiftDonutModalManager.ts b/advanced/dapps/chain-abstraction-demo/controllers/GiftDonutModalManager.ts index 77bb13752..ff70032a4 100644 --- a/advanced/dapps/chain-abstraction-demo/controllers/GiftDonutModalManager.ts +++ b/advanced/dapps/chain-abstraction-demo/controllers/GiftDonutModalManager.ts @@ -168,6 +168,14 @@ class GiftDonutModalManager { } getBalanceBySymbol(symbol: string): string { + if(symbol === "ETH") { + const ethBalance = this.state.state.balances.find((b) => b.symbol === "ETH")?.balance || "0.00"; + // Convert ETH balance to USD equivalent (0.0005 ETH = 1 USD) + const usdEquivalent = parseFloat(ethBalance) / 0.0005; + + return usdEquivalent.toFixed(2); + } + const balance = this.state.state.balances.find((b) => b.symbol === symbol); return balance?.balance || "0.00"; } diff --git a/advanced/dapps/chain-abstraction-demo/data/EIP155Data.ts b/advanced/dapps/chain-abstraction-demo/data/EIP155Data.ts index 2d04941c8..ac2382359 100644 --- a/advanced/dapps/chain-abstraction-demo/data/EIP155Data.ts +++ b/advanced/dapps/chain-abstraction-demo/data/EIP155Data.ts @@ -13,9 +13,10 @@ export interface Network { } export interface Token { + id: string; // Unique identifier for the token + type: "erc20" | "native" name: string; icon: string; - address: string; supportedChainIds: number[]; decimals: number; } @@ -43,26 +44,37 @@ export const supportedNetworks: Network[] = [ export const supportedTokens: Token[] = [ { + id: "USDC", + type: "erc20", name: "USDC", icon: "/token-images/USDC.png", - address: "0x1", supportedChainIds: [arbitrum.id, base.id, optimism.id], - decimals: 6 + decimals: 6, }, { + id: "USDT", + type: "erc20", name: "USDT", icon: "/token-images/USDT.png", - address: "0x2", supportedChainIds: [arbitrum.id, optimism.id], - decimals: 6 + decimals: 6, }, { + id: "USDS", + type: "erc20", name: "USDS", icon: "/token-images/USDS(DAI).png", - address: "0x3", supportedChainIds: [base.id], - decimals: 18 + decimals: 18, }, + { + id: "ETH", + type: "native", + name: "ETH", + icon: "/token-images/ETH.png", + supportedChainIds: [arbitrum.id, base.id, optimism.id], + decimals: 18, + } ]; export function isTokenSupportedOnNetwork( diff --git a/advanced/dapps/chain-abstraction-demo/public/token-images/ETH.png b/advanced/dapps/chain-abstraction-demo/public/token-images/ETH.png new file mode 100644 index 000000000..db6015497 Binary files /dev/null and b/advanced/dapps/chain-abstraction-demo/public/token-images/ETH.png differ diff --git a/advanced/dapps/chain-abstraction-demo/utils/BalanceFetcherUtil.ts b/advanced/dapps/chain-abstraction-demo/utils/BalanceFetcherUtil.ts index f69107454..7ef2513fe 100644 --- a/advanced/dapps/chain-abstraction-demo/utils/BalanceFetcherUtil.ts +++ b/advanced/dapps/chain-abstraction-demo/utils/BalanceFetcherUtil.ts @@ -1,4 +1,5 @@ -import { usdcTokenAddresses, usdtTokenAddresses, usdsTokenAddresses } from "@/consts/tokens"; +import { supportedTokens } from "@/data/EIP155Data"; +import { tokenAddresses } from "@/consts/tokens"; import { createPublicClient, erc20Abi, Hex, http, PublicClient } from "viem"; import { formatBalance } from "@/utils/FormatterUtil"; import { getChain } from "@/utils/NetworksUtil"; @@ -25,7 +26,11 @@ async function fetchTokenBalance({ }: { publicClient: PublicClient; userAddress: Hex; - tokenConfig: TokenConfig; + tokenConfig: { + symbol: string; + decimals: number; + address: Hex; + }; chainId: number; }): Promise { try { @@ -44,15 +49,16 @@ async function fetchTokenBalance({ }; } catch (error) { console.error(`Error fetching ${tokenConfig.symbol} balance:`, error); - return null; } } + function getTransport({ chainId }: { chainId: number }) { return http( `https://rpc.walletconnect.org/v1/?chainId=eip155:${chainId}&projectId=${process.env["NEXT_PUBLIC_PROJECT_ID"]}`, ); } + export async function fetchFallbackBalances( userAddress: Hex, currentChainIdAsHex: Hex, @@ -63,7 +69,6 @@ export async function fetchFallbackBalances( const chain = getChain(currentChainId); if (!chain) { console.error(`Chain not found for ID: ${currentChainId}`); - return []; } @@ -74,70 +79,56 @@ export async function fetchFallbackBalances( }) as PublicClient; const balances: TokenBalance[] = []; + const tokenBalancePromises: Promise[] = []; - // Fetch native token balance - try { - const nativeBalance = await publicClient.getBalance({ - address: userAddress, - }); - - balances.push({ - symbol: chain.nativeCurrency.symbol, - balance: formatBalance(nativeBalance, chain.nativeCurrency.decimals), - address: "0x" as Hex, - chainId: currentChainId, - }); - } catch (error) { - console.error(`Error fetching native balance:`, error); - } + // Filter tokens supported on the current chain + const tokensForChain = supportedTokens.filter(token => + token.supportedChainIds.includes(currentChainId) + ); - // Get supported tokens for current chain - const supportedTokens: TokenConfig[] = []; - - // Add USDC if supported - const usdcAddress = usdcTokenAddresses[currentChainId]; - if (usdcAddress) { - supportedTokens.push({ - symbol: "USDC", - decimals: 6, - address: usdcAddress, - }); + const nativeTokens = tokensForChain.filter(token => token.type === "native"); + for (const nativeToken of nativeTokens) { + try { + const nativeBalance = await publicClient.getBalance({ + address: userAddress, + }); + + balances.push({ + symbol: nativeToken.name, + balance: formatBalance(nativeBalance, nativeToken.decimals), + address: "0x" as Hex, + chainId: currentChainId, + }); + } catch (error) { + console.error(`Error fetching native ${nativeToken.name} balance:`, error); + } } - // Add USDT if supported - const usdtAddress = usdtTokenAddresses[currentChainId]; - if (usdtAddress) { - supportedTokens.push({ - symbol: "USDT", - decimals: 6, - address: usdtAddress, - }); - } + const erc20Tokens = tokensForChain.filter(token => token.type === "erc20"); + for (const erc20Token of erc20Tokens) { + const tokenAddressMap = tokenAddresses[erc20Token.id]; + if (!tokenAddressMap) continue; - // Add USDS if supported - const usdsAddress = usdsTokenAddresses[currentChainId]; - if (usdsAddress) { - supportedTokens.push({ - symbol: "USDS", - decimals: 18, - address: usdsAddress, - }); - } + const tokenAddress = tokenAddressMap[currentChainId]; + if (!tokenAddress) continue; - // Fetch token balances - const tokenResults = await Promise.all( - supportedTokens.map((token) => + tokenBalancePromises.push( fetchTokenBalance({ publicClient, userAddress, - tokenConfig: token, + tokenConfig: { + symbol: erc20Token.name, + decimals: erc20Token.decimals, + address: tokenAddress, + }, chainId: currentChainId, - }), - ), - ); + }) + ); + } - // Add successful token balances - tokenResults.forEach((result) => { + const tokenResults = await Promise.all(tokenBalancePromises); + + tokenResults.forEach(result => { if (result) { balances.push(result); } @@ -146,7 +137,6 @@ export async function fetchFallbackBalances( return balances; } catch (error) { console.error("Error in fetchFallbackBalances:", error); - return []; } }