Skip to content

add native ETH token in gift donut dapp #855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 33 additions & 15 deletions advanced/dapps/chain-abstraction-demo/app/hooks/useGiftDonut.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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,
Expand All @@ -77,28 +85,38 @@ 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", {
elapsedTime: Math.floor((Date.now() - startTime) / 1000),
});
}, 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 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ function GiftDonutForm({
type="button"
variant="secondary"
className="flex flex-1 gap-1"
disabled={!giftDonutModalManager.isTokenNetworkCompatible()}
disabled={!giftDonutModalManager.isTokenNetworkCompatible() || count === 0}
>
<p
className="flex items-center"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function TokenList({
<div key={index} className="w-full">
<TokenItem
token={tokenItem}
selected={token?.address === tokenItem.address}
selected={token?.id === tokenItem.id}
onClick={() => setSelectedToken(tokenItem)}
/>
{availableTokens.length - 1 !== index && <Separator />}
Expand Down Expand Up @@ -122,7 +122,7 @@ function TokenItem({
<div className="flex flex-col items-start gap-0.5">
<Label className="font-medium">{token.name}</Label>
<span className="text-xs text-muted-foreground">
Balance: ${formattedBalance}
Balance: ${formattedBalance} { token.type === "native" && token.name === "ETH" &&'(0.0005 ETH = $1)'}
</span>
</div>
{selected && <CheckIcon className="h-5 w-5 text-green-500" />}
Expand Down
7 changes: 7 additions & 0 deletions advanced/dapps/chain-abstraction-demo/consts/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,17 @@ export const usdsTokenAddresses: Record<number, Hex> = {
8453: "0x820c137fa70c8691f0e44dc420a5e53c168921dc", // Base
};

export const ethTokenAddresses: Record<number, Hex> = {
42161: "0x0000000000000000000000000000000000000000", // Arbitrum
10: "0x0000000000000000000000000000000000000000", // Optimism
8453: "0x0000000000000000000000000000000000000000", // Base
};

export const tokenAddresses: Record<string, Record<string, Hex>> = {
USDC: usdcTokenAddresses,
USDT: usdtTokenAddresses,
USDS: usdsTokenAddresses,
ETH: ethTokenAddresses,
};

export const getSupportedNetworks = (token: string): number[] => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Expand Down
26 changes: 19 additions & 7 deletions advanced/dapps/chain-abstraction-demo/data/EIP155Data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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(
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
106 changes: 48 additions & 58 deletions advanced/dapps/chain-abstraction-demo/utils/BalanceFetcherUtil.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -25,7 +26,11 @@ async function fetchTokenBalance({
}: {
publicClient: PublicClient;
userAddress: Hex;
tokenConfig: TokenConfig;
tokenConfig: {
symbol: string;
decimals: number;
address: Hex;
};
chainId: number;
}): Promise<TokenBalance | null> {
try {
Expand All @@ -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,
Expand All @@ -63,7 +69,6 @@ export async function fetchFallbackBalances(
const chain = getChain(currentChainId);
if (!chain) {
console.error(`Chain not found for ID: ${currentChainId}`);

return [];
}

Expand All @@ -74,70 +79,56 @@ export async function fetchFallbackBalances(
}) as PublicClient;

const balances: TokenBalance[] = [];
const tokenBalancePromises: Promise<TokenBalance | null>[] = [];

// 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);
}
Expand All @@ -146,7 +137,6 @@ export async function fetchFallbackBalances(
return balances;
} catch (error) {
console.error("Error in fetchFallbackBalances:", error);

return [];
}
}