diff --git a/app/api/evm-chain-faucet/route.ts b/app/api/evm-chain-faucet/route.ts index a43b9e91f08..c9efe81a0f6 100644 --- a/app/api/evm-chain-faucet/route.ts +++ b/app/api/evm-chain-faucet/route.ts @@ -1,10 +1,11 @@ import { NextRequest, NextResponse } from 'next/server'; -import { createWalletClient, http, parseEther, createPublicClient, defineChain, isAddress } from 'viem'; +import { createWalletClient, http, parseEther, createPublicClient, defineChain, isAddress, parseUnits } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { avalancheFuji } from 'viem/chains'; import { getAuthSession } from '@/lib/auth/authSession'; import { rateLimit } from '@/lib/rateLimit'; import { getL1ListStore, type L1ListItem } from '@/components/toolbox/stores/l1ListStore'; +import ERC20ABI from '@/contracts/icm-contracts/compiled/ExampleERC20.json'; const SERVER_PRIVATE_KEY = process.env.FAUCET_C_CHAIN_PRIVATE_KEY; const FAUCET_ADDRESS = process.env.FAUCET_C_CHAIN_ADDRESS; @@ -14,10 +15,19 @@ if (!SERVER_PRIVATE_KEY || !FAUCET_ADDRESS) { } // Helper function to find a testnet chain that supports BuilderHub faucet -function findSupportedChain(chainId: number): L1ListItem | undefined { +function findSupportedChain(chainId: number, faucetId?: string): L1ListItem | undefined { const testnetStore = getL1ListStore(true); + const allChains = testnetStore.getState().l1List; - return testnetStore.getState().l1List.find( + // If faucetId is provided, use it for exact match + if (faucetId) { + return allChains.find( + (chain: L1ListItem) => chain.id === faucetId && chain.hasBuilderHubFaucet + ); + } + + // Otherwise, find by chainId (original behavior for backward compatibility) + return allChains.find( (chain: L1ListItem) => chain.evmChainId === chainId && chain.hasBuilderHubFaucet ); } @@ -62,13 +72,14 @@ async function transferEVMTokens( sourceAddress: string, destinationAddress: string, chainId: number, - amount: string + amount: string, + faucetId?: string ): Promise<{ txHash: string }> { if (!account) { throw new Error('Wallet not initialized'); } - const l1Data = findSupportedChain(chainId); + const l1Data = findSupportedChain(chainId, faucetId); if (!l1Data) { throw new Error(`ChainID ${chainId} is not supported by Builder Hub Faucet`); } @@ -77,22 +88,63 @@ async function transferEVMTokens( const walletClient = createWalletClient({ account, chain: viemChain, transport: http() }); const publicClient = createPublicClient({ chain: viemChain, transport: http() }); - const balance = await publicClient.getBalance({ - address: sourceAddress as `0x${string}` - }); - - const amountToSend = parseEther(amount); + // Check if this is an ERC20 faucet or native token faucet + const isERC20Faucet = !!l1Data.erc20TokenAddress; + + if (isERC20Faucet) { + // ERC20 token transfer + const tokenAddress = l1Data.erc20TokenAddress as `0x${string}`; + + // Get token decimals + const decimals = await publicClient.readContract({ + address: tokenAddress, + abi: ERC20ABI.abi, + functionName: 'decimals', + }) as number; + + // Parse amount with correct decimals + const amountToSend = parseUnits(amount, decimals); + + // Check ERC20 balance + const balance = await publicClient.readContract({ + address: tokenAddress, + abi: ERC20ABI.abi, + functionName: 'balanceOf', + args: [sourceAddress], + }) as bigint; + + if (balance < amountToSend) { + throw new Error(`Insufficient ${l1Data.coinName} balance in faucet on ${l1Data.name}`); + } - if (balance < amountToSend) { - throw new Error(`Insufficient faucet balance on ${l1Data.name}`); - } + // Transfer ERC20 tokens + const txHash = await walletClient.writeContract({ + address: tokenAddress, + abi: ERC20ABI.abi, + functionName: 'transfer', + args: [destinationAddress, amountToSend], + }); + + return { txHash }; + } else { + // Native token transfer (existing logic) + const balance = await publicClient.getBalance({ + address: sourceAddress as `0x${string}` + }); + + const amountToSend = parseEther(amount); + + if (balance < amountToSend) { + throw new Error(`Insufficient faucet balance on ${l1Data.name}`); + } - const txHash = await walletClient.sendTransaction({ - to: destinationAddress as `0x${string}`, - value: amountToSend, - }); + const txHash = await walletClient.sendTransaction({ + to: destinationAddress as `0x${string}`, + value: amountToSend, + }); - return { txHash }; + return { txHash }; + } } async function validateFaucetRequest(request: NextRequest): Promise { @@ -115,6 +167,7 @@ async function validateFaucetRequest(request: NextRequest): Promise const searchParams = request.nextUrl.searchParams; const destinationAddress = searchParams.get('address')!; const chainId = parseInt(searchParams.get('chainId')!); - const supportedChain = findSupportedChain(chainId); + const faucetId = searchParams.get('faucetId'); + const supportedChain = findSupportedChain(chainId, faucetId || undefined); const dripAmount = (supportedChain?.faucetThresholds?.dripAmount || 3).toString(); const tx = await transferEVMTokens( FAUCET_ADDRESS!, destinationAddress, chainId, - dripAmount + dripAmount, + faucetId || undefined ); const response: TransferResponse = { diff --git a/components/toolbox/components/ConnectWallet/EVMFaucetButton.tsx b/components/toolbox/components/ConnectWallet/EVMFaucetButton.tsx index 7c29bbcc850..fb608fa1eef 100644 --- a/components/toolbox/components/ConnectWallet/EVMFaucetButton.tsx +++ b/components/toolbox/components/ConnectWallet/EVMFaucetButton.tsx @@ -8,6 +8,7 @@ const LOW_BALANCE_THRESHOLD = 1; interface EVMFaucetButtonProps { chainId: number; + faucetChainId?: string; // to distinguish between multiple faucets on same chainId className?: string; buttonProps?: React.ButtonHTMLAttributes; children?: React.ReactNode; @@ -15,6 +16,7 @@ interface EVMFaucetButtonProps { export const EVMFaucetButton = ({ chainId, + faucetChainId, className, buttonProps, children, @@ -29,10 +31,13 @@ export const EVMFaucetButton = ({ const l1List = useL1List(); const { claimEVMTokens, isClaimingEVM } = useTestnetFaucet(); - const chainConfig = l1List.find( - (chain: L1ListItem) => - chain.evmChainId === chainId && chain.hasBuilderHubFaucet - ); + // If faucetChainId is provided, find by ID, otherwise find by evmChainId for backward compatibility + const chainConfig = faucetChainId + ? l1List.find((chain: L1ListItem) => chain.id === faucetChainId) + : l1List.find( + (chain: L1ListItem) => + chain.evmChainId === chainId && chain.hasBuilderHubFaucet + ); if (!isTestnet || !chainConfig) { return null; @@ -44,7 +49,8 @@ export const EVMFaucetButton = ({ if (isRequestingTokens || !walletEVMAddress) return; try { - await claimEVMTokens(chainId, false); + // Pass the unique faucetChainId if available, otherwise just chainId + await claimEVMTokens(chainId, false, faucetChainId); } catch (error) { // error handled via notifications from useTestnetFaucet } diff --git a/components/toolbox/console/primary-network/Faucet.tsx b/components/toolbox/console/primary-network/Faucet.tsx index 9ec9249a2b4..ad47d85c78a 100644 --- a/components/toolbox/console/primary-network/Faucet.tsx +++ b/components/toolbox/console/primary-network/Faucet.tsx @@ -50,10 +50,11 @@ function EVMFaucetCard({ chain }: { chain: L1ListItem }) { - Request {chain.name} Tokens + Request {chain.coinName} Tokens ); diff --git a/components/toolbox/stores/l1ListStore.ts b/components/toolbox/stores/l1ListStore.ts index 5665e7542c3..0382dcc2c8b 100644 --- a/components/toolbox/stores/l1ListStore.ts +++ b/components/toolbox/stores/l1ListStore.ts @@ -32,6 +32,7 @@ export type L1ListItem = { symbol: string; decimals: number; }; + erc20TokenAddress?: string; // If provided, faucet will drip this ERC20 token instead of native tokens }; const l1ListInitialStateFuji = { @@ -110,6 +111,32 @@ const l1ListInitialStateFuji = { "EVM-compatible L1 chain", "Deploy dApps & test interoperability with Dispatch" ] + }, + { + id: "dexalot-alot-cchain-fuji", + name: "Dexalot (ALOT on C-Chain)", + description: "Get ALOT ERC20 tokens on Fuji C-Chain for testing", + rpcUrl: "https://api.avax-test.network/ext/bc/C/rpc", + evmChainId: 43113, + coinName: "ALOT", + isTestnet: true, + subnetId: "11111111111111111111111111111111LpoYY", + wrappedTokenAddress: "0xd00ae08403B9bbb9124bB305C09058E32C39A48c", + validatorManagerAddress: "", + logoUrl: "https://images.ctfassets.net/gcj8jwzm6086/6tKCXL3AqxfxSUzXLGfN6r/be31715b87bc30c0e4d3da01a3d24e9a/dexalot-subnet.png", + wellKnownTeleporterRegistryAddress: "0xF86Cb19Ad8405AEFa7d09C778215D2Cb6eBfB228", + hasBuilderHubFaucet: true, + externalFaucetUrl: "https://core.app/tools/testnet-faucet", + explorerUrl: "https://testnet.snowtrace.io", + faucetThresholds: { + threshold: 10, + dripAmount: 0.2 + }, + erc20TokenAddress: "0x9983F755Bbd60d1886CbfE103c98C272AA0F03d6", + features: [ + "ERC20 ALOT tokens on C-Chain", + "Get testnet ALOT for Dexalot" + ] } ] as L1ListItem[], } diff --git a/hooks/useTestnetFaucet.ts b/hooks/useTestnetFaucet.ts index 9bad5e799bc..41c82c6a59b 100644 --- a/hooks/useTestnetFaucet.ts +++ b/hooks/useTestnetFaucet.ts @@ -28,11 +28,14 @@ export const useTestnetFaucet = () => { return l1List.filter((chain: L1ListItem) => chain.hasBuilderHubFaucet); }, [l1List]); - const claimEVMTokens = useCallback(async (chainId: number, silent: boolean = false): Promise => { + const claimEVMTokens = useCallback(async (chainId: number, silent: boolean = false, faucetChainId?: string): Promise => { if (!walletEVMAddress) { throw new Error("Wallet address is required") } if (!isTestnet) { throw new Error("Faucet is only available on testnet") } - const chainConfig = l1List.find((chain: L1ListItem) => chain.evmChainId === chainId && chain.hasBuilderHubFaucet); + // If faucetChainId is provided, find by ID, otherwise find by evmChainId for backward compatibility + const chainConfig = faucetChainId + ? l1List.find((chain: L1ListItem) => chain.id === faucetChainId) + : l1List.find((chain: L1ListItem) => chain.evmChainId === chainId && chain.hasBuilderHubFaucet); if (!chainConfig) { throw new Error(`Unsupported chain or faucet not available for chain ID ${chainId}`) } @@ -40,7 +43,9 @@ export const useTestnetFaucet = () => { try { const faucetRequest = async () => { - const response = await fetch(`/api/evm-chain-faucet?address=${walletEVMAddress}&chainId=${chainId}`); + // Include the faucet ID to distinguish between multiple faucets on the same chain + const faucetIdParam = chainConfig.id ? `&faucetId=${encodeURIComponent(chainConfig.id)}` : ''; + const response = await fetch(`/api/evm-chain-faucet?address=${walletEVMAddress}&chainId=${chainId}${faucetIdParam}`); const rawText = await response.text(); let data;