diff --git a/agent.ts b/agent.ts index 82ec5daa..36d4d8c3 100644 --- a/agent.ts +++ b/agent.ts @@ -114,13 +114,15 @@ export class AgentClient { buyA: boolean; out: string; inMax: string; + contractAddress?: string; }) { return await contractSwap( this.publicKey, params.to, params.buyA, params.out, - params.inMax + params.inMax, + { network: this.network, rpcUrl: this.rpcUrl, contractAddress: params.contractAddress } ); } @@ -142,7 +144,7 @@ export class AgentClient { async bridge(params: { amount: string; toAddress: string; - targetChain?: TargetChain; + targetChain?: "ethereum" | "polygon" | "arbitrum" | "base"; }) { return await bridgeTokenTool.func({ amount: params.amount, @@ -165,6 +167,7 @@ export class AgentClient { minA: string; desiredB: string; minB: string; + contractAddress?: string; }) => { return await contractDeposit( this.publicKey, @@ -172,7 +175,8 @@ export class AgentClient { params.desiredA, params.minA, params.desiredB, - params.minB + params.minB, + { network: this.network, rpcUrl: this.rpcUrl, contractAddress: params.contractAddress } ); }, @@ -181,22 +185,24 @@ export class AgentClient { shareAmount: string; minA: string; minB: string; + contractAddress?: string; }) => { return await contractWithdraw( this.publicKey, params.to, params.shareAmount, params.minA, - params.minB + params.minB, + { network: this.network, rpcUrl: this.rpcUrl, contractAddress: params.contractAddress } ); }, - getReserves: async () => { - return await contractGetReserves(this.publicKey); + getReserves: async (params?: { contractAddress?: string }) => { + return await contractGetReserves(this.publicKey, { network: this.network, rpcUrl: this.rpcUrl, contractAddress: params?.contractAddress }); }, - getShareId: async () => { - return await contractGetShareId(this.publicKey); + getShareId: async (params?: { contractAddress?: string }) => { + return await contractGetShareId(this.publicKey, { network: this.network, rpcUrl: this.rpcUrl, contractAddress: params?.contractAddress }); }, }; diff --git a/lib/contract.ts b/lib/contract.ts index e9fd7cc6..200884d5 100644 --- a/lib/contract.ts +++ b/lib/contract.ts @@ -12,10 +12,14 @@ import { import { signTransaction } from "./stellar"; import { buildTransaction } from "../utils/buildTransaction"; - // Configuration - const rpcUrl = "https://soroban-testnet.stellar.org"; - const contractAddress = "CCUMBJFVC3YJOW3OOR6WTWTESH473ZSXQEGYPQDWXAYYC4J77OT4NVHJ"; // From networks.testnet.contractId - const networkPassphrase = Networks.TESTNET; + export interface SorobanContractConfig { + network: "testnet" | "mainnet"; + rpcUrl: string; + contractAddress?: string; // If omitted, defaults to testnet pool below + simulate?: boolean; // Dry-run simulation mode + } + + const DEFAULT_TESTNET_CONTRACT = "CCUMBJFVC3YJOW3OOR6WTWTESH473ZSXQEGYPQDWXAYYC4J77OT4NVHJ"; // Utility functions for ScVal conversion const addressToScVal = (address: string) => { @@ -35,14 +39,24 @@ import { }; // Core contract interaction function - const contractInt = async (caller: string, functName: string, values: any) => { + const contractInt = async ( + caller: string, + functName: string, + values: any, + config: SorobanContractConfig = { network: "testnet", rpcUrl: "https://soroban-testnet.stellar.org" } + ) => { try { - const server = new rpc.Server(rpcUrl, { allowHttp: true }); + const server = new rpc.Server(config.rpcUrl, { allowHttp: true }); const sourceAccount = await server.getAccount(caller).catch((err) => { throw new Error(`Failed to fetch account ${caller}: ${err.message}`); }); - const contract = new Contract(contractAddress); + const targetContractId = config.contractAddress || (config.network === "testnet" ? DEFAULT_TESTNET_CONTRACT : ""); + if (!targetContractId) { + throw new Error("A specific contractAddress must be provided for Soroban LP/Swap operations on mainnet."); + } + + const contract = new Contract(targetContractId); // Build transaction using unified builder const sorobanOperation = { @@ -52,46 +66,41 @@ import { }; const transaction = buildTransaction("lp", sourceAccount, sorobanOperation); - const simulation = await server.simulateTransaction(transaction).catch((err) => { - console.error(`Simulation failed for ${functName}: ${err.message}`); - throw new Error(`Failed to simulate transaction: ${err.message}`); - }); - - console.log(`Simulation response for ${functName}:`, JSON.stringify(simulation, null, 2)); - - if ("results" in simulation && Array.isArray(simulation.results) && simulation.results.length > 0) { - console.log(`Read-only call detected for ${functName}`); - const result = simulation.results[0]; - if (result.xdr) { - try { - // Parse the return value from XDR - const scVal = xdr.ScVal.fromXDR(result.xdr, "base64"); - const parsedValue = scValToNative(scVal); - console.log(`Parsed simulation result for ${functName}:`, parsedValue); - return parsedValue; // Returns string for share_id, array for get_rsrvs - } catch (err) { - console.error(`Failed to parse XDR for ${functName}:`, err); - throw new Error(`Failed to parse simulation result: ${err instanceof Error ? err.message : String(err)}`); - } + // Simulate transaction if requested + if (config.simulate) { + const simulation = await server.simulateTransaction(transaction) as any; + if (simulation.error) { + throw new Error(`Simulation Failed: ${simulation.error}`); } - console.error(`No xdr field in simulation results[0] for ${functName}:`, result); - throw new Error("No return value in simulation results"); - } else if ("error" in simulation) { - console.error(`Simulation error for ${functName}:`, simulation.error); - throw new Error(`Simulation failed: ${simulation.error}`); + + let returnValue = null; + if (simulation.result?.retval) { + try { + returnValue = scValToNative(simulation.result.retval); + } catch(e) { + returnValue = "Failed to parse return value"; + } + } + + return JSON.stringify({ + status: "simulated", + minResourceFee: simulation.minResourceFee, + cost: simulation.cost, + events: simulation.events?.length || 0, + result: returnValue + }, (key, value) => typeof value === 'bigint' ? value.toString() : value, 2); } - - // For state-changing calls, prepare and submit transaction - console.log(`Submitting transaction for ${functName}`); + + // Prepare and sign transaction const preparedTx = await server.prepareTransaction(transaction).catch((err) => { - console.error(`Prepare transaction failed for ${functName}: ${err.message}`); throw new Error(`Failed to prepare transaction: ${err.message}`); }); const prepareTxXDR = preparedTx.toXDR(); - + let signedTxResponse: string; try { - signedTxResponse = signTransaction(prepareTxXDR, networkPassphrase); + const passphrase = config.network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET; + signedTxResponse = signTransaction(prepareTxXDR, passphrase); } catch (err: any) { throw new Error(`Failed to sign transaction: ${err.message}`); } @@ -99,7 +108,8 @@ import { // Handle both string and object response from signTransaction const signedXDR = signedTxResponse - const tx = TransactionBuilder.fromXDR(signedXDR, Networks.TESTNET); + const passphrase = config.network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET; + const tx = TransactionBuilder.fromXDR(signedXDR, passphrase); const txResult = await server.sendTransaction(tx).catch((err) => { console.error(`Send transaction failed for ${functName}: ${err.message}`); throw new Error(`Send transaction failed: ${err.message}`); @@ -146,9 +156,10 @@ import { }; // Contract interaction functions - export async function getShareId(caller: string): Promise { + export async function getShareId(caller: string, config?: SorobanContractConfig): Promise { try { - const result = await contractInt(caller, "share_id", null); + const result = await contractInt(caller, "share_id", null, config); + if (config?.simulate) return result as any; // Forward the simulation text console.log("Share ID:", result); return result as string | null; } catch (error: unknown) { @@ -164,7 +175,8 @@ import { desiredA: string, minA: string, desiredB: string, - minB: string + minB: string, + config?: SorobanContractConfig ) { try { const toScVal = addressToScVal(to); @@ -172,13 +184,14 @@ import { const minAScVal = numberToI128(minA); const desiredBScVal = numberToI128(desiredB); const minBScVal = numberToI128(minB); - await contractInt(caller, "deposit", [ + const result = await contractInt(caller, "deposit", [ toScVal, desiredAScVal, minAScVal, desiredBScVal, minBScVal, - ]); + ], config); + if (config?.simulate) return result as any; // Forward the simulation text console.log(`Deposited successfully to ${to}`); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -192,14 +205,16 @@ import { to: string, buyA: boolean, out: string, - inMax: string + inMax: string, + config?: SorobanContractConfig ) { try { const toScVal = addressToScVal(to); const buyAScVal = booleanToScVal(buyA); const outScVal = numberToI128(out); const inMaxScVal = numberToI128(inMax); - await contractInt(caller, "swap", [toScVal, buyAScVal, outScVal, inMaxScVal]); + const result = await contractInt(caller, "swap", [toScVal, buyAScVal, outScVal, inMaxScVal], config); + if (config?.simulate) return result as any; // Forward the simulation text console.log(`Swapped successfully to ${to}`); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -213,7 +228,8 @@ import { to: string, shareAmount: string, minA: string, - minB: string + minB: string, + config?: SorobanContractConfig ): Promise { try { const toScVal = addressToScVal(to); @@ -225,7 +241,8 @@ import { shareAmountScVal, minAScVal, minBScVal, - ]); + ], config); + if (config?.simulate) return result as any; // Forward the simulation text console.log(`Withdrawn successfully to ${to}:, ${result}`); return result ? (result as [BigInt, BigInt]) : null; } catch (error: unknown) { @@ -235,9 +252,10 @@ import { } } - export async function getReserves(caller: string): Promise { + export async function getReserves(caller: string, config?: SorobanContractConfig): Promise { try { - const result = await contractInt(caller, "get_rsrvs", null); + const result = await contractInt(caller, "get_rsrvs", null, config); + if (config?.simulate) return result as any; // Forward the simulation text console.log("Reserves:", result); return result ? (result as [BigInt, BigInt]) : null; } catch (error: unknown) { diff --git a/lib/stakeF.ts b/lib/stakeF.ts index d449d4e0..968a33ed 100644 --- a/lib/stakeF.ts +++ b/lib/stakeF.ts @@ -8,11 +8,9 @@ import { } from "@stellar/stellar-sdk"; import {signTransaction} from "./stellar"; import { buildTransaction } from "../utils/buildTransaction"; + import { SorobanContractConfig } from "./contract"; - // Configuration - const rpcUrl = "https://soroban-testnet.stellar.org"; - const contractAddress = "CBTYOERLDPHPODHLZ7XKPUIJJTEZKYMBKEUA2JBCRPRMMDK6A4GM2UZF"; // Replace with actual deployed contract address - const networkPassphrase = Networks.TESTNET; + const DEFAULT_STAKE_CONTRACT = "CBTYOERLDPHPODHLZ7XKPUIJJTEZKYMBKEUA2JBCRPRMMDK6A4GM2UZF"; const addressToScVal = (address: string) => { // Validate address format @@ -27,14 +25,24 @@ import { return nativeToScVal(value, { type: "i128" }); }; - const contractInt = async (caller: string, functName: string, values: any) => { + const contractInt = async ( + caller: string, + functName: string, + values: any, + config: SorobanContractConfig = { network: "testnet", rpcUrl: "https://soroban-testnet.stellar.org" } + ) => { try { - const server = new rpc.Server(rpcUrl, { allowHttp: true }); + const server = new rpc.Server(config.rpcUrl, { allowHttp: true }); const sourceAccount = await server.getAccount(caller).catch((err) => { throw new Error(`Failed to fetch account ${caller}: ${err.message}`); }); - const contract = new Contract(contractAddress); + const targetContractId = config.contractAddress || (config.network === "testnet" ? DEFAULT_STAKE_CONTRACT : ""); + if (!targetContractId) { + throw new Error("A specific contractAddress must be provided for Soroban Staking operations on mainnet."); + } + + const contract = new Contract(targetContractId); // Build transaction using unified builder const sorobanOperation = { @@ -52,7 +60,8 @@ import { let signedTxResponse: string; try { - signedTxResponse = signTransaction(prepareTxXDR, networkPassphrase); + const passphrase = config.network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET; + signedTxResponse = signTransaction(prepareTxXDR, passphrase); } catch (err: any) { throw new Error(`Failed to sign transaction: ${err.message}`); } @@ -60,7 +69,8 @@ import { // Handle both string and object response from signTransaction const signedXDR = signedTxResponse; - const tx = TransactionBuilder.fromXDR(signedXDR, Networks.TESTNET); + const passphrase = config.network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET; + const tx = TransactionBuilder.fromXDR(signedXDR, passphrase); const txResult = await server.sendTransaction(tx).catch((err) => { throw new Error(`Failed to send transaction: ${err.message}`); }); @@ -88,11 +98,11 @@ import { }; // Contract interaction functions - async function initialize(caller: string, tokenAddress: string, rewardRate: number) { + async function initialize(caller: string, tokenAddress: string, rewardRate: number, config?: SorobanContractConfig) { try { const tokenScVal = addressToScVal(tokenAddress); const rewardRateScVal = numberToI128(rewardRate); - await contractInt(caller, "initialize", [tokenScVal, rewardRateScVal]); + await contractInt(caller, "initialize", [tokenScVal, rewardRateScVal], config); return "Contract initialized successfully"; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -100,11 +110,11 @@ import { } } - async function stake(caller: string, amount: number) { + async function stake(caller: string, amount: number, config?: SorobanContractConfig) { try { const userScVal = addressToScVal(caller); const amountScVal = numberToI128(amount); - await contractInt(caller, "stake", [userScVal, amountScVal]); + await contractInt(caller, "stake", [userScVal, amountScVal], config); return `Staked ${amount} successfully`; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -112,11 +122,11 @@ import { } } - async function unstake(caller: string, amount: number) { + async function unstake(caller: string, amount: number, config?: SorobanContractConfig) { try { const userScVal = addressToScVal(caller); const amountScVal = numberToI128(amount); - await contractInt(caller, "unstake", [userScVal, amountScVal]); + await contractInt(caller, "unstake", [userScVal, amountScVal], config); return `Unstaked ${amount} successfully`; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -124,10 +134,10 @@ import { } } - async function claimRewards(caller: string) { + async function claimRewards(caller: string, config?: SorobanContractConfig) { try { const userScVal = addressToScVal(caller); - await contractInt(caller, "claim_rewards", userScVal); + await contractInt(caller, "claim_rewards", userScVal, config); return "Rewards claimed successfully"; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -135,10 +145,10 @@ import { } } - async function getStake(caller: string, userAddress: string) { + async function getStake(caller: string, userAddress: string, config?: SorobanContractConfig) { try { const userScVal = addressToScVal(userAddress); - const result = await contractInt(caller, "get_stake", userScVal); + const result = await contractInt(caller, "get_stake", userScVal, config); return `Stake for ${userAddress}: ${result}`; return result; // Returns i128 as a BigInt } catch (error: unknown) { diff --git a/tools/contract.ts b/tools/contract.ts index d742f8b2..7ea8e134 100644 --- a/tools/contract.ts +++ b/tools/contract.ts @@ -10,6 +10,8 @@ import { // Assuming env variables are already loaded elsewhere const STELLAR_PUBLIC_KEY = process.env.STELLAR_PUBLIC_KEY!; +const STELLAR_NETWORK = (process.env.STELLAR_NETWORK as "testnet" | "mainnet") || "testnet"; +const SOROBAN_RPC_URL = process.env.SOROBAN_RPC_URL || "https://soroban-testnet.stellar.org"; if (!STELLAR_PUBLIC_KEY) { throw new Error("Missing Stellar environment variables"); @@ -30,6 +32,7 @@ export const StellarLiquidityContractTool = new DynamicStructuredTool({ out: z.string().optional(), // For swap inMax: z.string().optional(), // For swap shareAmount: z.string().optional(), // For withdraw + contractAddress: z.string().optional(), // For overriding default pool on mainnet }), func: async (input: any) => { const { @@ -43,38 +46,46 @@ export const StellarLiquidityContractTool = new DynamicStructuredTool({ out, inMax, shareAmount, + contractAddress, } = input; + + const config = { + network: STELLAR_NETWORK, + rpcUrl: SOROBAN_RPC_URL, + contractAddress, + }; + try { switch (action) { case "get_share_id": { - const result = await getShareId(STELLAR_PUBLIC_KEY); + const result = await getShareId(STELLAR_PUBLIC_KEY, config); return result ?? "No share ID found."; } case "deposit": { if (!to || !desiredA || !minA || !desiredB || !minB) { throw new Error("to, desiredA, minA, desiredB, and minB are required for deposit"); } - const result = await deposit(STELLAR_PUBLIC_KEY, to, desiredA, minA, desiredB, minB); + const result = await deposit(STELLAR_PUBLIC_KEY, to, desiredA, minA, desiredB, minB, config); return result ??`Deposited successfully to ${to}.`; } case "swap": { if (!to || buyA === undefined || !out || !inMax) { throw new Error("to, buyA, out, and inMax are required for swap"); } - const result=await swap(STELLAR_PUBLIC_KEY, to, buyA, out, inMax); + const result=await swap(STELLAR_PUBLIC_KEY, to, buyA, out, inMax, config); return result ?? `Swapped successfully to ${to}.`; } case "withdraw": { if (!to || !shareAmount || !minA || !minB) { throw new Error("to, shareAmount, minA, and minB are required for withdraw"); } - const result = await withdraw(STELLAR_PUBLIC_KEY, to, shareAmount, minA, minB); + const result = await withdraw(STELLAR_PUBLIC_KEY, to, shareAmount, minA, minB, config); return result ? `Withdrawn successfully to ${to}: ${JSON.stringify(result)}` : "Withdraw failed or returned no value."; } case "get_reserves": { - const result = await getReserves(STELLAR_PUBLIC_KEY); + const result = await getReserves(STELLAR_PUBLIC_KEY, config); return result ? `Reserves: ${JSON.stringify(result)}` : "No reserves found."; diff --git a/tools/stake.ts b/tools/stake.ts index 266458f3..f4b93b79 100644 --- a/tools/stake.ts +++ b/tools/stake.ts @@ -10,6 +10,8 @@ import { // Assuming env variables are already loaded elsewhere const STELLAR_PUBLIC_KEY = process.env.STELLAR_PUBLIC_KEY!; +const STELLAR_NETWORK = (process.env.STELLAR_NETWORK as "testnet" | "mainnet") || "testnet"; +const SOROBAN_RPC_URL = process.env.SOROBAN_RPC_URL || "https://soroban-testnet.stellar.org"; if (!STELLAR_PUBLIC_KEY) { throw new Error("Missing Stellar environment variables"); @@ -25,16 +27,24 @@ export const StellarContractTool = new DynamicStructuredTool({ rewardRate: z.number().optional(), // Only for initialize amount: z.number().optional(), // For stake/unstake userAddress: z.string().optional(), // For get_stake + contractAddress: z.string().optional(), // For overriding default pool on mainnet }), func: async (input: any) => { - const { action, tokenAddress, rewardRate, amount, userAddress } = input; + const { action, tokenAddress, rewardRate, amount, userAddress, contractAddress } = input; + + const config = { + network: STELLAR_NETWORK, + rpcUrl: SOROBAN_RPC_URL, + contractAddress, + }; + try { switch (action) { case "initialize": { if (!tokenAddress || rewardRate === undefined) { throw new Error("tokenAddress and rewardRate are required for initialize"); } - const result = await initialize(STELLAR_PUBLIC_KEY, tokenAddress, rewardRate); + const result = await initialize(STELLAR_PUBLIC_KEY, tokenAddress, rewardRate, config); return result ?? "Contract initialized successfully."; } @@ -42,7 +52,7 @@ export const StellarContractTool = new DynamicStructuredTool({ if (amount === undefined) { throw new Error("amount is required for stake"); } - const result = await stake(STELLAR_PUBLIC_KEY, amount); + const result = await stake(STELLAR_PUBLIC_KEY, amount, config); return result ?? `Staked ${amount} successfully.`; } @@ -50,12 +60,12 @@ export const StellarContractTool = new DynamicStructuredTool({ if (amount === undefined) { throw new Error("amount is required for unstake"); } - const result = await unstake(STELLAR_PUBLIC_KEY, amount); + const result = await unstake(STELLAR_PUBLIC_KEY, amount, config); return result ?? `Unstaked ${amount} successfully.`; } case "claim_rewards": { - const result = await claimRewards(STELLAR_PUBLIC_KEY); + const result = await claimRewards(STELLAR_PUBLIC_KEY, config); return result ?? "Rewards claimed successfully."; } @@ -63,7 +73,7 @@ export const StellarContractTool = new DynamicStructuredTool({ if (!userAddress) { throw new Error("userAddress is required for get_stake"); } - const stakeAmount = await getStake(STELLAR_PUBLIC_KEY, userAddress); + const stakeAmount = await getStake(STELLAR_PUBLIC_KEY, userAddress, config); return `Stake for ${userAddress}: ${stakeAmount}`; }