From d744e389ae64eb52ed8ad899892f1e3cc0a7b6de Mon Sep 17 00:00:00 2001 From: KarenZita01 Date: Tue, 21 Apr 2026 15:04:51 +0100 Subject: [PATCH] feat: add pre-execution simulation for swap, bridge, and LP (#35) --- README.md | 104 ++++++++++++ agent.ts | 341 +++++++++++++++++++++++++++++++++++++- test/simulation-tests.mjs | 52 ++++++ tools/bridge.ts | 2 +- 4 files changed, 497 insertions(+), 2 deletions(-) create mode 100644 test/simulation-tests.mjs diff --git a/README.md b/README.md index 9f41eafc..e7073aa3 100644 --- a/README.md +++ b/README.md @@ -341,12 +341,116 @@ const shareId = await agent.lp.getShareId(); --- +## ๐ŸŽฏ Pre-Execution Simulation + +AgentKit provides comprehensive simulation capabilities to test transactions before execution, allowing users to: + +- **Validate parameters** and catch errors early +- **Estimate gas fees** and resource costs +- **Verify transaction outcomes** without committing funds +- **Test edge cases** and failure scenarios + +### Swap Simulation + +Simulate token swaps before execution: + +```typescript +import { AgentClient } from "stellar-agentkit"; + +const agent = new AgentClient({ + network: "testnet", + publicKey: "YOUR_TESTNET_PUBLIC_KEY" +}); + +// Simulate a swap operation +const swapSimulation = await agent.simulate.swap({ + to: "recipient_address", + buyA: true, + out: "100", + inMax: "110" +}); + +console.log("Swap simulation result:", swapSimulation); +// Returns: { status: "simulated", minResourceFee: 12345, cost: {...}, events: 2, result: {...} } +``` + +### Bridge Simulation + +Simulate cross-chain bridge operations: + +```typescript +// Simulate a bridge operation +const bridgeSimulation = await agent.simulate.bridge({ + amount: "100", + toAddress: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", + targetChain: "ethereum" +}); + +console.log("Bridge simulation result:", bridgeSimulation); +// Returns: { status: "simulated", network: "stellar-testnet", targetChain: "ethereum", +// amount: "100", minResourceFee: 23456, cost: {...}, events: 3, +// result: {...}, transactionXDR: "..." } +``` + +### Liquidity Pool Simulation + +Simulate LP operations: + +```typescript +// Simulate LP deposit +const depositSimulation = await agent.simulate.lp.deposit({ + to: "recipient_address", + desiredA: "1000", + minA: "950", + desiredB: "1000", + minB: "950" +}); + +// Simulate LP withdrawal +const withdrawSimulation = await agent.simulate.lp.withdraw({ + to: "recipient_address", + shareAmount: "100", + minA: "95", + minB: "95" +}); + +console.log("LP simulation results:", { depositSimulation, withdrawSimulation }); +``` + +### Simulation Result Format + +All simulation methods return structured results: + +```typescript +interface SimulationResult { + status: "simulated" | "failed"; // Success/failure status + error?: string; // Error details if failed + minResourceFee?: number; // Estimated resource fee + cost?: any; // Detailed cost breakdown + events?: number; // Number of events emitted + result?: any; // Expected return value +} +``` + +### Benefits of Simulation + +โœ… **Risk Mitigation**: Test parameters before committing real funds +โœ… **Cost Estimation**: Understand gas fees and resource costs upfront +โœ… **Error Detection**: Catch invalid parameters early +โœ… **Development Efficiency**: Faster iteration without waiting for transactions +โœ… **Mainnet Safety**: Reduce risk of costly mistakes on live networks + +--- + ## ๐Ÿงช Testing ```bash # Run test suite node test/bridge-tests.mjs +# Run simulation tests +node test/simulation-tests.mjs + # View test results # โœ… 20/20 tests passed # โœ… 100% success rate diff --git a/agent.ts b/agent.ts index 36d4d8c3..9e2cb6d8 100644 --- a/agent.ts +++ b/agent.ts @@ -14,7 +14,7 @@ import { type SwapBestRouteParams, type SwapBestRouteResult, } from "./lib/dex"; -import { bridgeTokenTool } from "./tools/bridge"; +import { bridgeTokenTool, TARGET_CHAIN_MAP } from "./tools/bridge"; import { Horizon, Keypair, @@ -60,6 +60,48 @@ export interface LaunchTokenResult { issuerLocked: boolean; } +// Simulation result types +export interface SimulationResult { + status: "simulated" | "failed"; + error?: string; + minResourceFee?: number; + cost?: any; + events?: number; + result?: any; +} + +export interface SwapSimulationResult extends SimulationResult { + to: string; + buyA: boolean; + out: string; + inMax: string; +} + +export interface BridgeSimulationResult extends SimulationResult { + network: "stellar-testnet" | "stellar-mainnet"; + targetChain: "ethereum" | "polygon" | "arbitrum" | "base"; + amount: string; + toAddress: string; + transactionXDR?: string; +} + +export interface LpSimulationResult extends SimulationResult { + to: string; +} + +export interface LpDepositSimulationResult extends LpSimulationResult { + desiredA: string; + minA: string; + desiredB: string; + minB: string; +} + +export interface LpWithdrawSimulationResult extends LpSimulationResult { + shareAmount: string; + minA: string; + minB: string; +} + export type { StellarAssetInput, QuoteSwapParams, @@ -238,6 +280,303 @@ export class AgentClient { }, }; + /** + * Pre-execution simulation interface. + * + * Allows users to simulate transactions before execution to: + * - Validate parameters and catch errors early + * - Estimate gas fees and resource costs + * - Verify transaction outcomes without committing funds + * - Test edge cases and failure scenarios + * + * All simulation methods return detailed results including: + * - Success/failure status + * - Estimated costs (fees, resources) + * - Expected outcomes (amounts, balances) + * - Error details if applicable + */ + public simulate = { + /** + * Simulate a swap operation before execution. + * + * @param params Swap parameters identical to agent.swap() + * @returns Simulation results with costs, outcomes, and validation + */ + swap: async (params: { + to: string; + buyA: boolean; + out: string; + inMax: string; + contractAddress?: string; + }): Promise => { + const result = await contractSwap( + this.publicKey, + params.to, + params.buyA, + params.out, + params.inMax, + { network: this.network, rpcUrl: this.rpcUrl, contractAddress: params.contractAddress, simulate: true } + ); + + // Parse the JSON result from contract simulation + const simulationData = JSON.parse(result as string); + + return { + status: simulationData.status, + error: simulationData.error, + minResourceFee: simulationData.minResourceFee, + cost: simulationData.cost, + events: simulationData.events, + result: simulationData.result, + to: params.to, + buyA: params.buyA, + out: params.out, + inMax: params.inMax, + }; + }, + + /** + * Simulate a bridge operation before execution. + * + * @param params Bridge parameters identical to agent.bridge() + * @returns Simulation results with costs, outcomes, and validation + */ + bridge: async (params: { + amount: string; + toAddress: string; + targetChain?: "ethereum" | "polygon" | "arbitrum" | "base"; + }): Promise => { + // For bridge simulation, we need to create a special simulation mode + // that doesn't actually submit transactions but returns the expected outcome + return await this.simulateBridgeOperation(params); + }, + + /** + * Simulate LP operations before execution. + */ + lp: { + /** + * Simulate a liquidity deposit operation. + * + * @param params Deposit parameters identical to agent.lp.deposit() + * @returns Simulation results with costs, outcomes, and validation + */ + deposit: async (params: { + to: string; + desiredA: string; + minA: string; + desiredB: string; + minB: string; + contractAddress?: string; + }): Promise => { + const result = await contractDeposit( + this.publicKey, + params.to, + params.desiredA, + params.minA, + params.desiredB, + params.minB, + { network: this.network, rpcUrl: this.rpcUrl, contractAddress: params.contractAddress, simulate: true } + ); + + // Parse the JSON result from contract simulation + const simulationData = JSON.parse(result as string); + + return { + status: simulationData.status, + error: simulationData.error, + minResourceFee: simulationData.minResourceFee, + cost: simulationData.cost, + events: simulationData.events, + result: simulationData.result, + to: params.to, + desiredA: params.desiredA, + minA: params.minA, + desiredB: params.desiredB, + minB: params.minB, + }; + }, + + /** + * Simulate a liquidity withdrawal operation. + * + * @param params Withdraw parameters identical to agent.lp.withdraw() + * @returns Simulation results with costs, outcomes, and validation + */ + withdraw: async (params: { + to: string; + shareAmount: string; + minA: string; + minB: string; + contractAddress?: string; + }): Promise => { + const result = await contractWithdraw( + this.publicKey, + params.to, + params.shareAmount, + params.minA, + params.minB, + { network: this.network, rpcUrl: this.rpcUrl, contractAddress: params.contractAddress, simulate: true } + ); + + // Parse JSON result from contract simulation + let simulationData; + try { + // Check if result is a string (JSON) or other type + if (typeof result === 'string') { + simulationData = JSON.parse(result); + } else { + // Handle case where result is not JSON (e.g., for withdraw returning tuple) + simulationData = { + status: "simulated", + result: result + }; + } + } catch (e) { + // Fallback for any parsing errors + simulationData = { + status: "simulated", + result: result + }; + } + + return { + status: simulationData.status || "simulated", + error: simulationData.error, + minResourceFee: simulationData.minResourceFee, + cost: simulationData.cost, + events: simulationData.events, + result: simulationData.result, + to: params.to, + shareAmount: params.shareAmount, + minA: params.minA, + minB: params.minB, + }; + }, + }, + }; + + /** + * Internal method to simulate bridge operations. + * Bridge operations use external SDK (AllbridgeCoreSdk) which doesn't have built-in simulation, + * so we create a simulation by building the transaction and analyzing it without submitting. + */ + private async simulateBridgeOperation(params: { + amount: string; + toAddress: string; + targetChain?: "ethereum" | "polygon" | "arbitrum" | "base"; + }): Promise { + try { + const targetChain = params.targetChain ?? "ethereum"; + const fromNetwork = this.network === "mainnet" ? "stellar-mainnet" : "stellar-testnet"; + + // Import the bridge SDK for simulation + const { AllbridgeCoreSdk, ChainSymbol, FeePaymentMethod, Messenger, nodeRpcUrlsDefault } = await import("@allbridge/bridge-core-sdk"); + const { ensure } = await import("../utils/utils"); + + const sdk = new AllbridgeCoreSdk({ + ...nodeRpcUrlsDefault, + SRB: process.env.SRB_PROVIDER_URL || "https://soroban-testnet.stellar.org", + }); + + const chainDetailsMap = await sdk.chainDetailsMap(); + const destinationChainSymbol = TARGET_CHAIN_MAP[targetChain as keyof typeof TARGET_CHAIN_MAP]; + + const sourceToken = ensure( + chainDetailsMap[ChainSymbol.SRB].tokens.find((t) => t.symbol === "USDC") + ); + + const destinationChainDetails = chainDetailsMap[destinationChainSymbol]; + if (!destinationChainDetails) { + throw new Error(`Chain not supported by Allbridge: ${targetChain}`); + } + const destinationToken = ensure( + destinationChainDetails.tokens.find((t) => t.symbol === "USDC") + ); + + // Build transaction parameters (same as actual bridge) + const sendParams = { + amount: params.amount, + fromAccountAddress: this.publicKey, + toAccountAddress: params.toAddress, + sourceToken, + destinationToken, + messenger: Messenger.ALLBRIDGE, + extraGas: "1.15", + extraGasFormat: "FLOAT" as const, + gasFeePaymentMethod: FeePaymentMethod.WITH_STABLECOIN, + }; + + // Get the unsigned transaction XDR without signing or submitting + const xdrTx = (await sdk.bridge.rawTxBuilder.send(sendParams)) as string; + + // Simulate the transaction using Stellar RPC + const { rpc: StellarRpc, Networks } = await import("@stellar/stellar-sdk"); + const server = new StellarRpc.Server( + this.network === "mainnet" + ? "https://mainnet.stellar.validationcloudapi.com/v1/default" + : "https://soroban-testnet.stellar.org" + ); + + const networkPassphrase = this.network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET; + const { buildTransactionFromXDR } = await import("../utils/buildTransaction"); + + const transaction = buildTransactionFromXDR( + "bridge", + xdrTx, + networkPassphrase + ); + + // Simulate the transaction + const simulation = await server.simulateTransaction(transaction); + + if (simulation.error) { + return { + status: "failed", + error: simulation.error, + network: fromNetwork, + targetChain, + amount: params.amount, + toAddress: params.toAddress, + }; + } + + // Parse simulation results + let returnValue = null; + if (simulation.result?.retval) { + try { + const { scValToNative } = await import("@stellar/stellar-sdk"); + returnValue = scValToNative(simulation.result.retval); + } catch (e) { + returnValue = "Failed to parse return value"; + } + } + + return { + status: "simulated", + network: fromNetwork, + targetChain, + amount: params.amount, + toAddress: params.toAddress, + minResourceFee: simulation.minResourceFee, + cost: simulation.cost, + events: simulation.events?.length || 0, + result: returnValue, + transactionXDR: xdrTx, // Include XDR for debugging + }; + + } catch (error: any) { + return { + status: "failed", + error: error.message, + network: this.network === "mainnet" ? "stellar-mainnet" : "stellar-testnet", + targetChain: params.targetChain ?? "ethereum", + amount: params.amount, + toAddress: params.toAddress, + }; + } + } + /** * Launch a new token on the Stellar network. * diff --git a/test/simulation-tests.mjs b/test/simulation-tests.mjs new file mode 100644 index 00000000..53396007 --- /dev/null +++ b/test/simulation-tests.mjs @@ -0,0 +1,52 @@ +import { AgentClient } from "../index.js"; + +// Test simulation functionality +async function testSimulation() { + console.log("๐Ÿงช Testing Stellar AgentKit Simulation Features\n"); + + const agent = new AgentClient({ + network: "testnet", + publicKey: process.env.STELLAR_PUBLIC_KEY || "GD5DJJBRB6A5QMFOGGGFOZPWKX2MLTWJKHPZJP6V6M7J5N54XTJYH" + }); + + try { + // Test swap simulation + console.log("1๏ธโƒฃ Testing swap simulation..."); + const swapSim = await agent.simulate.swap({ + to: "GD5DJJBRB6A5QMFOGGGFOZPWKX2MLTWJKHPZJP6V6M7J5N54XTJYH", + buyA: true, + out: "100", + inMax: "110" + }); + console.log("Swap simulation result:", JSON.stringify(swapSim, null, 2)); + + // Test LP deposit simulation + console.log("\n2๏ธโƒฃ Testing LP deposit simulation..."); + const depositSim = await agent.simulate.lp.deposit({ + to: "GD5DJJBRB6A5QMFOGGGFOZPWKX2MLTWJKHPZJP6V6M7J5N54XTJYH", + desiredA: "1000", + minA: "950", + desiredB: "1000", + minB: "950" + }); + console.log("LP deposit simulation result:", JSON.stringify(depositSim, null, 2)); + + // Test bridge simulation + console.log("\n3๏ธโƒฃ Testing bridge simulation..."); + const bridgeSim = await agent.simulate.bridge({ + amount: "10", + toAddress: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb", + targetChain: "ethereum" + }); + console.log("Bridge simulation result:", JSON.stringify(bridgeSim, null, 2)); + + console.log("\nโœ… All simulation tests completed successfully!"); + + } catch (error) { + console.error("โŒ Simulation test failed:", error.message); + console.error("Stack:", error.stack); + } +} + +// Run tests +testSimulation().catch(console.error); diff --git a/tools/bridge.ts b/tools/bridge.ts index e72730d9..8d31c1f1 100644 --- a/tools/bridge.ts +++ b/tools/bridge.ts @@ -32,7 +32,7 @@ type StellarNetwork = "stellar-testnet" | "stellar-mainnet"; */ export type TargetChain = "ethereum" | "polygon" | "arbitrum" | "base"; -const TARGET_CHAIN_MAP: Record = { +export const TARGET_CHAIN_MAP: Record = { ethereum: ChainSymbol.ETH, polygon: ChainSymbol.POL, arbitrum: ChainSymbol.ARB,