diff --git a/agent.ts b/agent.ts index 43c19eca..cc2d945e 100644 --- a/agent.ts +++ b/agent.ts @@ -5,7 +5,7 @@ import { getReserves as contractGetReserves, getShareId as contractGetShareId, } from "./lib/contract"; -import { bridgeTokenTool } from "./tools/bridge"; +import { bridgeTokenTool, TargetChain } from "./tools/bridge"; import { Horizon, Keypair, @@ -16,6 +16,8 @@ import { BASE_FEE } from "@stellar/stellar-sdk"; +const { Server } = Horizon; + export interface AgentConfig { network: "testnet" | "mainnet"; rpcUrl?: string; @@ -106,23 +108,29 @@ export class AgentClient { } /** - * Bridge tokens from Stellar to EVM compatible chains. - * + * Bridge USDC from Stellar to an EVM-compatible chain. + * * ⚠️ IMPORTANT: Mainnet bridging requires BOTH: * 1. AgentClient initialized with allowMainnet: true * 2. ALLOW_MAINNET_BRIDGE=true in your .env file - * - * This dual-safeguard approach prevents accidental mainnet bridging. - * + * + * Supported target chains: "ethereum" | "polygon" | "arbitrum" | "base" + * + * @example + * await agent.bridge({ amount: "100", toAddress: "0x...", targetChain: "polygon" }); + * * @param params Bridge parameters - * @returns Bridge transaction result with status, hash, and network + * @returns Bridge transaction result with status, hash, network, and targetChain */ async bridge(params: { amount: string; toAddress: string; + targetChain?: TargetChain; }) { return await bridgeTokenTool.func({ - ...params, + amount: params.amount, + toAddress: params.toAddress, + targetChain: params.targetChain ?? "ethereum", fromNetwork: this.network === "mainnet" ? "stellar-mainnet" diff --git a/tests/unit/tools/bridge.test.ts b/tests/unit/tools/bridge.test.ts index 09ee94a8..daac399c 100644 --- a/tests/unit/tools/bridge.test.ts +++ b/tests/unit/tools/bridge.test.ts @@ -1,46 +1,71 @@ -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { Networks } from '@stellar/stellar-sdk'; +import { ChainSymbol } from '@allbridge/bridge-core-sdk'; type StellarNetwork = "stellar-testnet" | "stellar-mainnet"; +type TargetChain = "ethereum" | "polygon" | "arbitrum" | "base"; -describe('Bridge Tool - Network Configuration', () => { - const STELLAR_NETWORK_CONFIG: Record = { - "stellar-testnet": { - networkPassphrase: Networks.TESTNET, - }, - "stellar-mainnet": { - networkPassphrase: Networks.PUBLIC, - }, - }; +const TARGET_CHAIN_MAP: Record = { + ethereum: ChainSymbol.ETH, + polygon: ChainSymbol.POL, + arbitrum: ChainSymbol.ARB, + base: ChainSymbol.BAS, +}; + +const STELLAR_NETWORK_CONFIG: Record = { + "stellar-testnet": { networkPassphrase: Networks.TESTNET }, + "stellar-mainnet": { networkPassphrase: Networks.PUBLIC }, +}; + +describe('Bridge Tool - Multi-Chain Support', () => { + + describe('Target Chain Mapping', () => { + it('should map ethereum to ChainSymbol.ETH', () => { + expect(TARGET_CHAIN_MAP["ethereum"]).toBe(ChainSymbol.ETH); + }); + + it('should map polygon to ChainSymbol.POL', () => { + expect(TARGET_CHAIN_MAP["polygon"]).toBe(ChainSymbol.POL); + }); + + it('should map arbitrum to ChainSymbol.ARB', () => { + expect(TARGET_CHAIN_MAP["arbitrum"]).toBe(ChainSymbol.ARB); + }); + + it('should map base to ChainSymbol.BAS', () => { + expect(TARGET_CHAIN_MAP["base"]).toBe(ChainSymbol.BAS); + }); + + it('should support all four target chains', () => { + const chains: TargetChain[] = ["ethereum", "polygon", "arbitrum", "base"]; + chains.forEach((chain) => { + expect(TARGET_CHAIN_MAP[chain]).toBeDefined(); + }); + }); + }); describe('Type Safety', () => { - it('should have correct network configuration type', () => { - const testNetwork: StellarNetwork = "stellar-testnet"; - expect(testNetwork).toBe("stellar-testnet"); + it('should accept valid TargetChain values', () => { + const validChains: TargetChain[] = ["ethereum", "polygon", "arbitrum", "base"]; + expect(validChains).toHaveLength(4); + expect(validChains).toContain("polygon"); + expect(validChains).toContain("arbitrum"); + expect(validChains).toContain("base"); }); - it('should validate network options', () => { - const validNetworks: StellarNetwork[] = ["stellar-testnet", "stellar-mainnet"]; - expect(validNetworks).toContain("stellar-testnet"); - expect(validNetworks).toContain("stellar-mainnet"); - expect(validNetworks).toHaveLength(2); + it('should default to ethereum when targetChain is not specified', () => { + const defaultChain: TargetChain = "ethereum"; + expect(TARGET_CHAIN_MAP[defaultChain]).toBe(ChainSymbol.ETH); }); }); - describe('Network Passphrases', () => { + describe('Network Configuration', () => { it('should have correct testnet passphrase', () => { expect(STELLAR_NETWORK_CONFIG["stellar-testnet"].networkPassphrase).toBe(Networks.TESTNET); - expect(STELLAR_NETWORK_CONFIG["stellar-testnet"].networkPassphrase).toBe("Test SDF Network ; September 2015"); }); it('should have correct mainnet passphrase', () => { expect(STELLAR_NETWORK_CONFIG["stellar-mainnet"].networkPassphrase).toBe(Networks.PUBLIC); - expect(STELLAR_NETWORK_CONFIG["stellar-mainnet"].networkPassphrase).toBe("Public Global Stellar Network ; September 2015"); - }); - - it('should have passphrase for both networks', () => { - expect(STELLAR_NETWORK_CONFIG["stellar-testnet"]).toBeDefined(); - expect(STELLAR_NETWORK_CONFIG["stellar-mainnet"]).toBeDefined(); }); }); @@ -48,7 +73,6 @@ describe('Bridge Tool - Network Configuration', () => { it('should block mainnet when ALLOW_MAINNET_BRIDGE is not set', () => { const fromNetwork: StellarNetwork = "stellar-mainnet"; const allowMainnetBridge = undefined; - const shouldBlock = fromNetwork === "stellar-mainnet" && allowMainnetBridge !== "true"; expect(shouldBlock).toBe(true); }); @@ -64,7 +88,6 @@ describe('Bridge Tool - Network Configuration', () => { it('should allow mainnet when ALLOW_MAINNET_BRIDGE is true', () => { const fromNetwork: StellarNetwork = "stellar-mainnet"; const allowMainnetBridge = "true"; - const shouldBlock = fromNetwork === "stellar-mainnet" && allowMainnetBridge !== "true"; expect(shouldBlock).toBe(false); }); @@ -76,5 +99,24 @@ describe('Bridge Tool - Network Configuration', () => { const shouldBlock = fromNetwork === "stellar-mainnet" && allowMainnetBridge !== "true"; expect(shouldBlock).toBe(false); }); + + it('should apply mainnet safeguard for all target chains', () => { + const chains: TargetChain[] = ["ethereum", "polygon", "arbitrum", "base"]; + chains.forEach((chain) => { + const fromNetwork: StellarNetwork = "stellar-mainnet"; + const allowMainnetBridge: string | undefined = undefined; + const shouldBlock = fromNetwork === "stellar-mainnet" && allowMainnetBridge !== "true"; + expect(shouldBlock).toBe(true); + }); + }); + }); + + describe('Chain Symbol Values', () => { + it('should have correct string values for chain symbols', () => { + expect(ChainSymbol.ETH).toBe("ETH"); + expect(ChainSymbol.POL).toBe("POL"); + expect(ChainSymbol.ARB).toBe("ARB"); + expect(ChainSymbol.BAS).toBe("BAS"); + }); }); }); diff --git a/tools/bridge.ts b/tools/bridge.ts index 252d6b30..e72730d9 100644 --- a/tools/bridge.ts +++ b/tools/bridge.ts @@ -11,8 +11,6 @@ import { Keypair, Keypair as StellarKeypair, rpc, - TransactionBuilder as StellarTransactionBuilder, - TransactionBuilder, Networks } from "@stellar/stellar-sdk"; import { ensure } from "../utils/utils"; @@ -25,8 +23,22 @@ dotenv.config({ path: ".env" }); const fromAddress = process.env.STELLAR_PUBLIC_KEY as string; const privateKey = process.env.STELLAR_PRIVATE_KEY as string; + type StellarNetwork = "stellar-testnet" | "stellar-mainnet"; +/** + * Supported target EVM chains for bridging from Stellar. + * Maps user-facing chain names to Allbridge ChainSymbol values. + */ +export type TargetChain = "ethereum" | "polygon" | "arbitrum" | "base"; + +const TARGET_CHAIN_MAP: Record = { + ethereum: ChainSymbol.ETH, + polygon: ChainSymbol.POL, + arbitrum: ChainSymbol.ARB, + base: ChainSymbol.BAS, +}; + const STELLAR_NETWORK_CONFIG: Record = { "stellar-testnet": { networkPassphrase: Networks.TESTNET, @@ -39,25 +51,32 @@ const STELLAR_NETWORK_CONFIG: Record { // Mainnet safeguard - additional layer beyond AgentClient if ( @@ -69,6 +88,8 @@ export const bridgeTokenTool = new DynamicStructuredTool({ ); } + const destinationChainSymbol = TARGET_CHAIN_MAP[targetChain]; + const sdk = new AllbridgeCoreSdk({ ...nodeRpcUrlsDefault, SRB: `${process.env.SRB_PROVIDER_URL}`, @@ -81,10 +102,13 @@ export const bridgeTokenTool = new DynamicStructuredTool({ (t) => t.symbol === "USDC" ) ); + + const destinationChainDetails = chainDetailsMap[destinationChainSymbol]; + if (!destinationChainDetails) { + throw new Error(`Chain not supported by Allbridge: ${targetChain}`); + } const destinationToken = ensure( - chainDetailsMap[ChainSymbol.ETH].tokens.find( - (t) => t.symbol === "USDC" - ) + destinationChainDetails.tokens.find((t) => t.symbol === "USDC") ); const sendParams = { @@ -99,11 +123,8 @@ export const bridgeTokenTool = new DynamicStructuredTool({ gasFeePaymentMethod: FeePaymentMethod.WITH_STABLECOIN, }; - const xdrTx = (await sdk.bridge.rawTxBuilder.send( - sendParams - )) as string; + const xdrTx = (await sdk.bridge.rawTxBuilder.send(sendParams)) as string; - // Use unified transaction builder for XDR-based bridge operations const srbKeypair = Keypair.fromSecret(privateKey); const transaction = buildTransactionFromXDR( "bridge", @@ -135,10 +156,7 @@ export const bridgeTokenTool = new DynamicStructuredTool({ sentRestoreXdrTx.hash ); - // Handle FAILED restore explicitly - if ( - confirmRestoreXdrTx.status === rpc.Api.GetTransactionStatus.FAILED - ) { + if (confirmRestoreXdrTx.status === rpc.Api.GetTransactionStatus.FAILED) { throw new Error( `Restore transaction failed. Hash: ${sentRestoreXdrTx.hash}` ); @@ -151,14 +169,12 @@ export const bridgeTokenTool = new DynamicStructuredTool({ status: "pending_restore", hash: sentRestoreXdrTx.hash, network: fromNetwork, + targetChain, }; } - // Get new tx with updated sequences - const xdrTx2 = (await sdk.bridge.rawTxBuilder.send( - sendParams - )) as string; - + // Rebuild tx with updated sequence numbers after restore + const xdrTx2 = (await sdk.bridge.rawTxBuilder.send(sendParams)) as string; const transaction2 = buildTransactionFromXDR( "bridge", xdrTx2, @@ -176,6 +192,7 @@ export const bridgeTokenTool = new DynamicStructuredTool({ status: "pending", hash: sent.hash, network: fromNetwork, + targetChain, }; } @@ -183,7 +200,7 @@ export const bridgeTokenTool = new DynamicStructuredTool({ throw new Error(`Transaction failed. Hash: ${sent.hash}`); } - // TrustLine check and setup for destinationToken if it is SRB + // TrustLine check and setup for source token on Stellar side const destinationTokenSBR = sourceToken; const balanceLine = await sdk.utils.srb.getBalanceLine( @@ -193,18 +210,14 @@ export const bridgeTokenTool = new DynamicStructuredTool({ const notEnoughBalanceLine = !balanceLine || - Big(balanceLine.balance) - .add(amount) - .gt(Big(balanceLine.limit)); + Big(balanceLine.balance).add(amount).gt(Big(balanceLine.limit)); if (notEnoughBalanceLine) { - const xdrTx = - await sdk.utils.srb.buildChangeTrustLineXdrTx({ - sender: fromAddress, - tokenAddress: destinationTokenSBR.tokenAddress, - }); + const xdrTx = await sdk.utils.srb.buildChangeTrustLineXdrTx({ + sender: fromAddress, + tokenAddress: destinationTokenSBR.tokenAddress, + }); - // Use unified transaction builder for XDR-based bridge TrustLine operation const keypair = StellarKeypair.fromSecret(privateKey); const trustTx = buildTransactionFromXDR( "bridge", @@ -222,6 +235,7 @@ export const bridgeTokenTool = new DynamicStructuredTool({ status: "trustline_submitted", hash: submit.hash, network: fromNetwork, + targetChain, }; } @@ -229,8 +243,9 @@ export const bridgeTokenTool = new DynamicStructuredTool({ status: "confirmed", hash: sent.hash, network: fromNetwork, + targetChain, asset: sourceToken.symbol, amount, }; }, -}); \ No newline at end of file +});