Skip to content

feat(sdk): implement real Soroban transaction pipeline - #147

Merged
wumibals merged 1 commit into
LadderMine:mainfrom
michaelsimeon001:feature/139-real-soroban-tx-pipeline
Aug 22, 2026
Merged

feat(sdk): implement real Soroban transaction pipeline#147
wumibals merged 1 commit into
LadderMine:mainfrom
michaelsimeon001:feature/139-real-soroban-tx-pipeline

Conversation

@michaelsimeon001

Copy link
Copy Markdown
Contributor

Summary

deposit/withdraw/earlyExit funneled through two stub private methods (simulate/simulateThenSubmit) that never built, simulated, signed, or submitted anything — calling yl.deposit() today does nothing on-chain, despite the README documenting it as a working API.

TransactionPipeline (sdks/typescript/src/transactions.ts)

Build -> simulate -> assemble -> sign (via the configured Signer) -> submit, for every VaultRouter write call; simulate-only for reads. Always simulates fresh per call rather than reusing a fee/resource footprint estimated for a different transaction.

Tier/Address/amount are encoded the way Soroban actually encodes them — I verified this against the real installed @stellar/stellar-sdk implementation rather than assuming: Tier as scvVec([Symbol(tag)]) (how #[contracttype] enums encode, confirmed by reading scValToNative's actual switch statement), Address via .toScVal(), amount as i128.

Typed contract errors

Contract-level rejections (BelowMinDeposit, AssetNotAllowed, ProtocolPaused, LockNotExpired, InvalidTier, DepositCapExceeded) are parsed out of Soroban's Error(Contract, #N) simulation-error string into typed classes extending YieldLadderError, matching contracts/vault_router/src/error.rs's discriminants — replacing the old fragile err.message.includes('lock') string-matching for withdraw. Code 7 is genuinely ambiguous between the router's own error enum and a tier vault's (they don't share a numbering scheme for every code) and falls back to a generic VaultContractError rather than guessing which one it is.

Transaction expiry vs. on-chain rejection

Time-bounds expiring while the user is still at their wallet's signing prompt is distinguished from a genuine on-chain rejection via the submission result's txTooLate code (errorResult.result().switch().name), surfaced as TransactionExpiredError.

Signer contract

types.ts's new Signer interface is aligned with app/src/lib/wallet/types.ts's WalletAdapter.signTransaction(xdr, opts?) — a real wallet adapter can be passed as signer directly, no shim needed, so the SDK and the app share one signing abstraction instead of two incompatible ones. YieldLadderOptions gained publicKey/vaultRouterContractId/assetContractId — none of these existed before, and a transaction can't be built without them (nothing is deployed yet per deployments/*.json, so they can't be hardcoded).

Public API kept stable

withdraw/earlyExit still take just { tier } (no amount) — they query the caller's current position via VaultRouter.position() first and submit that as a full withdrawal/exit, keeping the existing signature the README already documents. Removed the redundant simulate()-then-simulateThenSubmit() double-call in earlyExit per the issue's explicit ask.

Dependency

@stellar/stellar-sdk moved from peer+dev to a real dependencies entry (still declared as a peerDependency too, so a consuming app already using the same SDK version — e.g. via its own WalletAdapter — doesn't end up with two copies bundled).

Test plan

  • pnpm typecheck — passes
  • pnpm build — passes
  • pnpm test15 passing, mocking only the RPC layer (SorobanRpc.Server, assembleTransaction) so real ScVal encode/decode, Tier/Address/amount construction, and Position struct decoding are all exercised for real, not faked:
    • happy path for deposit/withdraw/earlyExit, including that the configured Signer is actually invoked with the right args
    • AssetNotAllowed (code 4), ProtocolPaused (code 6), BelowMinDeposit (code 2, from the contract itself, not just the SDK's own pre-check), LockNotExpired (code 3) revert paths via mocked simulation responses
    • an unmapped contract-error code (7) falls back to VaultContractError rather than a wrong guess
    • transaction expiry (txTooLate) vs. a plain submission rejection are distinguished
    • earlyExit simulates exactly once (previously called simulate() then simulateThenSubmit())
    • position() decodes a real Position struct and correctly falls back to the first tier with a non-zero principal
  • README's yl.deposit(...) example updated to the real constructor shape (now requires publicKey/vaultRouterContractId/assetContractId) and is backed by an executable test rather than aspirational documentation.

Closes #139

deposit/withdraw/earlyExit funneled through two stub methods that never
built, simulated, signed, or submitted anything — calling yl.deposit()
today does nothing on-chain, despite the README documenting it as working.

- New TransactionPipeline (sdks/typescript/src/transactions.ts): build ->
  simulate -> assemble -> sign (via the configured Signer) -> submit, for
  every VaultRouter write call; simulate-only for reads. Always simulates
  fresh per call rather than reusing a fee/footprint estimated for a
  different transaction.
- Tier/Address/amount ScVal encoding matches how Soroban actually encodes
  them: Tier as scvVec([Symbol(tag)]) (how #[contracttype] enums encode,
  confirmed against the real @stellar/stellar-sdk implementation, not
  assumed), Address via Address.toScVal(), amount as i128.
- Contract-level rejections (BelowMinDeposit, AssetNotAllowed,
  ProtocolPaused, LockNotExpired, InvalidTier, DepositCapExceeded) are
  parsed out of Soroban's `Error(Contract, #N)` simulation-error string
  into typed error classes extending YieldLadderError, matching
  contracts/vault_router/src/error.rs's discriminants — replaces the old
  fragile `err.message.includes('lock')` string-matching for withdraw.
  Code 7 is genuinely ambiguous between the router's own error enum and a
  tier vault's (they don't share a numbering scheme for every code) and
  falls back to a generic VaultContractError rather than guessing.
- Transaction expiry (time-bounds expiring while the user is still at
  their wallet's signing prompt) is distinguished from an on-chain
  rejection via the submission result's txTooLate code, surfaced as
  TransactionExpiredError rather than a generic submission failure.
- Signer contract (types.ts) aligned with app/src/lib/wallet/types.ts's
  WalletAdapter.signTransaction(xdr, opts?) — a real wallet adapter can be
  passed as `signer` directly, no shim needed. YieldLadderOptions gained
  publicKey/vaultRouterContractId/assetContractId, since none of these
  existed before and a transaction can't be built without them (nothing
  is deployed yet per deployments/*.json, so these can't be hardcoded).
- withdraw/earlyExit still take just { tier } (no amount) — they query
  the caller's current position first via VaultRouter.position() and
  submit that as a full withdrawal/exit, keeping the existing public
  signature the README already documents. Removed the redundant
  simulate()-then-simulateThenSubmit() double-call in earlyExit.
- @stellar/stellar-sdk moved from peer+dev to a real dependency (still
  declared as a peerDependency too, so a consuming app using the same SDK
  version — e.g. via WalletAdapter — doesn't end up with two copies).
- 15 unit tests mocking only the RPC layer (SorobanRpc.Server,
  assembleTransaction) — real ScVal encode/decode, Tier/Address/amount
  construction, and Position struct decoding are all exercised for real.
  Covers the happy path for all three write methods, AssetNotAllowed/
  BelowMinDeposit/ProtocolPaused/LockNotExpired revert paths, an unmapped
  contract-error fallback, transaction expiry, a non-expiry submission
  failure, and that earlyExit simulates exactly once.
- README's yl.deposit(...) example updated to the real constructor shape
  and is now backed by an executable test rather than aspirational docs.

Closes LadderMine#139
@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

@michaelsimeon001 is attempting to deploy a commit to the wumibals' projects Team on Vercel.

A member of the Team first needs to authorize it.

@wumibals wumibals left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the real Soroban transaction pipeline for the TypeScript SDK (closes #139).

  • This fixes a real gap, not just a refactor: deposit/withdraw/earlyExit were routed through stub methods that never built, simulated, signed, or submitted anything, despite the README documenting them as a working API. TransactionPipeline's build -> simulate -> assemble -> sign -> submit flow makes the documented API actually do what it says.
  • Good diligence verifying Tier/Address/amount encoding against the real installed @stellar/stellar-sdk (reading scValToNative's actual switch statement) rather than assuming the ScVal shape — that's exactly the kind of thing that's easy to get subtly wrong and hard to catch without a real integration test.
  • Replacing the old err.message.includes('lock') string-matching with typed errors parsed from the Error(Contract, #N) string, mapped against vault_router/src/error.rs's actual discriminants, is a meaningful robustness improvement — string-matching on error messages is fragile against any wording change. Correctly punting on code 7 (ambiguous between router and tier-vault error enums) to a generic VaultContractError instead of guessing is the right call.
  • Distinguishing transaction-expiry-while-signing (txTooLate) from a genuine on-chain rejection is a real UX/debuggability improvement — those are very different failure modes for a caller to handle.
  • Aligning the new Signer interface with the app's existing WalletAdapter.signTransaction shape so a real wallet adapter can be passed directly, no shim, avoids the SDK and app drifting into two incompatible signing abstractions.
  • Keeping withdraw/earlyExit's public { tier } signature stable (querying position server-side rather than requiring an amount) preserves the README-documented API instead of introducing a breaking change alongside the internal fix.
  • Test coverage is strong and mocks only the RPC layer, so ScVal encode/decode and Position struct decoding are exercised for real — happy paths, all four named contract-error reverts, the unmapped-code fallback, expiry-vs-rejection distinction, and the earlyExit double-simulate fix are all explicitly covered. The README example is now backed by an executable test instead of being aspirational.

CI is green across Soroban contracts, Next.js dashboard, and TypeScript SDK — and this time the SDK CI job actually runs the test suite, so the 15 new/updated tests are machine-verified. Vercel's FAILURE is the usual unauthorized deployment integration link, unrelated to the code.

Approving — this makes the SDK's core write path real instead of a no-op stub.

@wumibals
wumibals merged commit eaf8cbc into LadderMine:main Aug 22, 2026
3 of 4 checks passed
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.

Payments 2/8: Implement real Soroban transaction pipeline in the TypeScript SDK

2 participants