diff --git a/.gitignore b/.gitignore index 2713db2..1e17ae7 100644 --- a/.gitignore +++ b/.gitignore @@ -131,4 +131,8 @@ api.json # JSDoc docs/ -plugin-*/ \ No newline at end of file +plugin-*/vialabs-testnet/artifacts/ +vialabs-testnet/cache/ +vialabs-testnet/typechain-types/ +vialabs-testnet/node_modules/ +vialabs-testnet/quickstart-token-ref/ diff --git a/agentkit-core/src/actions/ViaLabsAction/index.ts b/agentkit-core/src/actions/ViaLabsAction/index.ts new file mode 100644 index 0000000..73cf336 --- /dev/null +++ b/agentkit-core/src/actions/ViaLabsAction/index.ts @@ -0,0 +1,29 @@ +/** + * ViaLabs Cross-Chain Actions + * + * Export all ViaLabs-related actions and utilities + */ + +export { ViaLabsBridgeAction, vialabsBridge, ViaLabsBridgeInput } from "./vialabsBridgeAction"; +export { ViaLabsInfoAction, vialabsGetInfo, ViaLabsInfoInput } from "./vialabsInfoAction"; + +export { + VIALABS_SUPPORTED_CHAINS, + ViaLabsBridgeABI, + MessageClientABI, + isVialabsChainSupported, + getVialabsChainConfig, + getSupportedChainIds, + getTestnetChainIds, + getMainnetChainIds, +} from "./vialabsConstants"; + +export { + isRouteSupported, + getVialabsTokenBalance, + getVialabsTokenInfo, + isDestinationChainActive, + formatBridgeSummary, + getSupportedChainsSummary, +} from "./vialabsHelpers"; + diff --git a/agentkit-core/src/actions/ViaLabsAction/vialabsBridgeAction.ts b/agentkit-core/src/actions/ViaLabsAction/vialabsBridgeAction.ts new file mode 100644 index 0000000..5e7bee5 --- /dev/null +++ b/agentkit-core/src/actions/ViaLabsAction/vialabsBridgeAction.ts @@ -0,0 +1,342 @@ +/** + * ViaLabs Cross-Chain Bridge Action + * + * Enables AI agents to bridge tokens across chains using ViaLabs + * cross-chain messaging infrastructure. + */ + +import { z } from "zod"; +import type { ZeroXgaslessSmartAccount, Transaction } from "@0xgasless/smart-account"; +import { encodeFunctionData, parseUnits, createPublicClient, http } from "viem"; +import type { AgentkitAction } from "../../agentkit"; +import { sendTransaction } from "../../services"; +import { + ViaLabsBridgeABI, + isVialabsChainSupported, + getVialabsChainConfig, + getSupportedChainIds, +} from "./vialabsConstants"; +import { + isRouteSupported, + getVialabsTokenInfo, + getVialabsTokenBalance, + isDestinationChainActive, + formatBridgeSummary, + getSupportedChainsSummary, +} from "./vialabsHelpers"; + +const VIALABS_BRIDGE_PROMPT = ` +This tool bridges tokens across different blockchains using ViaLabs cross-chain messaging. + +It takes the following inputs: +- tokenAddress: The address of the ViaLabs-enabled token contract to bridge +- destChainId: The destination chain ID to bridge tokens to +- recipient: The recipient address on the destination chain +- amount: The amount of tokens to bridge (as a string, e.g., "10.5") + +Important notes: +- The token contract must be a ViaLabs-enabled cross-chain token (extends MessageClient) +- Both source and destination chains must be configured on the token contract +- Tokens are burned on the source chain and minted on the destination chain +- This is NOT a wrapped token bridge - it's native cross-chain token transfer +- The action will wait and track until tokens arrive on destination chain + +Supported chains include: Avalanche, Avalanche Fuji (testnet), Base, Base Sepolia (testnet), BNB Chain. + +Example usage: +"Bridge 100 tokens from contract 0x... to Base Sepolia chain (84532) for recipient 0x..." +`; + +/** + * Input schema for ViaLabs bridge action + */ +export const ViaLabsBridgeInput = z + .object({ + tokenAddress: z + .string() + .describe("The ViaLabs-enabled token contract address to bridge from"), + destChainId: z + .number() + .describe("The destination chain ID to bridge tokens to"), + recipient: z + .string() + .describe("The recipient address on the destination chain"), + amount: z + .string() + .describe("The amount of tokens to bridge (e.g., '10.5')"), + }) + .strip() + .describe("Input for bridging tokens across chains using ViaLabs"); + +// ERC20 ABI for balance checking +const ERC20_BALANCE_ABI = [ + { + name: "balanceOf", + type: "function", + stateMutability: "view", + inputs: [{ name: "account", type: "address" }], + outputs: [{ name: "", type: "uint256" }], + }, +] as const; + +// Chain RPC endpoints for destination tracking +const CHAIN_RPC_ENDPOINTS: Record = { + 43113: "https://api.avax-test.network/ext/bc/C/rpc", // Avalanche Fuji + 84532: "https://sepolia.base.org", // Base Sepolia + 43114: "https://api.avax.network/ext/bc/C/rpc", // Avalanche Mainnet + 8453: "https://mainnet.base.org", // Base Mainnet + 56: "https://bsc-dataseed.binance.org/", // BNB Chain +}; + +// Destination token addresses (same order as HELLO_ERC20_TESTNET_TOKENS) +const DESTINATION_TOKEN_ADDRESSES: Record = { + 43113: "0xc8600dE63d7cbA25967ecf4894be84dB1c9Ee137", // Avalanche Fuji + 84532: "0xb9dB93d419bEDc2C20fe39248D560E7CB1aAABD0", // Base Sepolia +}; + +/** + * Sleep utility + */ +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Get balance on destination chain + */ +async function getDestinationBalance( + destChainId: number, + tokenAddress: `0x${string}`, + recipient: `0x${string}`, +): Promise { + const rpcUrl = CHAIN_RPC_ENDPOINTS[destChainId]; + if (!rpcUrl) { + throw new Error(`No RPC endpoint for chain ${destChainId}`); + } + + // Get the destination token address + const destTokenAddress = DESTINATION_TOKEN_ADDRESSES[destChainId] || tokenAddress; + + const client = createPublicClient({ + transport: http(rpcUrl), + }); + + const balance = await client.readContract({ + address: destTokenAddress, + abi: ERC20_BALANCE_ABI, + functionName: "balanceOf", + args: [recipient], + }); + + return balance; +} + +/** + * Bridge tokens across chains using ViaLabs with destination tracking + */ +export async function vialabsBridge( + wallet: ZeroXgaslessSmartAccount, + args: z.infer, +): Promise { + try { + const sourceChainId = wallet.rpcProvider.chain?.id; + const startTime = Date.now(); + + if (!sourceChainId) { + return "Error: Could not determine source chain ID from wallet."; + } + + // Validate source chain is supported + if (!isVialabsChainSupported(sourceChainId)) { + return `Error: Source chain ${sourceChainId} is not supported by ViaLabs.\n\n${getSupportedChainsSummary()}`; + } + + // Validate destination chain is supported + if (!isVialabsChainSupported(args.destChainId)) { + return `Error: Destination chain ${args.destChainId} is not supported by ViaLabs.\n\n${getSupportedChainsSummary()}`; + } + + // Validate route + if (!isRouteSupported(sourceChainId, args.destChainId)) { + return `Error: Route from chain ${sourceChainId} to ${args.destChainId} is not supported.`; + } + + // Cannot bridge to same chain + if (sourceChainId === args.destChainId) { + return "Error: Cannot bridge to the same chain. Use a regular transfer instead."; + } + + const tokenAddress = args.tokenAddress as `0x${string}`; + const recipient = args.recipient as `0x${string}`; + + // Get token info + const tokenInfo = await getVialabsTokenInfo(wallet, tokenAddress); + if (!tokenInfo) { + return `Error: Could not get token info for ${tokenAddress}. Make sure this is a valid ViaLabs-enabled token contract.`; + } + + // Check if destination chain is active on the token contract + const isActive = await isDestinationChainActive(wallet, tokenAddress, args.destChainId); + if (!isActive) { + const destConfig = getVialabsChainConfig(args.destChainId); + return `Error: Destination chain ${destConfig?.name || args.destChainId} is not configured on this token contract. The token contract owner needs to configure this chain first.`; + } + + // Get wallet address - try smart account first, fall back to EOA wallet + let walletAddress: `0x${string}`; + try { + walletAddress = (await wallet.getAddress()) as `0x${string}`; + } catch (addrError) { + // If smart account fails, try to create EOA wallet from env + const pk = process.env.PRIVATE_KEY as `0x${string}` | undefined; + const rpc = process.env.RPC_URL; + const chainId = process.env.CHAIN_ID ? Number(process.env.CHAIN_ID) : undefined; + + if (!pk) { + return `Error: Could not get wallet address. Smart account may not be initialized on this chain and no PRIVATE_KEY configured for EOA fallback.`; + } + + if (!rpc) { + return `Error: Could not get wallet address. Smart account may not be initialized on this chain and no RPC_URL configured for EOA fallback.`; + } + + try { + const { createEoaWallet } = await import("../../services"); + const eoa = createEoaWallet({ privateKey: pk, rpcUrl: rpc, chainId }); + walletAddress = eoa.address as `0x${string}`; + } catch (eoaError) { + return `Error: Could not get wallet address. Smart account failed and EOA fallback also failed: ${eoaError instanceof Error ? eoaError.message : String(eoaError)}`; + } + } + + const balance = await getVialabsTokenBalance(wallet, tokenAddress, walletAddress); + const amountBigInt = parseUnits(args.amount, tokenInfo.decimals); + + if (balance < amountBigInt) { + const formattedBalance = (Number(balance) / 10 ** tokenInfo.decimals).toFixed(6); + return `Error: Insufficient balance. You have ${formattedBalance} ${tokenInfo.symbol} but trying to bridge ${args.amount} ${tokenInfo.symbol}.`; + } + + // Get initial balance on destination chain for tracking + let initialDestBalance: bigint; + try { + initialDestBalance = await getDestinationBalance(args.destChainId, tokenAddress, recipient); + } catch (e) { + initialDestBalance = 0n; + } + + // Encode the bridge function call + const data = encodeFunctionData({ + abi: ViaLabsBridgeABI, + functionName: "bridge", + args: [BigInt(args.destChainId), recipient, amountBigInt], + }); + + // Create and send transaction + const tx: Transaction = { + to: tokenAddress, + data, + value: 0n, + }; + + const response = await sendTransaction(wallet, tx); + + if (!response || !response.success) { + return `Bridge transaction failed: ${response?.error || "Unknown error"}`; + } + + const txTime = Date.now(); + + // Format initial success message + const sourceConfig = getVialabsChainConfig(sourceChainId); + const destConfig = getVialabsChainConfig(args.destChainId); + const summary = formatBridgeSummary({ + tokenSymbol: tokenInfo.symbol, + amount: args.amount, + sourceChainId, + destChainId: args.destChainId, + recipient: args.recipient, + txHash: response.txHash, + }); + + // Build status message with source chain confirmation + let statusMessage = `✅ SOURCE CHAIN TRANSACTION CONFIRMED!\n\n${summary}\n\n`; + statusMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + statusMessage += `🔄 VIALABS CROSS-CHAIN VALIDATION IN PROGRESS...\n`; + statusMessage += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`; + statusMessage += `⏳ Waiting for ViaLabs validators to relay message...\n`; + statusMessage += ` • Source: ${sourceConfig?.name || sourceChainId} ✅\n`; + statusMessage += ` • Validators: Processing...\n`; + statusMessage += ` • Destination: ${destConfig?.name || args.destChainId} ⏳\n\n`; + + // Poll for destination balance change + const maxWaitTime = 10 * 60 * 1000; // 10 minutes max + const pollInterval = 15 * 1000; // Check every 15 seconds + let elapsed = 0; + let bridgeCompleted = false; + let finalDestBalance = initialDestBalance; + + while (elapsed < maxWaitTime) { + await sleep(pollInterval); + elapsed = Date.now() - txTime; + + try { + finalDestBalance = await getDestinationBalance(args.destChainId, tokenAddress, recipient); + + if (finalDestBalance > initialDestBalance) { + bridgeCompleted = true; + break; + } + } catch (e) { + // Continue polling on error + } + + // Update progress (this won't show in real-time due to how the action returns, + // but we log it for debugging) + const elapsedSecs = Math.floor(elapsed / 1000); + console.log(`[ViaLabs] Waiting for cross-chain message... ${elapsedSecs}s elapsed`); + } + + const totalTime = Date.now() - startTime; + const totalSeconds = Math.floor(totalTime / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + const timeString = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`; + + if (bridgeCompleted) { + const receivedAmount = finalDestBalance - initialDestBalance; + const formattedReceived = (Number(receivedAmount) / 10 ** tokenInfo.decimals).toFixed(6); + + return `✅ CROSS-CHAIN BRIDGE COMPLETE!\n\n${summary}\n\n` + + `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` + + `🎉 TOKENS RECEIVED ON DESTINATION CHAIN!\n` + + `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` + + ` • Source: ${sourceConfig?.name || sourceChainId} ✅ (burned ${args.amount} ${tokenInfo.symbol})\n` + + ` • ViaLabs Validators: Relayed ✅\n` + + ` • Destination: ${destConfig?.name || args.destChainId} ✅ (minted ${formattedReceived} ${tokenInfo.symbol})\n\n` + + `⏱️ Total cross-chain time: ${timeString}\n` + + `📊 ViaLabs validator processing verified!`; + } else { + return statusMessage + + `⚠️ Destination chain confirmation pending after ${timeString}.\n` + + ` ViaLabs validators are still processing the message.\n` + + ` The tokens should arrive soon. You can check the balance manually:\n\n` + + ` Destination Token: ${DESTINATION_TOKEN_ADDRESSES[args.destChainId] || tokenAddress}\n` + + ` Recipient: ${recipient}\n` + + ` Chain: ${destConfig?.name || args.destChainId}`; + } + } catch (error) { + return `Error executing ViaLabs bridge: ${error instanceof Error ? error.message : String(error)}`; + } +} + +/** + * ViaLabs Bridge Action class + */ +export class ViaLabsBridgeAction implements AgentkitAction { + public name = "vialabs_bridge"; + public description = VIALABS_BRIDGE_PROMPT; + public argsSchema = ViaLabsBridgeInput; + public func = vialabsBridge; + public smartAccountRequired = true; +} diff --git a/agentkit-core/src/actions/ViaLabsAction/vialabsConstants.ts b/agentkit-core/src/actions/ViaLabsAction/vialabsConstants.ts new file mode 100644 index 0000000..b6c582e --- /dev/null +++ b/agentkit-core/src/actions/ViaLabsAction/vialabsConstants.ts @@ -0,0 +1,188 @@ +/** + * ViaLabs Cross-Chain Messaging Constants + * + * Contains contract addresses, ABIs, and chain configurations + * for ViaLabs cross-chain messaging infrastructure. + */ + +// Supported chain IDs for ViaLabs cross-chain messaging +export const VIALABS_SUPPORTED_CHAINS: Record< + number, + { + name: string; + messageV3: `0x${string}`; + feeToken: `0x${string}`; // USDC or USDT + wrappedGas: `0x${string}`; // WETH, WAVAX, WBNB, etc. + explorer: string; + isTestnet: boolean; + } +> = { + // Testnets + 43113: { + // Avalanche Fuji + name: "Avalanche Fuji", + messageV3: "0x0000000000000000000000000000000000000000", // To be updated with actual address + feeToken: "0x5425890298aed601595a70AB815c96711a31Bc65", // USDC on Fuji + wrappedGas: "0xd00ae08403B9bbb9124bB305C09058E32C39A48c", // WAVAX on Fuji + explorer: "https://testnet.snowtrace.io", + isTestnet: true, + }, + 84532: { + // Base Sepolia + name: "Base Sepolia", + messageV3: "0x0000000000000000000000000000000000000000", // To be updated with actual address + feeToken: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // USDC on Base Sepolia + wrappedGas: "0x4200000000000000000000000000000000000006", // WETH on Base Sepolia + explorer: "https://sepolia.basescan.org", + isTestnet: true, + }, + + // Mainnets + 43114: { + // Avalanche C-Chain + name: "Avalanche", + messageV3: "0x0000000000000000000000000000000000000000", // To be updated with actual address + feeToken: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", // USDC on Avalanche + wrappedGas: "0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7", // WAVAX on Avalanche + explorer: "https://snowtrace.io", + isTestnet: false, + }, + 8453: { + // Base + name: "Base", + messageV3: "0x0000000000000000000000000000000000000000", // To be updated with actual address + feeToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base + wrappedGas: "0x4200000000000000000000000000000000000006", // WETH on Base + explorer: "https://basescan.org", + isTestnet: false, + }, + 56: { + // BNB Chain + name: "BNB Chain", + messageV3: "0x0000000000000000000000000000000000000000", // To be updated with actual address + feeToken: "0x55d398326f99059fF775485246999027B3197955", // USDT on BSC + wrappedGas: "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", // WBNB on BSC + explorer: "https://bscscan.com", + isTestnet: false, + }, +}; + +// Test HelloERC20 token addresses deployed for testing +// These are ViaLabs-enabled cross-chain tokens deployed on testnets (v2 with real MessageClient) +export const HELLO_ERC20_TESTNET_TOKENS: Record = { + 43113: "0xc8600dE63d7cbA25967ecf4894be84dB1c9Ee137", // Avalanche Fuji + 84532: "0xb9dB93d419bEDc2C20fe39248D560E7CB1aAABD0", // Base Sepolia +}; + +// Common cross-chain token contract ABI for ViaLabs-enabled tokens +// This ABI supports the bridge() function that triggers cross-chain transfers +export const ViaLabsBridgeABI = [ + // bridge function - burns tokens on source and triggers cross-chain message + { + inputs: [ + { internalType: "uint256", name: "_destChainId", type: "uint256" }, + { internalType: "address", name: "_recipient", type: "address" }, + { internalType: "uint256", name: "_amount", type: "uint256" }, + ], + name: "bridge", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + // Standard ERC20 functions + { + inputs: [ + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "approve", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "account", type: "address" }], + name: "balanceOf", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "decimals", + outputs: [{ internalType: "uint8", name: "", type: "uint8" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "symbol", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "name", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + // Check if chain is configured for cross-chain + { + inputs: [{ internalType: "uint256", name: "chainId", type: "uint256" }], + name: "isChainActive", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, +] as const; + +// MessageClient ABI for interacting with ViaLabs messaging directly +export const MessageClientABI = [ + // Get fee estimate for cross-chain message + { + inputs: [{ internalType: "uint256", name: "_destChainId", type: "uint256" }], + name: "getFee", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + // Check message status + { + inputs: [{ internalType: "uint256", name: "_txId", type: "uint256" }], + name: "getMessageStatus", + outputs: [{ internalType: "uint8", name: "", type: "uint8" }], + stateMutability: "view", + type: "function", + }, +] as const; + +// Helper to check if a chain is supported by ViaLabs +export function isVialabsChainSupported(chainId: number): boolean { + return chainId in VIALABS_SUPPORTED_CHAINS; +} + +// Helper to get chain config +export function getVialabsChainConfig(chainId: number) { + return VIALABS_SUPPORTED_CHAINS[chainId] || null; +} + +// Get list of supported chain IDs +export function getSupportedChainIds(): number[] { + return Object.keys(VIALABS_SUPPORTED_CHAINS).map(Number); +} + +// Get testnet chain IDs only +export function getTestnetChainIds(): number[] { + return Object.entries(VIALABS_SUPPORTED_CHAINS) + .filter(([, config]) => config.isTestnet) + .map(([id]) => Number(id)); +} + +// Get mainnet chain IDs only +export function getMainnetChainIds(): number[] { + return Object.entries(VIALABS_SUPPORTED_CHAINS) + .filter(([, config]) => !config.isTestnet) + .map(([id]) => Number(id)); +} diff --git a/agentkit-core/src/actions/ViaLabsAction/vialabsHelpers.ts b/agentkit-core/src/actions/ViaLabsAction/vialabsHelpers.ts new file mode 100644 index 0000000..0e98a1c --- /dev/null +++ b/agentkit-core/src/actions/ViaLabsAction/vialabsHelpers.ts @@ -0,0 +1,151 @@ +/** + * ViaLabs Helper Functions + * + * Utility functions for ViaLabs cross-chain messaging operations + */ + +import type { ZeroXgaslessSmartAccount } from "@0xgasless/smart-account"; +import { + VIALABS_SUPPORTED_CHAINS, + ViaLabsBridgeABI, + isVialabsChainSupported, + getVialabsChainConfig, +} from "./vialabsConstants"; + +/** + * Check if a cross-chain route is supported + */ +export function isRouteSupported(sourceChainId: number, destChainId: number): boolean { + return isVialabsChainSupported(sourceChainId) && isVialabsChainSupported(destChainId); +} + +/** + * Get token balance for a ViaLabs-enabled token + */ +export async function getVialabsTokenBalance( + wallet: ZeroXgaslessSmartAccount, + tokenAddress: `0x${string}`, + ownerAddress: `0x${string}`, +): Promise { + try { + const balance = (await wallet.rpcProvider.readContract({ + abi: ViaLabsBridgeABI, + address: tokenAddress, + functionName: "balanceOf", + args: [ownerAddress], + })) as bigint; + return balance; + } catch (error) { + console.error("Error getting token balance:", error); + return BigInt(0); + } +} + +/** + * Get token info (name, symbol, decimals) + */ +export async function getVialabsTokenInfo( + wallet: ZeroXgaslessSmartAccount, + tokenAddress: `0x${string}`, +): Promise<{ name: string; symbol: string; decimals: number } | null> { + try { + const [name, symbol, decimals] = await Promise.all([ + wallet.rpcProvider.readContract({ + abi: ViaLabsBridgeABI, + address: tokenAddress, + functionName: "name", + }) as Promise, + wallet.rpcProvider.readContract({ + abi: ViaLabsBridgeABI, + address: tokenAddress, + functionName: "symbol", + }) as Promise, + wallet.rpcProvider.readContract({ + abi: ViaLabsBridgeABI, + address: tokenAddress, + functionName: "decimals", + }) as Promise, + ]); + return { name, symbol, decimals }; + } catch (error) { + console.error("Error getting token info:", error); + return null; + } +} + +/** + * Check if destination chain is active on the token contract + */ +export async function isDestinationChainActive( + wallet: ZeroXgaslessSmartAccount, + tokenAddress: `0x${string}`, + destChainId: number, +): Promise { + try { + const isActive = (await wallet.rpcProvider.readContract({ + abi: ViaLabsBridgeABI, + address: tokenAddress, + functionName: "isChainActive", + args: [BigInt(destChainId)], + })) as boolean; + return isActive; + } catch (error) { + // If the function doesn't exist or reverts, assume it's not a ViaLabs token + console.error("Error checking chain active status:", error); + return false; + } +} + +/** + * Format a bridge transaction summary + */ +export function formatBridgeSummary(params: { + tokenSymbol: string; + amount: string; + sourceChainId: number; + destChainId: number; + recipient: string; + txHash?: string; +}): string { + const sourceConfig = getVialabsChainConfig(params.sourceChainId); + const destConfig = getVialabsChainConfig(params.destChainId); + + const sourceName = sourceConfig?.name || `Chain ${params.sourceChainId}`; + const destName = destConfig?.name || `Chain ${params.destChainId}`; + + let summary = `Bridge ${params.amount} ${params.tokenSymbol} from ${sourceName} to ${destName}`; + summary += `\nRecipient: ${params.recipient}`; + + if (params.txHash) { + const explorer = sourceConfig?.explorer || ""; + summary += `\nTransaction: ${params.txHash}`; + if (explorer) { + summary += `\nExplorer: ${explorer}/tx/${params.txHash}`; + } + } + + return summary; +} + +/** + * Get supported chains summary for user display + */ +export function getSupportedChainsSummary(): string { + const chains = Object.entries(VIALABS_SUPPORTED_CHAINS); + const testnets = chains.filter(([, c]) => c.isTestnet); + const mainnets = chains.filter(([, c]) => !c.isTestnet); + + let summary = "**ViaLabs Supported Chains:**\n\n"; + + summary += "**Testnets:**\n"; + testnets.forEach(([id, config]) => { + summary += `- ${config.name} (Chain ID: ${id})\n`; + }); + + summary += "\n**Mainnets:**\n"; + mainnets.forEach(([id, config]) => { + summary += `- ${config.name} (Chain ID: ${id})\n`; + }); + + return summary; +} diff --git a/agentkit-core/src/actions/ViaLabsAction/vialabsInfoAction.ts b/agentkit-core/src/actions/ViaLabsAction/vialabsInfoAction.ts new file mode 100644 index 0000000..72d6db0 --- /dev/null +++ b/agentkit-core/src/actions/ViaLabsAction/vialabsInfoAction.ts @@ -0,0 +1,106 @@ +/** + * ViaLabs Get Info Action + * + * Provides information about ViaLabs cross-chain messaging support + * and helps users understand the available chains and requirements. + */ + +import { z } from "zod"; +import type { ZeroXgaslessSmartAccount } from "@0xgasless/smart-account"; +import type { AgentkitAction } from "../../agentkit"; +import { + VIALABS_SUPPORTED_CHAINS, + isVialabsChainSupported, +} from "./vialabsConstants"; +import { getSupportedChainsSummary } from "./vialabsHelpers"; + +const VIALABS_INFO_PROMPT = ` +This tool provides information about ViaLabs cross-chain messaging capabilities. + +Use this tool when: +- The user asks about cross-chain bridging options +- The user wants to know which chains are supported for ViaLabs +- The user needs help understanding how to bridge tokens across chains +- The user asks about ViaLabs capabilities + +It can optionally take a chainId to check if a specific chain is supported. +`; + +/** + * Input schema for ViaLabs info action + */ +export const ViaLabsInfoInput = z + .object({ + chainId: z + .number() + .optional() + .describe("Optional chain ID to check if it's supported"), + }) + .strip() + .describe("Input for getting ViaLabs cross-chain info"); + +/** + * Get information about ViaLabs cross-chain messaging + */ +export async function vialabsGetInfo( + wallet: ZeroXgaslessSmartAccount, + args: z.infer, +): Promise { + try { + const currentChainId = wallet.rpcProvider.chain?.id; + + let response = "# ViaLabs Cross-Chain Messaging\n\n"; + + // Check if specific chain was requested + if (args.chainId) { + const isSupported = isVialabsChainSupported(args.chainId); + const chainConfig = VIALABS_SUPPORTED_CHAINS[args.chainId]; + + if (isSupported && chainConfig) { + response += `✅ Chain ${args.chainId} (${chainConfig.name}) is supported!\n\n`; + response += `- Type: ${chainConfig.isTestnet ? "Testnet" : "Mainnet"}\n`; + response += `- Explorer: ${chainConfig.explorer}\n`; + } else { + response += `❌ Chain ${args.chainId} is not currently configured for ViaLabs bridging.\n\n`; + } + } + + // Add current chain info + if (currentChainId) { + const currentConfig = VIALABS_SUPPORTED_CHAINS[currentChainId]; + if (currentConfig) { + response += `\n## Current Chain\n`; + response += `You are connected to ${currentConfig.name} (Chain ID: ${currentChainId})\n`; + response += `Type: ${currentConfig.isTestnet ? "Testnet" : "Mainnet"}\n`; + } + } + + // Add supported chains summary + response += `\n${getSupportedChainsSummary()}`; + + // Add usage instructions + response += `\n## How to Bridge Tokens\n\n`; + response += `To bridge tokens using ViaLabs, you need:\n`; + response += `1. A ViaLabs-enabled token contract deployed on both source and destination chains\n`; + response += `2. The token contract address\n`; + response += `3. The destination chain ID\n`; + response += `4. The recipient address on the destination chain\n`; + response += `5. The amount to bridge\n\n`; + response += `Use the \`vialabs_bridge\` tool to execute a bridge transaction.\n`; + + return response; + } catch (error) { + return `Error getting ViaLabs info: ${error instanceof Error ? error.message : String(error)}`; + } +} + +/** + * ViaLabs Info Action class + */ +export class ViaLabsInfoAction implements AgentkitAction { + public name = "vialabs_info"; + public description = VIALABS_INFO_PROMPT; + public argsSchema = ViaLabsInfoInput; + public func = vialabsGetInfo; + public smartAccountRequired = true; +} diff --git a/agentkit-core/src/actions/index.ts b/agentkit-core/src/actions/index.ts index 90d27fb..fc613a5 100644 --- a/agentkit-core/src/actions/index.ts +++ b/agentkit-core/src/actions/index.ts @@ -19,6 +19,7 @@ import { import { DisperseAction } from "./disperseAction"; import { GetEoaAddressAction } from "./getEoaAddressAction"; import { GetEoaBalanceAction } from "./getEoaBalanceAction"; +import { ViaLabsBridgeAction, ViaLabsInfoAction } from "./ViaLabsAction"; export function getAllAgentkitActions(): AgentkitAction[] { return [ @@ -41,6 +42,8 @@ export function getAllAgentkitActions(): AgentkitAction[] { new SearchPairsAction(), new GetPairsByTokenAddressesAction(), new DisperseAction(), + new ViaLabsBridgeAction(), + new ViaLabsInfoAction(), ]; } diff --git a/vialabs-testnet/.env.example b/vialabs-testnet/.env.example new file mode 100644 index 0000000..569c33b --- /dev/null +++ b/vialabs-testnet/.env.example @@ -0,0 +1,3 @@ +PRIVATE_KEY=your_private_key_here +SNOWTRACE_API_KEY=optional_for_verification +BASESCAN_API_KEY=optional_for_verification diff --git a/vialabs-testnet/README.md b/vialabs-testnet/README.md new file mode 100644 index 0000000..6668a17 --- /dev/null +++ b/vialabs-testnet/README.md @@ -0,0 +1,55 @@ +# ViaLabs HelloERC20 Testnet Setup + +This folder contains the HelloERC20 example contract for testing ViaLabs cross-chain messaging on testnets. + +## Prerequisites + +1. Node.js v18+ +2. Testnet native tokens: + - Avalanche Fuji: Get AVAX from [Avalanche Faucet](https://core.app/en/tools/testnet-faucet/?subnet=c&token=c) + - Base Sepolia: Get ETH from [Base Faucet](https://www.alchemy.com/faucets/base-sepolia) + +## Quick Start + +```bash +# Install dependencies +npm install + +# Set up your private key +cp .env.example .env +# Edit .env with your private key + +# Deploy to Avalanche Fuji +npx hardhat run scripts/deploy.ts --network fuji + +# Deploy to Base Sepolia +npx hardhat run scripts/deploy.ts --network baseSepolia + +# Configure cross-chain (run after deploying to both chains) +npx hardhat run scripts/configure.ts --network fuji +npx hardhat run scripts/configure.ts --network baseSepolia +``` + +## Contract Overview + +The `HelloERC20` contract is a simple cross-chain token that: +1. Mints 1,000,000 tokens to the deployer on first deployment +2. Implements `bridge(destChainId, recipient, amount)` to burn tokens and send cross-chain message +3. Implements `_processMessage()` to receive messages and mint tokens on destination + +## Testing with AgentKit + +After deployment and configuration, update the token addresses in your AgentKit demo: + +```typescript +// In agentkit-demo, ask the agent: +"Bridge 100 HELLO tokens to Base Sepolia (chain 84532) to address 0x..." +``` + +## ViaLabs Fee Notes + +- **Testnets**: Most fees are sponsored by ViaLabs +- **Source fee**: Paid in USDC/USDT (FEE_TOKEN) +- **Destination gas**: Paid in wrapped native tokens (WAVAX, WETH) + +For testnets, ensure your contract has small amounts of testnet USDC and wrapped native tokens. diff --git a/vialabs-testnet/contracts/HelloERC20.sol b/vialabs-testnet/contracts/HelloERC20.sol new file mode 100644 index 0000000..88ad374 --- /dev/null +++ b/vialabs-testnet/contracts/HelloERC20.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +/** + * @title HelloERC20 + * @dev Cross-chain ERC20 token using ViaLabs MessageClient + * + * This token implements proper cross-chain bridging via ViaLabs infrastructure: + * - Burn tokens on source chain + * - Call _sendMessage() to send cross-chain message via ViaLabs validators + * - Receive _processMessage() on destination chain + * - Mint tokens to recipient on destination chain + */ + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; +import "@vialabs-io/contracts/MessageClient.sol"; + +contract HelloERC20 is ERC20, ERC20Burnable, MessageClient { + // Events + event BridgeInitiated(uint256 indexed txId, uint256 indexed destChainId, address indexed recipient, uint256 amount); + event BridgeReceived(uint256 indexed txId, uint256 indexed sourceChainId, address indexed recipient, uint256 amount); + + constructor() ERC20("HelloERC20", "HELLO") { + // Set MESSAGE_OWNER to deployer (required for configureClient to work) + MESSAGE_OWNER = msg.sender; + + // Mint initial supply to deployer for testing + _mint(msg.sender, 1_000_000 * 10**decimals()); + } + + /** + * @dev Check if a chain is active for bridging + */ + function isChainActive(uint256 _chainId) external view returns (bool) { + return CHAINS[_chainId].endpoint != address(0); + } + + /** + * @dev Bridge tokens to another chain + * Burns tokens on this chain and sends cross-chain message via ViaLabs + */ + function bridge(uint256 _destChainId, address _recipient, uint256 _amount) external returns (uint256 txId) { + require(CHAINS[_destChainId].endpoint != address(0), "Destination chain not configured"); + require(_amount > 0, "Amount must be greater than 0"); + require(balanceOf(msg.sender) >= _amount, "Insufficient balance"); + + // Burn tokens on source chain + _burn(msg.sender, _amount); + + // Encode the message data (recipient and amount) + bytes memory _data = abi.encode(_recipient, _amount); + + // Send cross-chain message via ViaLabs - this calls the ViaLabs validator network + txId = _sendMessage(_destChainId, _data); + + emit BridgeInitiated(txId, _destChainId, _recipient, _amount); + } + + /** + * @dev Process incoming bridge message from ViaLabs + * This is called by the ViaLabs message relay when a cross-chain message arrives + */ + function _processMessage( + uint256 _txId, + uint256 _sourceChainId, + bytes calldata _data + ) internal virtual override { + // Decode the message data + (address _recipient, uint256 _amount) = abi.decode(_data, (address, uint256)); + + // Mint tokens to recipient on destination chain + _mint(_recipient, _amount); + + emit BridgeReceived(_txId, _sourceChainId, _recipient, _amount); + } + + /** + * @dev Mint additional tokens (for testing only) + */ + function mint(address _to, uint256 _amount) external onlyMessageOwner { + _mint(_to, _amount); + } +} diff --git a/vialabs-testnet/hardhat.config.ts b/vialabs-testnet/hardhat.config.ts new file mode 100644 index 0000000..3c10611 --- /dev/null +++ b/vialabs-testnet/hardhat.config.ts @@ -0,0 +1,39 @@ +import { HardhatUserConfig } from "hardhat/config"; +import "@nomicfoundation/hardhat-toolbox"; +import * as dotenv from "dotenv"; + +dotenv.config(); + +const PRIVATE_KEY = process.env.PRIVATE_KEY || "0x0000000000000000000000000000000000000000000000000000000000000001"; + +const config: HardhatUserConfig = { + solidity: { + version: "0.8.17", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + networks: { + fuji: { + url: "https://api.avax-test.network/ext/bc/C/rpc", + chainId: 43113, + accounts: [PRIVATE_KEY], + }, + baseSepolia: { + url: "https://sepolia.base.org", + chainId: 84532, + accounts: [PRIVATE_KEY], + }, + }, + etherscan: { + apiKey: { + avalancheFujiTestnet: process.env.SNOWTRACE_API_KEY || "", + baseSepolia: process.env.BASESCAN_API_KEY || "", + }, + }, +}; + +export default config; diff --git a/vialabs-testnet/package.json b/vialabs-testnet/package.json new file mode 100644 index 0000000..33e5031 --- /dev/null +++ b/vialabs-testnet/package.json @@ -0,0 +1,23 @@ +{ + "name": "vialabs-testnet", + "version": "1.0.0", + "description": "ViaLabs HelloERC20 testnet deployment", + "scripts": { + "compile": "hardhat compile", + "deploy:fuji": "hardhat run scripts/deploy.ts --network fuji", + "deploy:baseSepolia": "hardhat run scripts/deploy.ts --network baseSepolia", + "configure:fuji": "hardhat run scripts/configure.ts --network fuji", + "configure:baseSepolia": "hardhat run scripts/configure.ts --network baseSepolia", + "test": "hardhat test" + }, + "dependencies": { + "@openzeppelin/contracts": "^4.9.3", + "@vialabs-io/contracts": "github:VIALabs-io/npm-contracts", + "@vialabs-io/npm-registry": "github:VIALabs-io/npm-registry" + }, + "devDependencies": { + "@nomicfoundation/hardhat-toolbox": "^4.0.0", + "dotenv": "^16.3.1", + "hardhat": "^2.19.0" + } +} diff --git a/vialabs-testnet/scripts/configure.ts b/vialabs-testnet/scripts/configure.ts new file mode 100644 index 0000000..f07b67a --- /dev/null +++ b/vialabs-testnet/scripts/configure.ts @@ -0,0 +1,121 @@ +import { ethers } from "hardhat"; + +// ViaLabs chain configs from @vialabs-io/npm-registry +const CHAIN_CONFIGS: Record = { + 43113: { // Avalanche Fuji + name: "avalanche-testnet", + message: "0x8f92F60ffFB05d8c64E755e54A216090D8D6Eaf9", + feeToken: "0x5425890298aed601595a70ab815c96711a31bc65", + weth: "0xD59A1806BAa7f46d1e07A07649784fA682708794", + }, + 84532: { // Base Sepolia + name: "base-testnet", + message: "0xE700Ee5d8B7dEc62987849356821731591c048cF", + feeToken: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + weth: "0x32D9c1DA01F221aa0eab4A0771Aaa8E2344ECd35", + }, +}; + +// Update these addresses after deploying to both chains +const DEPLOYMENTS: Record = { + 43113: "0xc8600dE63d7cbA25967ecf4894be84dB1c9Ee137", // Avalanche Fuji (v2 with MESSAGE_OWNER) + 84532: "0xb9dB93d419bEDc2C20fe39248D560E7CB1aAABD0", // Base Sepolia (v2 with MESSAGE_OWNER) +}; + +async function main() { + const [deployer] = await ethers.getSigners(); + const network = await ethers.provider.getNetwork(); + const currentChainId = Number(network.chainId); + + console.log("=== Configuring cross-chain for HelloERC20 ==="); + console.log("Network:", network.name, "Chain ID:", currentChainId); + console.log("Deployer:", deployer.address); + + const chainConfig = CHAIN_CONFIGS[currentChainId]; + if (!chainConfig) { + console.error(`Chain ${currentChainId} not supported`); + return; + } + + // Get current chain's contract address + const currentAddress = DEPLOYMENTS[currentChainId]; + if (!currentAddress) { + console.error(`❌ No deployment found for chain ${currentChainId}`); + console.log("Please update DEPLOYMENTS in this script with the contract addresses"); + return; + } + + // Connect to the contract + const HelloERC20 = await ethers.getContractFactory("HelloERC20"); + const token = HelloERC20.attach(currentAddress); + + console.log("Connected to HelloERC20 at:", currentAddress); + console.log("Using MessageV3:", chainConfig.message); + + // Collect all other chains for configuration + const otherChainIds: number[] = []; + const otherEndpoints: string[] = []; + const confirmations: number[] = []; + + for (const [chainId, address] of Object.entries(DEPLOYMENTS)) { + const destChainId = Number(chainId); + + if (destChainId === currentChainId) continue; + if (!address) { + console.log(`⚠️ Skipping chain ${destChainId} - no address configured`); + continue; + } + + otherChainIds.push(destChainId); + otherEndpoints.push(address); + confirmations.push(1); // 1 block confirmation for testnets + + console.log(`Adding chain ${destChainId}: ${address}`); + } + + if (otherChainIds.length === 0) { + console.error("No other chains to configure. Deploy to at least 2 chains first."); + return; + } + + console.log("\nConfiguring cross-chain messaging..."); + console.log("MessageV3:", chainConfig.message); + console.log("Other chains:", otherChainIds); + console.log("Other endpoints:", otherEndpoints); + console.log("Confirmations:", confirmations); + + try { + // Call configureClient inherited from MessageClient + const tx = await token.configureClient( + chainConfig.message, // MessageV3 bridge address + otherChainIds, // destination chain IDs + otherEndpoints, // corresponding HelloERC20 addresses + confirmations // required confirmations + ); + + console.log("Transaction hash:", tx.hash); + console.log("Waiting for confirmation..."); + + await tx.wait(); + console.log("✅ Cross-chain configuration complete!"); + + // Verify configuration + console.log("\nVerifying chain configs:"); + for (let i = 0; i < otherChainIds.length; i++) { + const isActive = await token.isChainActive(otherChainIds[i]); + console.log(` Chain ${otherChainIds[i]}: active=${isActive}`); + } + } catch (error) { + console.error("Error configuring cross-chain:", error); + } +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/vialabs-testnet/scripts/deploy.ts b/vialabs-testnet/scripts/deploy.ts new file mode 100644 index 0000000..85675f7 --- /dev/null +++ b/vialabs-testnet/scripts/deploy.ts @@ -0,0 +1,69 @@ +import { ethers } from "hardhat"; + +// ViaLabs chain configs from @vialabs-io/npm-registry +const CHAIN_CONFIGS: Record = { + 43113: { // Avalanche Fuji + name: "avalanche-testnet", + message: "0x8f92F60ffFB05d8c64E755e54A216090D8D6Eaf9", + feeToken: "0x5425890298aed601595a70ab815c96711a31bc65", + weth: "0xD59A1806BAa7f46d1e07A07649784fA682708794", + }, + 84532: { // Base Sepolia + name: "base-testnet", + message: "0xE700Ee5d8B7dEc62987849356821731591c048cF", + feeToken: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + weth: "0x32D9c1DA01F221aa0eab4A0771Aaa8E2344ECd35", + }, +}; + +async function main() { + const [deployer] = await ethers.getSigners(); + const network = await ethers.provider.getNetwork(); + const chainId = Number(network.chainId); + + console.log("=== Deploying HelloERC20 with ViaLabs MessageClient ==="); + console.log("Network:", network.name, "Chain ID:", chainId); + console.log("Deployer:", deployer.address); + console.log("Balance:", ethers.formatEther(await ethers.provider.getBalance(deployer.address)), "native"); + + const chainConfig = CHAIN_CONFIGS[chainId]; + if (!chainConfig) { + console.error(`Chain ${chainId} not supported`); + return; + } + console.log("Chain config:", chainConfig.name); + console.log("MessageV3 address:", chainConfig.message); + + const HelloERC20 = await ethers.getContractFactory("HelloERC20"); + const token = await HelloERC20.deploy(); + + await token.waitForDeployment(); + const tokenAddress = await token.getAddress(); + + console.log("\n✅ HelloERC20 deployed to:", tokenAddress); + + // Log deployment info + const deploymentInfo = { + chainId: chainId, + network: chainConfig.name, + contractAddress: tokenAddress, + messageV3: chainConfig.message, + deployer: deployer.address, + timestamp: new Date().toISOString(), + }; + + console.log("\nDeployment info:", JSON.stringify(deploymentInfo, null, 2)); + console.log("\nNext steps:"); + console.log("1. Deploy to the other chain"); + console.log("2. Run the configure script to set up cross-chain messaging"); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/vialabs-testnet/tsconfig.json b/vialabs-testnet/tsconfig.json new file mode 100644 index 0000000..4e44a26 --- /dev/null +++ b/vialabs-testnet/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + } +} \ No newline at end of file