diff --git a/agent.ts b/agent.ts index 346f2153..41bfbae8 100644 --- a/agent.ts +++ b/agent.ts @@ -7,15 +7,17 @@ import { } from "./lib/contract"; import { bridgeTokenTool } from "./tools/bridge"; import { - Server, Keypair, Asset, TransactionBuilder, Operation, Networks, - BASE_FEE + BASE_FEE, + Horizon } from "@stellar/stellar-sdk"; +type Network = "testnet" | "mainnet"; + export interface AgentConfig { network: "testnet" | "mainnet"; rpcUrl?: string; @@ -76,7 +78,7 @@ export class AgentClient { this.network = config.network; this.publicKey = config.publicKey || process.env.STELLAR_PUBLIC_KEY || ""; - this.rpcUrl = config.rpcUrl || (config.network === "mainnet" + this.rpcUrl = config.rpcUrl || (config.network === ("mainnet" as string) ? "https://horizon.stellar.org" : "https://horizon-testnet.stellar.org"); @@ -101,10 +103,24 @@ export class AgentClient { params.to, params.buyA, params.out, - params.inMax + params.inMax, + this.getSorobanConfig() ); } + private getSorobanConfig() { + // Attempt to map Horizon URLs to Soroban URLs if not explicitly provided + // Horizon: https://horizon-testnet.stellar.org -> Soroban: https://soroban-testnet.stellar.org + const rpcUrl = this.rpcUrl.includes("horizon") + ? this.rpcUrl.replace("horizon", "soroban") + : this.rpcUrl; + + return { + rpcUrl, + networkPassphrase: (this.network as string) === "mainnet" ? Networks.PUBLIC : Networks.TESTNET + }; + } + /** * Bridge tokens from Stellar to EVM compatible chains. * @@ -120,14 +136,16 @@ export class AgentClient { async bridge(params: { amount: string; toAddress: string; + destinationChain?: string; + symbol?: string; }) { return await bridgeTokenTool.func({ ...params, fromNetwork: - this.network === "mainnet" + (this.network as string) === "mainnet" ? "stellar-mainnet" : "stellar-testnet", - }); + } as any); } /** @@ -147,7 +165,8 @@ export class AgentClient { params.desiredA, params.minA, params.desiredB, - params.minB + params.minB, + this.getSorobanConfig() ); }, @@ -162,16 +181,17 @@ export class AgentClient { params.to, params.shareAmount, params.minA, - params.minB + params.minB, + this.getSorobanConfig() ); }, getReserves: async () => { - return await contractGetReserves(this.publicKey); + return await contractGetReserves(this.publicKey, this.getSorobanConfig()); }, getShareId: async () => { - return await contractGetShareId(this.publicKey); + return await contractGetShareId(this.publicKey, this.getSorobanConfig()); }, }; @@ -236,8 +256,8 @@ export class AgentClient { const distributorPublicKey = distributorKeypair.publicKey(); // Connect to Stellar network - const server = new Server(this.rpcUrl); - const networkPassphrase = this.network === "mainnet" ? Networks.PUBLIC : Networks.TESTNET; + const server = new Horizon.Server(this.rpcUrl); + const networkPassphrase = (this.network as string) === "mainnet" ? Networks.PUBLIC : Networks.TESTNET; // Step 1: Load or create issuer account let issuerAccount; @@ -349,14 +369,14 @@ export class AgentClient { * @returns true if trustline exists, false otherwise */ private async checkTrustlineExists( - server: Server, + server: Horizon.Server, accountPublicKey: string, asset: Asset ): Promise { try { const account = await server.loadAccount(accountPublicKey); - return account.balances.some(balance => { + return account.balances.some((balance: Horizon.ServerApi.BalanceLine) => { if (balance.asset_type === 'native') return false; return ( @@ -386,7 +406,7 @@ export class AgentClient { * @returns Transaction hash of the trustline creation */ private async createTrustline( - server: Server, + server: Horizon.Server, accountKeypair: Keypair, asset: Asset, networkPassphrase: string @@ -436,7 +456,7 @@ export class AgentClient { * @returns Transaction hash of the locking operation */ private async lockIssuerAccount( - server: Server, + server: Horizon.Server, issuerKeypair: Keypair, networkPassphrase: string ): Promise<{ hash: string }> { diff --git a/examples/token-launch-example.ts b/examples/token-launch-example.ts index b100dea9..3a0cb531 100644 --- a/examples/token-launch-example.ts +++ b/examples/token-launch-example.ts @@ -57,9 +57,12 @@ async function exampleTokenLaunch() { console.log(`Distributor: ${result.distributorPublicKey}`); console.log(`Issuer Locked: ${result.issuerLocked}`); - } catch (error) { + } catch (error: unknown) { console.error("\n❌ Token launch failed:"); - console.error(error.message); + const message = error instanceof Error + ? error.message + : String(error) + console.error(message) } } @@ -95,9 +98,12 @@ async function exampleTokenLaunchWithLocking() { console.log(`The issuer account is now locked - no more tokens can be minted.`); console.log(`Transaction Hash: ${result.transactionHash}`); - } catch (error) { + } catch (error: unknown) { console.error("\n❌ Token launch with locking failed:"); - console.error(error.message); + const message = error instanceof Error + ? error.message + : String(error) + console.error(message) } } diff --git a/gap_analysis_report.md b/gap_analysis_report.md new file mode 100644 index 00000000..46e3b042 --- /dev/null +++ b/gap_analysis_report.md @@ -0,0 +1,105 @@ +# Stellar AgentKit Gap Analysis Report + +## Part 1: Tool Analysis (`tools/` directory) + +### 1. `bridge.ts` (`bridgeTokenTool`) +- **Action/Wrap**: Bridges tokens from Stellar (mainnet/testnet) to EVM compatible chains using AllbridgeCoreSdk. +- **Zod Schema**: + ```typescript + z.object({ + amount: z.string().describe("The amount of tokens to bridge"), + toAddress: z.string().describe("The destination address"), + fromNetwork: z + .enum(["stellar-testnet", "stellar-mainnet"]) + .default("stellar-testnet") + .describe("Source Stellar network"), + }) + ``` +- **Try/Catch Exists**: **No** top-level `try/catch` in the `func`. +- **Error Typing**: Any errors thrown by `AllbridgeCoreSdk` or Stellar operations will bubble up as raw, untyped errors to the caller. Explicit manual throws just use standard strings. +- **Pre-SDK Validation**: Minimal. It validates runtime safety by checking `process.env.ALLOW_MAINNET_BRIDGE` but lacks proactive valid EVM address validation or `amount` parsing before executing SDK chains. + +### 2. `contract.ts` (`StellarLiquidityContractTool`) +- **Action/Wrap**: Interacts with a liquidity AMM contract on Stellar Soroban (actions: `get_share_id`, `deposit`, `swap`, `withdraw`, `get_reserves`). +- **Zod Schema**: + ```typescript + z.object({ + action: z.enum(["get_share_id", "deposit", "swap", "withdraw", "get_reserves"]), + to: z.string().optional(), + desiredA: z.string().optional(), + minA: z.string().optional(), + desiredB: z.string().optional(), + minB: z.string().optional(), + buyA: z.boolean().optional(), + out: z.string().optional(), + inMax: z.string().optional(), + shareAmount: z.string().optional(), + }) + ``` +- **Try/Catch Exists**: **Yes** +- **Error Typing**: Typed as `error: any` in the catch block (`error.message` is extracted). +- **Pre-SDK Validation**: Within the `switch(action)`, there are manual `if/throw` blocks asserting requisite params exist, but weak input validation occurs prior to schema parsing (all inputs blindly take strings without strict address validation). + +### 3. `stake.ts` (`StellarContractTool`) +- **Action/Wrap**: Interacts with a staking contract on Stellar Soroban (actions: `initialize`, `stake`, `unstake`, `claim_rewards`, `get_stake`). +- **Zod Schema**: + ```typescript + z.object({ + action: z.enum(["initialize", "stake", "unstake", "claim_rewards", "get_stake"]), + tokenAddress: z.string().optional(), + rewardRate: z.number().optional(), + amount: z.number().optional(), + userAddress: z.string().optional(), + }) + ``` +- **Try/Catch Exists**: **Yes** +- **Error Typing**: Typed as `error: any` securely in a wrapper. +- **Pre-SDK Validation**: Like above, checks `undefined` state for target arguments inside a JS switch condition. No cryptographically secure formatting checks prior to execution. + +### 4. `stellar.ts` (`stellarSendPaymentTool`) +- **Action/Wrap**: Sends a standard payment (XLM native asset) on the Stellar network. +- **Zod Schema**: + ```typescript + z.object({ + recipient: z.string().describe("The Stellar address to send to"), + amount: z.string().describe("The amount of XLM to send (as a string)"), + }) + ``` +- **Try/Catch Exists**: **Yes** +- **Error Typing**: Intelligently typed through fallback object casting: `(error as { response?: { data?: { title?: string } }; message?: string })`. +- **Pre-SDK Validation**: Very strong. It explicitly asserts `StellarSdk.StrKey.isValidEd25519PublicKey(recipient)` and verifies computational bounds `!amount || isNaN(Number(amount)) || Number(amount) <= 0` prior to any SDK calls. + +--- + +## Part 2: Critical Gap Analysis + +### 1. Every tool that has no try/catch +- `bridgeTokenTool` (`tools/bridge.ts`) does not encapsulate its complex multi-step cross-chain logic inside a `try/catch` block, guaranteeing SDK errors halt flow unexpectedly. + +### 2. Every tool with missing or weak Zod validation +- `bridgeTokenTool` (`bridge.ts`): No `z.string().regex()` matches to enforce valid EVM `toAddress` formats or parsing bounds for `amount`. +- `StellarLiquidityContractTool` (`contract.ts`) & `StellarContractTool` (`stake.ts`): Huge structural vulnerability. They use a monolithic catch-all schema where every sub-argument across all actions is arbitrarily `.optional()`, relying purely on JavaScript boilerplate validation. + +### 3. Every tool in README not yet in tools/ +- **None missing**. All functionality explicitly advertised in the README (`Swap`, `Bridge`, and `LP operations`) exists efficiently in the tooling architecture (`bridge.ts` and `contract.ts`). + +### 4. All TypeScript errors from `tsc` +Issuing `npx tsc --noEmit` yielded 7 errors spanning 3 files: +1. `agent.ts:10:3` - `error TS2614: Module '"@stellar/stellar-sdk"' has no exported member 'Server'. Did you mean to use 'import Server from "@stellar/stellar-sdk"' instead?` +2. `agent.ts:240:33` - `error TS2367: This comparison appears to be unintentional because the types '"testnet"' and '"mainnet"' have no overlap.` +3. `agent.ts:359:36` - `error TS7006: Parameter 'balance' implicitly has an 'any' type.` +4. `examples/token-launch-example.ts:62:19` - `error TS18046: 'error' is of type 'unknown'.` +5. `examples/token-launch-example.ts:100:19` - `error TS18046: 'error' is of type 'unknown'.` +6. `tests/unit/tools/bridge.test.ts:60:64` - `error TS2367: This comparison appears to be unintentional because the types '"false"' and '"true"' have no overlap.` +7. `tests/unit/tools/bridge.test.ts:76:27` - `error TS2367: This comparison appears to be unintentional because the types '"stellar-testnet"' and '"stellar-mainnet"' have no overlap.` + +### 5. Whether there is a test suite at all +- **Yes.** The agent integrates a fully functioning `vitest` suite. Output yields `5 passed (5), Tests 28 passed (28)`. + +### 6. The 3 highest-impact contributions possible +1. **Refactor Zod Schemas into Discriminated Unions:** + - Overhaul `contract.ts` and `stake.ts` schemas from amorphous grids of `.optional()` fields into runtime-safe discriminated unions based on the `action` variant. This provides LLMs 100% rigid interfaces to interface flawlessly against. +2. **Resolve Global TypeScript Bleeding & Build Failure:** + - Resolve the compile-time breakages. Fixing `@stellar/stellar-sdk` Server imports and repairing impossible boolean/enum tests means AgentKit integrates correctly in typed deployment pipelines. +3. **Overhaul the Bridge Tool Exception Handling Pipeline:** + - As cross-chain operations entail extreme failure rates, retrofitting `bridgeTokenTool` with an all-encompassing `try/catch` and highly typed error objects mapping specific ALLBRIDGE faults saves devastating crashes down the line. diff --git a/lib/contract.ts b/lib/contract.ts index e9fd7cc6..ade957bb 100644 --- a/lib/contract.ts +++ b/lib/contract.ts @@ -12,10 +12,16 @@ 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; + // Configuration defaults (can be overridden) + const DEFAULT_RPC_URL = "https://soroban-testnet.stellar.org"; + const DEFAULT_CONTRACT_ADDRESS = "CCUMBJFVC3YJOW3OOR6WTWTESH473ZSXQEGYPQDWXAYYC4J77OT4NVHJ"; + const DEFAULT_NETWORK_PASSPHRASE = Networks.TESTNET; + + export interface ContractConfig { + rpcUrl?: string; + contractAddress?: string; + networkPassphrase?: string; + } // Utility functions for ScVal conversion const addressToScVal = (address: string) => { @@ -35,7 +41,11 @@ import { }; // Core contract interaction function - const contractInt = async (caller: string, functName: string, values: any) => { + const contractInt = async (caller: string, functName: string, values: any, config?: ContractConfig) => { + const rpcUrl = config?.rpcUrl || DEFAULT_RPC_URL; + const contractAddress = config?.contractAddress || DEFAULT_CONTRACT_ADDRESS; + const networkPassphrase = config?.networkPassphrase || DEFAULT_NETWORK_PASSPHRASE; + try { const server = new rpc.Server(rpcUrl, { allowHttp: true }); const sourceAccount = await server.getAccount(caller).catch((err) => { @@ -50,7 +60,7 @@ import { functionName: functName, args: values == null ? undefined : Array.isArray(values) ? values : [values], }; - const transaction = buildTransaction("lp", sourceAccount, sorobanOperation); + const transaction = buildTransaction("lp", sourceAccount, sorobanOperation, { networkPassphrase }); const simulation = await server.simulateTransaction(transaction).catch((err) => { console.error(`Simulation failed for ${functName}: ${err.message}`); @@ -97,9 +107,9 @@ import { } // Handle both string and object response from signTransaction - const signedXDR = signedTxResponse + const signedXDR = signedTxResponse - const tx = TransactionBuilder.fromXDR(signedXDR, Networks.TESTNET); + const tx = TransactionBuilder.fromXDR(signedXDR, networkPassphrase); 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,9 @@ import { }; // Contract interaction functions - export async function getShareId(caller: string): Promise { + export async function getShareId(caller: string, config?: ContractConfig): Promise { try { - const result = await contractInt(caller, "share_id", null); + const result = await contractInt(caller, "share_id", null, config); console.log("Share ID:", result); return result as string | null; } catch (error: unknown) { @@ -164,7 +174,8 @@ import { desiredA: string, minA: string, desiredB: string, - minB: string + minB: string, + config?: ContractConfig ) { try { const toScVal = addressToScVal(to); @@ -178,7 +189,7 @@ import { minAScVal, desiredBScVal, minBScVal, - ]); + ], config); console.log(`Deposited successfully to ${to}`); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -192,14 +203,15 @@ import { to: string, buyA: boolean, out: string, - inMax: string + inMax: string, + config?: ContractConfig ) { 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]); + await contractInt(caller, "swap", [toScVal, buyAScVal, outScVal, inMaxScVal], config); console.log(`Swapped successfully to ${to}`); } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -213,7 +225,8 @@ import { to: string, shareAmount: string, minA: string, - minB: string + minB: string, + config?: ContractConfig ): Promise { try { const toScVal = addressToScVal(to); @@ -225,7 +238,7 @@ import { shareAmountScVal, minAScVal, minBScVal, - ]); + ], config); console.log(`Withdrawn successfully to ${to}:, ${result}`); return result ? (result as [BigInt, BigInt]) : null; } catch (error: unknown) { @@ -235,9 +248,9 @@ import { } } - export async function getReserves(caller: string): Promise { + export async function getReserves(caller: string, config?: ContractConfig): Promise { try { - const result = await contractInt(caller, "get_rsrvs", null); + const result = await contractInt(caller, "get_rsrvs", null, config); 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..6bbc6fa1 100644 --- a/lib/stakeF.ts +++ b/lib/stakeF.ts @@ -9,10 +9,16 @@ import { import {signTransaction} from "./stellar"; import { buildTransaction } from "../utils/buildTransaction"; - // Configuration - const rpcUrl = "https://soroban-testnet.stellar.org"; - const contractAddress = "CBTYOERLDPHPODHLZ7XKPUIJJTEZKYMBKEUA2JBCRPRMMDK6A4GM2UZF"; // Replace with actual deployed contract address - const networkPassphrase = Networks.TESTNET; + // Configuration defaults (can be overridden) + const DEFAULT_RPC_URL = "https://soroban-testnet.stellar.org"; + const DEFAULT_CONTRACT_ADDRESS = "CBTYOERLDPHPODHLZ7XKPUIJJTEZKYMBKEUA2JBCRPRMMDK6A4GM2UZF"; + const DEFAULT_NETWORK_PASSPHRASE = Networks.TESTNET; + + export interface StakeConfig { + rpcUrl?: string; + contractAddress?: string; + networkPassphrase?: string; + } const addressToScVal = (address: string) => { // Validate address format @@ -27,7 +33,11 @@ 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?: StakeConfig) => { + const rpcUrl = config?.rpcUrl || DEFAULT_RPC_URL; + const contractAddress = config?.contractAddress || DEFAULT_CONTRACT_ADDRESS; + const networkPassphrase = config?.networkPassphrase || DEFAULT_NETWORK_PASSPHRASE; + try { const server = new rpc.Server(rpcUrl, { allowHttp: true }); const sourceAccount = await server.getAccount(caller).catch((err) => { @@ -42,7 +52,7 @@ import { functionName: functName, args: values == null ? undefined : Array.isArray(values) ? values : [values], }; - const transaction = buildTransaction("stake", sourceAccount, sorobanOperation); + const transaction = buildTransaction("stake", sourceAccount, sorobanOperation, { networkPassphrase }); // Prepare and sign transaction const preparedTx = await server.prepareTransaction(transaction).catch((err) => { @@ -60,7 +70,7 @@ import { // Handle both string and object response from signTransaction const signedXDR = signedTxResponse; - const tx = TransactionBuilder.fromXDR(signedXDR, Networks.TESTNET); + const tx = TransactionBuilder.fromXDR(signedXDR, networkPassphrase); 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?: StakeConfig) { 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?: StakeConfig) { 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?: StakeConfig) { 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?: StakeConfig) { 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,12 +145,11 @@ import { } } - async function getStake(caller: string, userAddress: string) { + async function getStake(caller: string, userAddress: string, config?: StakeConfig) { 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) { const errorMessage = error instanceof Error ? error.message : String(error); return errorMessage; // Returns error message as a string diff --git a/lib/stellar.ts b/lib/stellar.ts index 1d66b91d..872f25e3 100644 --- a/lib/stellar.ts +++ b/lib/stellar.ts @@ -1,9 +1,13 @@ import { Keypair, TransactionBuilder } from "stellar-sdk"; -export const signTransaction = (txXDR: string, networkPassphrase: string) => { - const keypair = Keypair.fromSecret(`${process.env.STELLAR_PRIVATE_KEY}`); - const transaction = TransactionBuilder.fromXDR(txXDR, networkPassphrase); - transaction.sign(keypair); - return transaction.toXDR(); - }; +export const signTransaction = (txXDR: string, networkPassphrase: string, secretKey?: string) => { + const finalSecret = secretKey || process.env.STELLAR_PRIVATE_KEY; + if (!finalSecret) { + throw new Error("No Stellar secret key provided for signing. Set STELLAR_PRIVATE_KEY in .env or pass it explicitly."); + } + const keypair = Keypair.fromSecret(finalSecret); + const transaction = TransactionBuilder.fromXDR(txXDR, networkPassphrase); + transaction.sign(keypair); + return transaction.toXDR(); +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 229b51fe..184ce2b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1640,7 +1640,6 @@ "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.2.tgz", "integrity": "sha512-BqVwEG+TaG2yCkBMbD3C4hdpustR4FpuUFRPUmqRZYYlPI9Hg4XMWxHWOWRzHE9Lkc9NDjzXFX7lDXSgzC7R1A==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", @@ -2030,7 +2029,6 @@ "integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "4.0.18", "fflate": "^0.8.2", @@ -2239,6 +2237,16 @@ "license": "Apache-2.0", "optional": true }, + "node_modules/bare-url": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", + "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base-x": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.1.tgz", @@ -3439,7 +3447,6 @@ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", "license": "MIT", - "peer": true, "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } @@ -4038,18 +4045,6 @@ } } }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "license": "MIT", - "optional": true, - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, "node_modules/noms": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", @@ -4289,7 +4284,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -5103,7 +5097,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5199,7 +5192,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -5275,7 +5267,6 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -5468,6 +5459,17 @@ } } }, + "node_modules/web3-eth-abi/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "optional": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/web3-eth-accounts": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-4.3.1.tgz", @@ -5672,7 +5674,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -5932,7 +5933,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=8.3.0" }, @@ -6016,7 +6016,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/tests/unit/tools/bridge.test.ts b/tests/unit/tools/bridge.test.ts index ac1de376..1cc2cf7d 100644 --- a/tests/unit/tools/bridge.test.ts +++ b/tests/unit/tools/bridge.test.ts @@ -55,9 +55,9 @@ describe('Bridge Tool - Network Configuration', () => { it('should block mainnet when ALLOW_MAINNET_BRIDGE is false', () => { const fromNetwork: StellarNetwork = "stellar-mainnet"; - const allowMainnetBridge = "false"; + const allowMainnetBridge: string | undefined = "false"; - const shouldBlock = fromNetwork === "stellar-mainnet" && allowMainnetBridge !== "true"; + const shouldBlock = fromNetwork === "stellar-mainnet" && (allowMainnetBridge as string) !== "true"; expect(shouldBlock).toBe(true); }); @@ -65,15 +65,15 @@ describe('Bridge Tool - Network Configuration', () => { const fromNetwork: StellarNetwork = "stellar-mainnet"; const allowMainnetBridge = "true"; - const shouldBlock = fromNetwork === "stellar-mainnet" && allowMainnetBridge !== "true"; + const shouldBlock = fromNetwork === "stellar-mainnet" && (allowMainnetBridge as string) !== "true"; expect(shouldBlock).toBe(false); }); it('should always allow testnet regardless of ALLOW_MAINNET_BRIDGE', () => { - const fromNetwork: StellarNetwork = "stellar-testnet"; + let fromNetwork: StellarNetwork = "stellar-testnet"; const allowMainnetBridge = undefined; - const shouldBlock = fromNetwork === "stellar-mainnet" && allowMainnetBridge !== "true"; + const shouldBlock = fromNetwork === ("stellar-mainnet" as StellarNetwork) && allowMainnetBridge !== "true"; expect(shouldBlock).toBe(false); }); }); diff --git a/tools/bridge.ts b/tools/bridge.ts index 252d6b30..e0942542 100644 --- a/tools/bridge.ts +++ b/tools/bridge.ts @@ -43,61 +43,84 @@ export const bridgeTokenTool = new DynamicStructuredTool({ schema: z.object({ amount: z.string().describe("The amount of tokens to bridge"), - toAddress: z.string().describe("The destination address"), + toAddress: z.string().describe("The destination address on the target chain"), fromNetwork: z .enum(["stellar-testnet", "stellar-mainnet"]) .default("stellar-testnet") .describe("Source Stellar network"), + destinationChain: z + .enum(["ETH", "BSC", "POL", "ARB", "OP", "AVA"]) + .default("ETH") + .describe("The destination chain symbol (ETH, BSC, POL, ARB, OP, AVA)"), + symbol: z + .string() + .default("USDC") + .describe("The token symbol to bridge (defaults to USDC)"), }), func: async ({ amount, toAddress, fromNetwork, + destinationChain = "ETH", + symbol = "USDC", }: { amount: string; toAddress: string; fromNetwork: StellarNetwork; + destinationChain?: string; + symbol?: string; }) => { - // Mainnet safeguard - additional layer beyond AgentClient - if ( - fromNetwork === "stellar-mainnet" && - process.env.ALLOW_MAINNET_BRIDGE !== "true" - ) { - throw new Error( - "Mainnet bridging is disabled. Set ALLOW_MAINNET_BRIDGE=true in your .env file to enable." - ); - } + try { + // Mainnet safeguard - additional layer beyond AgentClient + if ( + fromNetwork === "stellar-mainnet" && + process.env.ALLOW_MAINNET_BRIDGE !== "true" + ) { + throw new Error( + "Mainnet bridging is disabled. Set ALLOW_MAINNET_BRIDGE=true in your .env file to enable." + ); + } - const sdk = new AllbridgeCoreSdk({ - ...nodeRpcUrlsDefault, - SRB: `${process.env.SRB_PROVIDER_URL}`, - }); + const sdk = new AllbridgeCoreSdk({ + ...nodeRpcUrlsDefault, + SRB: `${process.env.SRB_PROVIDER_URL}`, + }); - const chainDetailsMap = await sdk.chainDetailsMap(); + const chainDetailsMap = await sdk.chainDetailsMap(); - const sourceToken = ensure( - chainDetailsMap[ChainSymbol.SRB].tokens.find( - (t) => t.symbol === "USDC" - ) - ); - const destinationToken = ensure( - chainDetailsMap[ChainSymbol.ETH].tokens.find( - (t) => t.symbol === "USDC" - ) - ); + const sourceToken = chainDetailsMap[ChainSymbol.SRB].tokens.find( + (t) => t.symbol === symbol + ); + + if (!sourceToken) { + throw new Error(`Token ${symbol} not found on Soroban network.`); + } - const sendParams = { - amount, - fromAccountAddress: fromAddress, - toAccountAddress: toAddress, - sourceToken, - destinationToken, - messenger: Messenger.ALLBRIDGE, - extraGas: "1.15", - extraGasFormat: AmountFormat.FLOAT, - gasFeePaymentMethod: FeePaymentMethod.WITH_STABLECOIN, - }; + const destChainSymbol = destinationChain as ChainSymbol; + if (!chainDetailsMap[destChainSymbol]) { + throw new Error(`Destination chain ${destinationChain} not supported.`); + } + + const destinationToken = chainDetailsMap[destChainSymbol].tokens.find( + (t) => t.symbol === symbol + ); + + if (!destinationToken) { + throw new Error(`Token ${symbol} not found on ${destinationChain} network.`); + } + + const sendParams = { + amount, + fromAccountAddress: fromAddress, + toAccountAddress: toAddress, + sourceToken, + destinationToken, + messenger: Messenger.ALLBRIDGE, + extraGas: "1.15", + extraGasFormat: AmountFormat.FLOAT, + gasFeePaymentMethod: FeePaymentMethod.WITH_STABLECOIN, + }; const xdrTx = (await sdk.bridge.rawTxBuilder.send( sendParams @@ -225,12 +248,16 @@ export const bridgeTokenTool = new DynamicStructuredTool({ }; } - return { - status: "confirmed", - hash: sent.hash, - network: fromNetwork, - asset: sourceToken.symbol, - amount, - }; + return { + status: "confirmed", + hash: sent.hash, + network: fromNetwork, + asset: sourceToken.symbol, + amount, + }; + } catch (error: any) { + console.error("Bridge tool error:", error); + throw new Error(`Bridge operation failed: ${error.message || String(error)}`); + } }, }); \ No newline at end of file diff --git a/tools/contract.ts b/tools/contract.ts index d742f8b2..8c56bd91 100644 --- a/tools/contract.ts +++ b/tools/contract.ts @@ -19,18 +19,32 @@ export const StellarLiquidityContractTool = new DynamicStructuredTool({ name: "stellar_liquidity_contract_tool", description: "Interact with a liquidity contract on Stellar Soroban: getShareId, deposit, swap, withdraw, getReserves.", - schema: z.object({ - action: z.enum(["get_share_id", "deposit", "swap", "withdraw", "get_reserves"]), - to: z.string().optional(), // For deposit, swap, withdraw - desiredA: z.string().optional(), // For deposit - minA: z.string().optional(), // For deposit, withdraw - desiredB: z.string().optional(), // For deposit - minB: z.string().optional(), // For deposit, withdraw - buyA: z.boolean().optional(), // For swap - out: z.string().optional(), // For swap - inMax: z.string().optional(), // For swap - shareAmount: z.string().optional(), // For withdraw - }), + schema: z.discriminatedUnion("action", [ + z.object({ action: z.literal("get_share_id") }), + z.object({ + action: z.literal("deposit"), + to: z.string().describe("Recipient address"), + desiredA: z.string().describe("Desired amount of asset A"), + minA: z.string().describe("Minimum amount of asset A"), + desiredB: z.string().describe("Desired amount of asset B"), + minB: z.string().describe("Minimum amount of asset B"), + }), + z.object({ + action: z.literal("swap"), + to: z.string().describe("Recipient address"), + buyA: z.boolean().describe("True if buying A, false if buying B"), + out: z.string().describe("Amount to receive"), + inMax: z.string().describe("Maximum amount to send"), + }), + z.object({ + action: z.literal("withdraw"), + to: z.string().describe("Recipient address"), + shareAmount: z.string().describe("Amount of share tokens to burn"), + minA: z.string().describe("Minimum amount of asset A to receive"), + minB: z.string().describe("Minimum amount of asset B to receive"), + }), + z.object({ action: z.literal("get_reserves") }), + ]), func: async (input: any) => { const { action, diff --git a/tools/stake.ts b/tools/stake.ts index 266458f3..825b49b3 100644 --- a/tools/stake.ts +++ b/tools/stake.ts @@ -19,52 +19,67 @@ export const StellarContractTool = new DynamicStructuredTool({ name: "stellar_contract_tool", description: "Interact with a staking contract on Stellar Soroban: initialize, stake, unstake, claim rewards, or get stake.", - schema: z.object({ - action: z.enum(["initialize", "stake", "unstake", "claim_rewards", "get_stake"]), - tokenAddress: z.string().optional(), // Only for initialize - rewardRate: z.number().optional(), // Only for initialize - amount: z.number().optional(), // For stake/unstake - userAddress: z.string().optional(), // For get_stake - }), + schema: z.discriminatedUnion("action", [ + z.object({ + action: z.literal("initialize"), + tokenAddress: z.string().describe("The token address to be used for rewards"), + rewardRate: z.number().describe("The rate of rewards issuance"), + fromNetwork: z.enum(["stellar-testnet", "stellar-mainnet"]).default("stellar-testnet"), + }), + z.object({ + action: z.literal("stake"), + amount: z.number().describe("The amount of tokens to stake"), + fromNetwork: z.enum(["stellar-testnet", "stellar-mainnet"]).default("stellar-testnet"), + }), + z.object({ + action: z.literal("unstake"), + amount: z.number().describe("The amount of tokens to unstake"), + fromNetwork: z.enum(["stellar-testnet", "stellar-mainnet"]).default("stellar-testnet"), + }), + z.object({ + action: z.literal("claim_rewards"), + fromNetwork: z.enum(["stellar-testnet", "stellar-mainnet"]).default("stellar-testnet"), + }), + z.object({ + action: z.literal("get_stake"), + userAddress: z.string().describe("The user address to query stake for"), + fromNetwork: z.enum(["stellar-testnet", "stellar-mainnet"]).default("stellar-testnet"), + }), + ]), func: async (input: any) => { - const { action, tokenAddress, rewardRate, amount, userAddress } = input; + const { action, tokenAddress, rewardRate, amount, userAddress, fromNetwork } = input; + + // Build dynamic config based on network + const config = { + networkPassphrase: fromNetwork === "stellar-mainnet" ? "Public Global Stellar Network ; October 2015" : "Test SDF Network ; September 2015", + rpcUrl: fromNetwork === "stellar-mainnet" ? "https://soroban.stellar.org" : "https://soroban-testnet.stellar.org" + }; + 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."; } case "stake": { - 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.`; } case "unstake": { - 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."; } case "get_stake": { - if (!userAddress) { - throw new Error("userAddress is required for get_stake"); - } - const stakeAmount = await getStake(STELLAR_PUBLIC_KEY, userAddress); - return `Stake for ${userAddress}: ${stakeAmount}`; + const stakeAmount = await getStake(STELLAR_PUBLIC_KEY, userAddress, config); + return stakeAmount; } default: diff --git a/tools/stellar.ts b/tools/stellar.ts index dfc4eb95..f0940bbd 100644 --- a/tools/stellar.ts +++ b/tools/stellar.ts @@ -11,6 +11,11 @@ export const stellarSendPaymentTool = new DynamicStructuredTool({ amount: z.string().describe("The amount of XLM to send (as a string)"), }), func: async ({ recipient, amount }: { recipient: string; amount: string }) => { + const isMainnet = process.env.STELLAR_NETWORK === "mainnet"; + const horizonUrl = isMainnet ? "https://horizon.stellar.org" : "https://horizon-testnet.stellar.org"; + const networkPassphrase = isMainnet ? StellarSdk.Networks.PUBLIC : StellarSdk.Networks.TESTNET; + + try { // Step 1: Validate inputs if (!StellarSdk.StrKey.isValidEd25519PublicKey(recipient)) { @@ -29,12 +34,12 @@ export const stellarSendPaymentTool = new DynamicStructuredTool({ const sourcePublicKey = keypair.publicKey(); // Step 3: Create an unsigned transaction - const server = new StellarSdk.Horizon.Server("https://horizon-testnet.stellar.org"); + const server = new StellarSdk.Horizon.Server(horizonUrl); const account = await server.loadAccount(sourcePublicKey); const transaction = new StellarSdk.TransactionBuilder(account, { fee: StellarSdk.BASE_FEE, - networkPassphrase: StellarSdk.Networks.TESTNET, + networkPassphrase, }) .addOperation( StellarSdk.Operation.payment({ @@ -51,15 +56,13 @@ export const stellarSendPaymentTool = new DynamicStructuredTool({ const signedTxXdr = transaction.toXDR(); // Step 5: Submit the transaction - const tx = new StellarSdk.Transaction(signedTxXdr, StellarSdk.Networks.TESTNET); - const response = await server.submitTransaction(tx); + const response = await server.submitTransaction(transaction); return `Transaction successful! Hash: ${response.hash}`; - } catch (error) { + } catch (error: any) { const errorMessage = - (error as { response?: { data?: { title?: string } }; message?: string }) - .response?.data?.title || - (error as Error).message || + error.response?.data?.title || + error.message || "Unknown error occurred"; return `Transaction failed: ${errorMessage}`; } diff --git a/utils/buildTransaction.ts b/utils/buildTransaction.ts index 329a6689..7aacc694 100644 --- a/utils/buildTransaction.ts +++ b/utils/buildTransaction.ts @@ -16,6 +16,7 @@ interface BuildTransactionConfig { fee?: string; timeout?: number; memo?: string; + networkPassphrase?: string; } /** @@ -54,9 +55,9 @@ export function buildTransaction( const fee = config.fee || BASE_FEE; const timeout = config.timeout !== undefined ? config.timeout : getDefaultTimeout(operationType); const memo = config.memo; + const networkPassphrase = config.networkPassphrase || Networks.TESTNET; // Build transaction parameters - const networkPassphrase = Networks.TESTNET; const memoValue = memo ? Memo.text(memo) : undefined; const params = { fee,