feat(sdk): implement real Soroban transaction pipeline - #147
Merged
wumibals merged 1 commit intoAug 22, 2026
Merged
Conversation
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
|
@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
approved these changes
Aug 22, 2026
wumibals
left a comment
Contributor
There was a problem hiding this comment.
Reviewed the real Soroban transaction pipeline for the TypeScript SDK (closes #139).
- This fixes a real gap, not just a refactor:
deposit/withdraw/earlyExitwere 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/amountencoding against the real installed@stellar/stellar-sdk(readingscValToNative'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 theError(Contract, #N)string, mapped againstvault_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 genericVaultContractErrorinstead 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
Signerinterface with the app's existingWalletAdapter.signTransactionshape 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
ScValencode/decode andPositionstruct decoding are exercised for real — happy paths, all four named contract-error reverts, the unmapped-code fallback, expiry-vs-rejection distinction, and theearlyExitdouble-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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
deposit/withdraw/earlyExitfunneled through two stub private methods (simulate/simulateThenSubmit) that never built, simulated, signed, or submitted anything — callingyl.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/amountare encoded the way Soroban actually encodes them — I verified this against the real installed@stellar/stellar-sdkimplementation rather than assuming:TierasscvVec([Symbol(tag)])(how#[contracttype]enums encode, confirmed by readingscValToNative's actual switch statement),Addressvia.toScVal(),amountasi128.Typed contract errors
Contract-level rejections (
BelowMinDeposit,AssetNotAllowed,ProtocolPaused,LockNotExpired,InvalidTier,DepositCapExceeded) are parsed out of Soroban'sError(Contract, #N)simulation-error string into typed classes extendingYieldLadderError, matchingcontracts/vault_router/src/error.rs's discriminants — replacing the old fragileerr.message.includes('lock')string-matching forwithdraw. 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 genericVaultContractErrorrather 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
txTooLatecode (errorResult.result().switch().name), surfaced asTransactionExpiredError.Signer contract
types.ts's newSignerinterface is aligned withapp/src/lib/wallet/types.ts'sWalletAdapter.signTransaction(xdr, opts?)— a real wallet adapter can be passed assignerdirectly, no shim needed, so the SDK and the app share one signing abstraction instead of two incompatible ones.YieldLadderOptionsgainedpublicKey/vaultRouterContractId/assetContractId— none of these existed before, and a transaction can't be built without them (nothing is deployed yet perdeployments/*.json, so they can't be hardcoded).Public API kept stable
withdraw/earlyExitstill take just{ tier }(no amount) — they query the caller's current position viaVaultRouter.position()first and submit that as a full withdrawal/exit, keeping the existing signature the README already documents. Removed the redundantsimulate()-then-simulateThenSubmit()double-call inearlyExitper the issue's explicit ask.Dependency
@stellar/stellar-sdkmoved from peer+dev to a realdependenciesentry (still declared as apeerDependencytoo, so a consuming app already using the same SDK version — e.g. via its ownWalletAdapter— doesn't end up with two copies bundled).Test plan
pnpm typecheck— passespnpm build— passespnpm test— 15 passing, mocking only the RPC layer (SorobanRpc.Server,assembleTransaction) so realScValencode/decode,Tier/Address/amountconstruction, andPositionstruct decoding are all exercised for real, not faked:deposit/withdraw/earlyExit, including that the configuredSigneris actually invoked with the right argsAssetNotAllowed(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 responsesVaultContractErrorrather than a wrong guesstxTooLate) vs. a plain submission rejection are distinguishedearlyExitsimulates exactly once (previously calledsimulate()thensimulateThenSubmit())position()decodes a realPositionstruct and correctly falls back to the first tier with a non-zero principalyl.deposit(...)example updated to the real constructor shape (now requirespublicKey/vaultRouterContractId/assetContractId) and is backed by an executable test rather than aspirational documentation.Closes #139