diff --git a/agentkit-core/src/actions/disperseAction_test.ts b/agentkit-core/src/actions/disperseAction_test.ts index 4ecd3ba..937e82a 100644 --- a/agentkit-core/src/actions/disperseAction_test.ts +++ b/agentkit-core/src/actions/disperseAction_test.ts @@ -18,12 +18,12 @@ let gaslessCallsCount = 0; const mockSendTransaction = async (wallet: any, tx: any) => { mockTransactions.push(tx); gaslessCallsCount++; // Count each call to sendTransaction (which is gasless) - + console.log(`šŸ“¤ Gasless Transaction ${gaslessCallsCount}:`); console.log(` To: ${tx.to}`); console.log(` Value: ${tx.value?.toString() || "0"}`); console.log(` Data: ${tx.data.slice(0, 10)}... (${tx.data.length} chars)`); - + return { success: true, txHash: `0x${Math.random().toString(16).slice(2, 10)}`, @@ -45,7 +45,7 @@ async function runTests() { recipients: [{ address: "invalid", amount: "1.0" }], tokenAddress: "eth", }); - + if (result1.includes("Invalid address format")) { console.log("āœ… PASS: Invalid address correctly rejected"); } else { @@ -59,7 +59,7 @@ async function runTests() { recipients: [{ address: "0x1234567890123456789012345678901234567890", amount: "-1" }], tokenAddress: "eth", }); - + if (result2.includes("Invalid amount")) { console.log("āœ… PASS: Invalid amount correctly rejected"); } else { @@ -71,7 +71,7 @@ async function runTests() { console.log("\nTest 3: Valid ETH batch transfer (šŸ”„ GASLESS TEST)"); mockTransactions = []; // Reset gaslessCallsCount = 0; - + const result3 = await disperseTokens(mockWallet, { recipients: [ { address: "0x1234567890123456789012345678901234567890", amount: "0.1" }, @@ -79,15 +79,19 @@ async function runTests() { ], tokenAddress: "eth", }); - - if (result3.includes("šŸš€ Gasless Batch Transfer Completed!") && - result3.includes("Total Recipients: 2") && - result3.includes("Successful Transfers: 2") && - mockTransactions.length === 2 && - gaslessCallsCount === 2) { + + if ( + result3.includes("šŸš€ Gasless Batch Transfer Completed!") && + result3.includes("Total Recipients: 2") && + result3.includes("Successful Transfers: 2") && + mockTransactions.length === 2 && + gaslessCallsCount === 2 + ) { console.log("āœ… PASS: ETH batch transfer works correctly"); - console.log(`šŸ”„ GASLESS CONFIRMED: ${gaslessCallsCount} transactions sent via gasless sendTransaction service`); - + console.log( + `šŸ”„ GASLESS CONFIRMED: ${gaslessCallsCount} transactions sent via gasless sendTransaction service`, + ); + // Verify transaction structure for gasless console.log("\nšŸ’” Transaction Structure Verification:"); mockTransactions.forEach((tx, i) => { @@ -103,7 +107,7 @@ async function runTests() { console.log("\nTest 4: Valid ERC20 token batch transfer (šŸ”„ GASLESS TEST)"); mockTransactions = []; // Reset gaslessCallsCount = 0; - + const result4 = await disperseTokens(mockWallet, { recipients: [ { address: "0x1234567890123456789012345678901234567890", amount: "100" }, @@ -111,19 +115,25 @@ async function runTests() { ], tokenAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC }); - - if (result4.includes("šŸš€ Gasless Batch Transfer Completed!") && - result4.includes("Total Recipients: 2") && - result4.includes("Successful Transfers: 2") && - mockTransactions.length === 2 && - gaslessCallsCount === 2) { + + if ( + result4.includes("šŸš€ Gasless Batch Transfer Completed!") && + result4.includes("Total Recipients: 2") && + result4.includes("Successful Transfers: 2") && + mockTransactions.length === 2 && + gaslessCallsCount === 2 + ) { console.log("āœ… PASS: ERC20 batch transfer works correctly"); - console.log(`šŸ”„ GASLESS CONFIRMED: ${gaslessCallsCount} transactions sent via gasless sendTransaction service`); - + console.log( + `šŸ”„ GASLESS CONFIRMED: ${gaslessCallsCount} transactions sent via gasless sendTransaction service`, + ); + // Verify ERC20 transfer encoding console.log("\nšŸ’” ERC20 Transfer Verification:"); mockTransactions.forEach((tx, i) => { - console.log(` Tx ${i + 1}: to=${tx.to} (token contract), value=0, data=${tx.data.slice(0, 10)}... (transfer function call)`); + console.log( + ` Tx ${i + 1}: to=${tx.to} (token contract), value=0, data=${tx.data.slice(0, 10)}... (transfer function call)`, + ); }); } else { console.log("āŒ FAIL: ERC20 batch transfer failed"); @@ -140,7 +150,7 @@ async function runTests() { ], tokenAddress: "eth", }); - + if (result5.includes("Total Amount Distributed: 4 ETH")) { console.log("āœ… PASS: Total amount calculated correctly"); } else { @@ -155,12 +165,12 @@ async function runTests() { address: `0x${"1".repeat(39)}${i.toString().padStart(1, "0")}`, amount: "1.0", })); - + DisperseInput.parse({ recipients: tooManyRecipients, tokenAddress: "eth", }); - + console.log("āŒ FAIL: Schema should reject 51 recipients"); } catch (error) { console.log("āœ… PASS: Schema correctly rejects too many recipients"); @@ -169,7 +179,7 @@ async function runTests() { console.log("\nšŸŽ‰ All tests completed!"); console.log("\nšŸ“‹ Summary:"); console.log("• Address validation: Working āœ…"); - console.log("• Amount validation: Working āœ…"); + console.log("• Amount validation: Working āœ…"); console.log("• ETH batch transfers: Working āœ…"); console.log("• ERC20 batch transfers: Working āœ…"); console.log("• Amount calculation: Working āœ…"); @@ -177,7 +187,6 @@ async function runTests() { console.log("• šŸ”„ GASLESS EXECUTION: Working āœ…"); console.log("\nšŸš€ The Disperse Action is ready for production!"); console.log("šŸ’° Users will pay $0 in gas fees thanks to 0xGasless paymaster!"); - } catch (error) { console.error("āŒ Test failed with error:", error); } finally { @@ -191,4 +200,4 @@ if (require.main === module) { runTests().catch(console.error); } -export { runTests }; \ No newline at end of file +export { runTests }; diff --git a/agentkit-core/src/constants.ts b/agentkit-core/src/constants.ts index e87b2e3..04cb9d1 100644 --- a/agentkit-core/src/constants.ts +++ b/agentkit-core/src/constants.ts @@ -1,46 +1,167 @@ -import { avalanche, fantom, moonbeam, metis, base, bsc, Chain } from "viem/chains"; +import { avalanche, fantom, moonbeam, metis, base, bsc, sepolia, Chain } from "viem/chains"; -export const supportedChains: Record = { - 8453: base, - 250: fantom, - 1284: moonbeam, - 1088: metis, - 43114: avalanche, - 56: bsc, +//i am adding this feature flag +export const TESTNET_SUPPORT = process.env.TESTNET_SUPPORT === 'true' || process.env.NODE_ENV === 'development'; + +// Helper function to get supported chains based on feature flag +export const getSupportedChains = (): Record => { + const mainnetChains: Record = { + 8453: base, + 250: fantom, + 1284: moonbeam, + 1088: metis, + 43114: avalanche, + 56: bsc, + }; + + if (TESTNET_SUPPORT) { + return { + ...mainnetChains, + 11155111: sepolia, + }; + } + + return mainnetChains; +}; + +export const supportedChains: Record = getSupportedChains(); + +// Helper function to get token mappings based on feature flag +export const getTokenMappings = (): Record> => { + const mainnetTokens: Record> = { + // Avalanche (43114) + 43114: { + USDT: "0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7", + USDC: "0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e", + WAVAX: "0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7", + "BTC.E": "0x152b9d0fdc40c096757f570a51e494bd4b943e50", + BUSD: "0x9c9e5fd8bbc25984b178fdce6117defa39d2db39", + WETH: "0x49d5c2bdffac6ce2bfdb6640f4f80f226bc10bab", + "USDC.E": "0xa7d7079b0fead91f3e65f86e8915cb59c1a4c664", + WBTC: "0x50b7545627a5162f82a992c33b87adc75187b218", + DAI: "0xd586e7f844cea2f87f50152665bcbc2c279d8d70", + }, + // BNB Chain (56) + 56: { + USDT: "0x55d398326f99059ff775485246999027b3197955", + WBNB: "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", + WETH: "0x2170ed0880ac9a755fd29b2688956bd959f933f8", + BUSD: "0xe9e7cea3dedca5984780bafc599bd69add087d56", + CAKE: "0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82", + SOL: "0x570a5d26f7765ecb712c0924e4de545b89fd43df", + TST: "0x86bb94ddd16efc8bc58e6b056e8df71d9e666429", + DAI: "0x1af3f329e8be154074d8769d1ffa4ee058b1dbc3", + TON: "0x76a797a59ba2c17726896976b7b3747bfd1d220f", + PEPE: "0x25d887ce7a35172c62febfd67a1856f20faebb00", + }, + // Base (8453) + 8453: { + WETH: "0x4200000000000000000000000000000000000006", + USDC: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + DAI: "0x50c5725949a6f0c72e6c4a641f24049a917db0cb", + }, + // Fantom (250) + 250: {}, + // Moonbeam (1284) + 1284: {}, + // Metis (1088) + 1088: {}, + }; + + if (TESTNET_SUPPORT) { + return { + ...mainnetTokens, + // Sepolia Testnet (11155111) + 11155111: { + // public Sepolia addresses + WETH: "0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14", + + // TODO: MAINTAINERS - Add actual Sepolia token addresses after deployment + USDC: (process.env.SEPOLIA_USDC_ADDRESS || "0x0000000000000000000000000000000000000000") as `0x${string}`, + USDT: (process.env.SEPOLIA_USDT_ADDRESS || "0x0000000000000000000000000000000000000000") as `0x${string}`, + DAI: (process.env.SEPOLIA_DAI_ADDRESS || "0x0000000000000000000000000000000000000000") as `0x${string}`, + } + }; + } + + return mainnetTokens; }; // Token mappings by chain ID and ticker symbol -export const tokenMappings: Record> = { - // Avalanche (43114) - 43114: { - USDT: "0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7", - USDC: "0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e", - WAVAX: "0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7", - "BTC.E": "0x152b9d0fdc40c096757f570a51e494bd4b943e50", - BUSD: "0x9c9e5fd8bbc25984b178fdce6117defa39d2db39", - WETH: "0x49d5c2bdffac6ce2bfdb6640f4f80f226bc10bab", - "USDC.E": "0xa7d7079b0fead91f3e65f86e8915cb59c1a4c664", - WBTC: "0x50b7545627a5162f82a992c33b87adc75187b218", - DAI: "0xd586e7f844cea2f87f50152665bcbc2c279d8d70", - }, - // BNB Chain (56) - 56: { - USDT: "0x55d398326f99059ff775485246999027b3197955", - WBNB: "0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c", - WETH: "0x2170ed0880ac9a755fd29b2688956bd959f933f8", - BUSD: "0xe9e7cea3dedca5984780bafc599bd69add087d56", - CAKE: "0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82", - SOL: "0x570a5d26f7765ecb712c0924e4de545b89fd43df", - TST: "0x86bb94ddd16efc8bc58e6b056e8df71d9e666429", - DAI: "0x1af3f329e8be154074d8769d1ffa4ee058b1dbc3", - TON: "0x76a797a59ba2c17726896976b7b3747bfd1d220f", - PEPE: "0x25d887ce7a35172c62febfd67a1856f20faebb00", - }, - // Add other chains as needed - 8453: {}, // Base - 250: {}, // Fantom - 1284: {}, // Moonbeam - 1088: {}, // Metis +export const tokenMappings: Record> = getTokenMappings(); + +// Network configuration type with optional entryPoint for mainnet +type NetworkConfig = { + rpcUrl: string; + paymaster?: string; + accountFactory?: string; + entryPoint?: string; + isTestnet?: boolean; + name: string; +}; + +// Helper function to get network config based on feature flag +export const getNetworkConfig = (): Record => { + const mainnetConfig: Record = { + // Mainnet configurations + 8453: { + rpcUrl: "https://rpc.ankr.com/base", + name: "Base" + }, + 250: { + rpcUrl: "https://rpc.ankr.com/fantom", + name: "Fantom" + }, + 1284: { + rpcUrl: "https://rpc.ankr.com/moonbeam", + name: "Moonbeam" + }, + 1088: { + rpcUrl: "https://rpc.ankr.com/metis", + name: "Metis" + }, + 43114: { + rpcUrl: "https://rpc.ankr.com/avalanche", + name: "Avalanche" + }, + 56: { + rpcUrl: "https://rpc.ankr.com/bsc", + name: "BNB Smart Chain" + }, + }; + + if (TESTNET_SUPPORT) { + return { + ...mainnetConfig, + // Testnet configurations + 11155111: { + rpcUrl: process.env.SEPOLIA_RPC_URL || "https://rpc.ankr.com/eth_sepolia", + paymaster: process.env.SEPOLIA_PAYMASTER_ADDRESS, // TODO: MAINTAINERS - Set after deployment + accountFactory: process.env.SEPOLIA_ACCOUNT_FACTORY_ADDRESS, // TODO: MAINTAINERS - Set after deployment + entryPoint: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", + isTestnet: true, + name: "Sepolia Testnet" + } + }; + } + + return mainnetConfig; +}; + +// Network configuration +export const networkConfig: Record = getNetworkConfig(); + +// Helper functions +export const getAvailableChains = (): number[] => { + return Object.keys(supportedChains).map(Number); +}; + +export const isTestnet = (chainId: number): boolean => { + return networkConfig[chainId]?.isTestnet || false; +}; + +export const getChainName = (chainId: number): string => { + return networkConfig[chainId]?.name || supportedChains[chainId]?.name || 'Unknown Chain'; }; // Common tokens that exist on most chains (for easier reference) @@ -57,15 +178,17 @@ Capabilities: Important Information: - The wallet is already configured with the SDK. DO NOT generate or mention private keys when using any tools. -- You can only operate on supported networks: Base (8453), Fantom (250), Moonbeam (1284), Metis (1088), Avalanche (43114), and BSC (56) +- You can operate on supported networks: Base (8453), Fantom (250), Moonbeam (1284), Metis (1088), Avalanche (43114), BSC (56)${TESTNET_SUPPORT ? ', and Sepolia Testnet (11155111) for testing' : ''} - All transactions are gasless - users don't need native tokens to perform actions - Default RPC uses Ankr's free tier which has rate limitations +${TESTNET_SUPPORT ? '- When using testnet chains, all tokens are for testing purposes only and have no real value' : ''} When interacting with tokens: - Always verify token addresses are valid - Check token balances before transfers - Use proper decimal precision for token amounts - You can use token symbols like "USDC", "USDT", "WETH" instead of addresses on supported chains +${TESTNET_SUPPORT ? '- Be aware when using testnets - transactions use test tokens with no real value' : ''} You can assist users by: 1. Getting wallet balances - when asked about balances, immediately check them without asking for confirmation @@ -76,7 +199,8 @@ You can assist users by: 4. Creating new smart accounts 5. Checking transaction status -Please ensure all addresses and token amounts are properly validated before executing transactions.`; +Please ensure all addresses and token amounts are properly validated before executing transactions. +${TESTNET_SUPPORT ? 'When working with testnet chains, always inform users they are using test tokens.' : ''}`; export const TokenABI = [ { @@ -370,4 +494,4 @@ export const TokenABI = [ export const Permit2Abi = [ "function approve(address token, address spender, uint160 amount, uint48 expiration) external", "function allowance(address owner, address token, address spender) external view returns (uint160, uint48, uint48)", -] as const; +] as const; \ No newline at end of file diff --git a/agentkit-core/src/utils/validation.ts b/agentkit-core/src/utils/validation.ts new file mode 100644 index 0000000..3243c10 --- /dev/null +++ b/agentkit-core/src/utils/validation.ts @@ -0,0 +1,64 @@ +import { networkConfig, supportedChains, tokenMappings, getChainName, isTestnet } from '../constants'; + +export const validateChainSupport = (chainId: number): void => { + if (!supportedChains[chainId]) { + const availableChains = Object.keys(supportedChains).join(', '); + throw new Error(`Chain ID ${chainId} is not supported. Available chains: ${availableChains}`); + } + + const config = networkConfig[chainId]; + if (config?.isTestnet && !config.paymaster) { + throw new Error(`Testnet chain ${chainId} is not fully configured. Missing paymaster address. Please contact maintainers.`); + } +}; + +export const validateTokenSupport = (chainId: number, tokenSymbol: string): void => { + validateChainSupport(chainId); + + const tokenAddress = tokenMappings[chainId]?.[tokenSymbol]; + if (!tokenAddress || tokenAddress === "0x0000000000000000000000000000000000000000") { + const availableTokens = Object.keys(tokenMappings[chainId] || {}).join(', '); + throw new Error( + `Token ${tokenSymbol} is not supported on chain ${chainId} (${getChainName(chainId)}). ` + + `Available tokens: ${availableTokens || 'None configured'}` + ); + } +}; + +export const warnIfTestnet = (chainId: number): void => { + if (isTestnet(chainId)) { + console.warn(`āš ļø You are using testnet chain ${getChainName(chainId)}. Transactions use test tokens with no real value.`); + } +}; + +export const validateAddress = (address: string): boolean => { + // Basic Ethereum address validation + const addressRegex = /^0x[a-fA-F0-9]{40}$/; + return addressRegex.test(address); +}; + +export const getTokenAddress = (chainId: number, tokenSymbol: string): `0x${string}` => { + validateTokenSupport(chainId, tokenSymbol); + return tokenMappings[chainId][tokenSymbol]; +}; + +export const getSupportedTokens = (chainId: number): string[] => { + validateChainSupport(chainId); + return Object.keys(tokenMappings[chainId] || {}); +}; + +export const getChainInfo = (chainId: number) => { + validateChainSupport(chainId); + const config = networkConfig[chainId]; + const chain = supportedChains[chainId]; + + return { + chainId, + name: getChainName(chainId), + isTestnet: isTestnet(chainId), + rpcUrl: config?.rpcUrl, + hasPaymaster: !!config?.paymaster, + supportedTokens: getSupportedTokens(chainId), + nativeCurrency: chain?.nativeCurrency, + }; +}; \ No newline at end of file diff --git a/agentkit-demo/.env.sample b/agentkit-demo/.env.sample index f6dd277..443f01c 100644 --- a/agentkit-demo/.env.sample +++ b/agentkit-demo/.env.sample @@ -1,5 +1,49 @@ +# ================================== +# CORE ENVIRONMENT VARIABLES +# ================================== OPENROUTER_API_KEY= PRIVATE_KEY= CHAIN_ID= RPC_URL= -API_KEY= \ No newline at end of file +API_KEY= + +# ================================== +# TESTNET SUPPORT CONFIGURATION +# ================================== +# Testnet Support (set to 'true' to enable testnet chains) +# Enabled by default in development mode (NODE_ENV=development) +TESTNET_SUPPORT=false + +# ================================== +# SEPOLIA TESTNET CONFIGURATION +# Required only when TESTNET_SUPPORT=true +# ================================== +# Sepolia RPC URL (defaults to Ankr if not provided) +SEPOLIA_RPC_URL=https://rpc.ankr.com/eth_sepolia + +# Smart Contract Addresses (TO BE PROVIDED BY MAINTAINERS) +# These addresses will be set after maintainers deploy contracts +SEPOLIA_PAYMASTER_ADDRESS= +SEPOLIA_ACCOUNT_FACTORY_ADDRESS= + +# Token Contract Addresses (TO BE PROVIDED BY MAINTAINERS) +# These can be existing Sepolia test tokens or newly deployed ones +SEPOLIA_USDC_ADDRESS= +SEPOLIA_USDT_ADDRESS= +SEPOLIA_DAI_ADDRESS= + +# ================================== +# OPTIONAL: CUSTOM RPC ENDPOINTS +# ================================== +# If you need higher rate limits, you can use your own RPC endpoints +# SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/YOUR_PROJECT_ID +# SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY + +# ================================== +# DEVELOPMENT NOTES +# ================================== +# For local development: +# 1. Set TESTNET_SUPPORT=true or use NODE_ENV=development +# 2. You can test the configuration changes without deployed contracts +# 3. Some features will show warnings until maintainers deploy infrastructure +# 4. All testnet functionality is safely disabled in production by default \ No newline at end of file