Skip to content

Refactor: Unified Soroban Client & Generic Contract Call Tool#46

Open
wolf1276 wants to merge 3 commits intoStellar-Tools:mainfrom
wolf1276:feature/soroban-unified-client
Open

Refactor: Unified Soroban Client & Generic Contract Call Tool#46
wolf1276 wants to merge 3 commits intoStellar-Tools:mainfrom
wolf1276:feature/soroban-unified-client

Conversation

@wolf1276
Copy link
Copy Markdown

@wolf1276 wolf1276 commented Mar 30, 2026

This PR introduces a major refactor and enhancement to the SDK's Soroban interaction capability.

Summary of Changes:

  1. Unified SorobanClient: Created to centralize contract interaction logic (Simulation, Preparation, Signing, Submission, and Result Parsing).
  2. Generic Contract Call Tool: Added utility which allows the AI agent (and developers) to interact with any Soroban contract just by providing the Contract ID and function parameters.
  3. Refactored Existing Tools: Updated (LP tools) and (Staking tools) to use the new unified , removing over 150 lines of duplicate code.
  4. Enhanced Agent Interface: Added method to for easier direct usage by developers.
  5. Modernized Types: Updated to support (ES2020) and improved type safety across contract calls.
  6. Testing: Added unit tests for the generic Soroban call tool.

This improvement significantly reduces technical debt and makes the SDK future-proof for any Stellar-based protocol.


Summary by cubic

Unifies Soroban contract interactions into a single SorobanClient and adds a generic contract call tool to call any contract with less code. Adds balance tools and agent methods, and refactors LP and Staking to the new client.

  • New Features

    • Added lib/soroban.ts with SorobanClient handling simulate, prepare, sign, submit, and result parsing.
    • Added stellar_generic_soroban_call and AgentClient.sorobanCall() for any contract/function call.
    • Added stellar_get_balance and stellar_get_all_balances, plus AgentClient.getBalance() and getAllBalances(); exported in index.ts with unit tests.
  • Bug Fixes

    • Balance tools: flexible network parsing (e.g., allow stellar-mainnet) and LP share formatting.
    • Detect issuer ambiguity for non-XLM assets and require/suggest assetIssuer.
    • Agent balance methods pass rpcUrl and network through to tools; tests updated to cover new logic.

Written for commit aa32e14. Summary will update on new commits.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9 issues found across 9 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/stakeF.ts">

<violation number="1" location="lib/stakeF.ts:45">
P2: Catch blocks assume thrown values always have `.message`, which can return `undefined` for non-`Error` throws and regress error reporting.</violation>
</file>

<file name="tools/soroban.ts">

<violation number="1" location="tools/soroban.ts:6">
P2: RPC URL and network passphrase are derived independently, allowing testnet RPC + mainnet passphrase mismatch when `SOROBAN_RPC_URL` is unset.</violation>
</file>

<file name="lib/contract.ts">

<violation number="1" location="lib/contract.ts:52">
P2: Catch blocks unsafely assume thrown values always have `.message`, which can throw in the error handler and hide the original exception.</violation>
</file>

<file name="tests/unit/tools/soroban.test.ts">

<violation number="1" location="tests/unit/tools/soroban.test.ts:8">
P2: Success-path test does not verify that request fields are forwarded to `SorobanClient.call`, so parameter-mapping regressions can pass unnoticed.</violation>
</file>

<file name="lib/soroban.ts">

<violation number="1" location="lib/soroban.ts:80">
P1: Transaction source is caller-dependent, but signing always uses a fixed private key, causing source/signer mismatch and likely auth failures for non-matching callers.</violation>
</file>

<file name="tools/balance.ts">

<violation number="1" location="tools/balance.ts:36">
P1: Non-native asset lookup is ambiguous because issuer is optional, so balance can be returned for the wrong issuer when asset codes collide.</violation>

<violation number="2" location="tools/balance.ts:74">
P2: Non-native balances are incorrectly forced into credit-asset format, which can misreport balances like liquidity pool shares.</violation>
</file>

<file name="agent.ts">

<violation number="1" location="agent.ts:144">
P1: New AgentClient wrappers bypass client network/rpc configuration, causing balance and Soroban calls to use env/default network instead of `this.network`/`this.rpcUrl`.</violation>

<violation number="2" location="agent.ts:169">
P2: `sorobanCall` does not validate `publicKey`/`caller`, so an empty caller can pass through and fail at runtime in Soroban account loading.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread lib/soroban.ts
throw new Error(`Failed to prepare transaction: ${err.message}`);
});

const signedXDR = signTransaction(preparedTx.toXDR(), this.networkPassphrase);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Transaction source is caller-dependent, but signing always uses a fixed private key, causing source/signer mismatch and likely auth failures for non-matching callers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/soroban.ts, line 80:

<comment>Transaction source is caller-dependent, but signing always uses a fixed private key, causing source/signer mismatch and likely auth failures for non-matching callers.</comment>

<file context>
@@ -0,0 +1,121 @@
+        throw new Error(`Failed to prepare transaction: ${err.message}`);
+      });
+
+      const signedXDR = signTransaction(preparedTx.toXDR(), this.networkPassphrase);
+      const tx = TransactionBuilder.fromXDR(signedXDR, this.networkPassphrase);
+      
</file context>
Fix with Cubic

Comment thread tools/balance.ts
Comment thread agent.ts Outdated
Comment thread lib/stakeF.ts
const errorMessage = error instanceof Error ? error.message : String(error);
return errorMessage;
} catch (error: any) {
return error.message;
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Catch blocks assume thrown values always have .message, which can return undefined for non-Error throws and regress error reporting.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/stakeF.ts, line 45:

<comment>Catch blocks assume thrown values always have `.message`, which can return `undefined` for non-`Error` throws and regress error reporting.</comment>

<file context>
@@ -27,123 +34,57 @@ import {
-      const errorMessage = error instanceof Error ? error.message : String(error);
-      return errorMessage;
+    } catch (error: any) {
+      return error.message;
     }
   }
</file context>
Suggested change
return error.message;
return error instanceof Error ? error.message : String(error);
Fix with Cubic

Comment thread tools/soroban.ts
import { SorobanClient } from "../lib/soroban";
import { Networks, nativeToScVal } from "@stellar/stellar-sdk";

const rpcUrl = process.env.SOROBAN_RPC_URL || "https://soroban-testnet.stellar.org";
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: RPC URL and network passphrase are derived independently, allowing testnet RPC + mainnet passphrase mismatch when SOROBAN_RPC_URL is unset.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tools/soroban.ts, line 6:

<comment>RPC URL and network passphrase are derived independently, allowing testnet RPC + mainnet passphrase mismatch when `SOROBAN_RPC_URL` is unset.</comment>

<file context>
@@ -0,0 +1,50 @@
+import { SorobanClient } from "../lib/soroban";
+import { Networks, nativeToScVal } from "@stellar/stellar-sdk";
+
+const rpcUrl = process.env.SOROBAN_RPC_URL || "https://soroban-testnet.stellar.org";
+const networkPassphrase = process.env.STELLAR_NETWORK === "mainnet" ? Networks.PUBLIC : Networks.TESTNET;
+
</file context>
Fix with Cubic

Comment thread lib/contract.ts
const errorMessage = error instanceof Error ? error.message : String(error);
console.error("Failed to get share ID:", errorMessage);
} catch (error: any) {
console.error("Failed to get share ID:", error.message);
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Catch blocks unsafely assume thrown values always have .message, which can throw in the error handler and hide the original exception.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/contract.ts, line 52:

<comment>Catch blocks unsafely assume thrown values always have `.message`, which can throw in the error handler and hide the original exception.</comment>

<file context>
@@ -33,127 +40,16 @@ import {
-      const errorMessage = error instanceof Error ? error.message : String(error);
-      console.error("Failed to get share ID:", errorMessage);
+    } catch (error: any) {
+      console.error("Failed to get share ID:", error.message);
       throw error;
     }
</file context>
Fix with Cubic

vi.mock("../../../lib/soroban", () => {
return {
SorobanClient: vi.fn().mockImplementation(() => ({
call: vi.fn().mockResolvedValue("mocked_result"),
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Success-path test does not verify that request fields are forwarded to SorobanClient.call, so parameter-mapping regressions can pass unnoticed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/unit/tools/soroban.test.ts, line 8:

<comment>Success-path test does not verify that request fields are forwarded to `SorobanClient.call`, so parameter-mapping regressions can pass unnoticed.</comment>

<file context>
@@ -0,0 +1,42 @@
+vi.mock("../../../lib/soroban", () => {
+  return {
+    SorobanClient: vi.fn().mockImplementation(() => ({
+      call: vi.fn().mockResolvedValue("mocked_result"),
+    })),
+  };
</file context>
Fix with Cubic

Comment thread tools/balance.ts Outdated
Comment thread agent.ts
args: any[] = [],
operationType: string = "other"
) {
const caller = this.publicKey;
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: sorobanCall does not validate publicKey/caller, so an empty caller can pass through and fail at runtime in Soroban account loading.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At agent.ts, line 169:

<comment>`sorobanCall` does not validate `publicKey`/`caller`, so an empty caller can pass through and fail at runtime in Soroban account loading.</comment>

<file context>
@@ -130,6 +132,50 @@ export class AgentClient {
+    args: any[] = [],
+    operationType: string = "other"
+  ) {
+    const caller = this.publicKey;
+    return await stellarGenericSorobanCallTool.func({
+      caller,
</file context>
Suggested change
const caller = this.publicKey;
const caller = this.publicKey;
if (!caller) {
throw new Error("sorobanCall requires a configured publicKey (AgentConfig.publicKey or STELLAR_PUBLIC_KEY)");
}
Fix with Cubic

…hare support

- Update network parsing to be more flexible (allow 'stellar-mainnet')
- Detect and prevent mismatched asset matches by requiring/suggesting assetIssuer
- Add support for formatting liquidity pool share balances
- Ensure AgentClient balance methods respect client network/rpc config
- Update unit tests to verify account loading and new balance logic
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tools/balance.ts">

<violation number="1" location="tools/balance.ts:38">
P2: Substring matching for network selection can route to mainnet for unintended inputs; use strict matching to avoid wrong endpoint selection.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread tools/balance.ts

let horizonUrl = rpcUrl;
if (!horizonUrl) {
horizonUrl = actualNetwork.includes("mainnet")
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Substring matching for network selection can route to mainnet for unintended inputs; use strict matching to avoid wrong endpoint selection.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tools/balance.ts, line 38:

<comment>Substring matching for network selection can route to mainnet for unintended inputs; use strict matching to avoid wrong endpoint selection.</comment>

<file context>
@@ -8,39 +8,73 @@ export const stellarGetBalanceTool = new DynamicStructuredTool({
+      
+      let horizonUrl = rpcUrl;
+      if (!horizonUrl) {
+        horizonUrl = actualNetwork.includes("mainnet")
+          ? "https://horizon.stellar.org" 
+          : "https://horizon-testnet.stellar.org";
</file context>
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant