feat(sdk): add TypeScript SDK and example DApp for the SwapTrade contracts - #264
Open
Cypher-Aura-19 wants to merge 1 commit into
Open
feat(sdk): add TypeScript SDK and example DApp for the SwapTrade contracts#264Cypher-Aura-19 wants to merge 1 commit into
Cypher-Aura-19 wants to merge 1 commit into
Conversation
…racts
Adds a typed client for the Soroban contracts and a small React demo that
drives it, so integrators no longer have to hand-encode ScVals and rediscover
the build -> simulate -> assemble -> sign -> submit -> poll sequence.
packages/swaptrade-sdk
- SwapTradeClient with one method per contract entry point, plus public
buildTransaction/simulate/invoke so an unwrapped method is still reachable.
- ScVal encoding and decoding for the cases where a naive Rust-to-JS mapping
is wrong: Option::None is ScVal::Void and decodes to undefined rather than
0, fieldless enums are one-element vectors, tuples are vectors, and
symbol_short! is capped at 9 characters.
- i128/u128/u64 are bigint in and out; a number is rejected rather than
silently truncated above 2^53.
- Typed errors with a code discriminator, resolving contract error numbers
against the catalogue in counter/src/errors.rs.
- rpcUrl, networkPassphrase, contractId and publicKey are required with no
defaults, because a default passphrase means signing against the wrong
chain. Plain HTTP is refused except on loopback.
examples/swap-demo
- One screen mapping the workflow onto the primitives the contract actually
exposes. Layering is one-directional: no component imports the SDK,
builds a transaction or encodes an ScVal.
- Browser signing goes through the SDK's SignTransaction callback to an
injected wallet. src/signer.ts imports browserWalletSigner only, so no
code path can read a signing key: Vite inlines every VITE_-prefixed value
into the bundle, which would publish a key rather than configure one.
There is no key input field, and no VITE_ secret variable.
- Without a wallet the app degrades to read-only instead of asking for a key.
Verification and CI
- 121 unit tests with no network access, mocked at boundaries: a fake RPC
server at the RpcServerLike seam, a fake client at the SDK boundary, and a
keyless wallet at globalThis.freighterApi. Tests decode the built XDR to
assert on the exact method name and argument list.
- 8 Playwright smoke tests run the production build in real Chromium against
an unreachable RPC port, so they stay self-contained. One fetches the
served JavaScript and asserts no secret-shaped string is in the artifact.
- sdk.yml is additive and path-filtered to packages/ and examples/; the
dormant Rust workflows are untouched. Its secret scan checks the bundle for
a secret-key shape, the bundle for a secret-shaped VITE_ name, and the
demo source for anything reading one.
- scripts/localnet_deploy.sh reduces the 12-step localnet setup to one
command; scripts/verify_localnet.ts exercises both SDK paths against a live
network. It uses keypairSigner, which is safe in Node because the process
is not a public asset, and defaults to --ephemeral: a generated identity,
funded by friendbot and discarded.
No contract was modified. Two limitations are documented rather than worked
around: counter does not compile on a clean checkout at 1b76016 (152
pre-existing errors), so the on-chain verification in docs/LOCALNET.md ran
against soroban-ping, the one workspace member that builds; and there is no
create_swap/fund_swap/accept_swap trio, so the demo maps onto
place_limit_order, mint and execute_due_orders.
Closes StelTade#254
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
Adds a typed TypeScript SDK for the SwapTrade Soroban contracts (
packages/swaptrade-sdk) and a small React demo that drives it (examples/swap-demo). Before this, callingplace_limit_orderfrom an application meant readingcounter/src/lib.rs, working out the positional argument order, and hand-encoding each value into anScVal— including the cases where the naive mapping is wrong. That work is now done once, in a tested layer, instead of once per integrator.No contract was modified. Where the issue's vocabulary had no counterpart in the contracts, I mapped onto the real primitives and documented the mismatch rather than adding contract functions to make the example read neatly.
Closes #254
Type of change
packages/swaptrade-sdk)examples/swap-demo)What changed
SDK —
packages/swaptrade-sdksrc/client.ts—SwapTradeClient: 23 methods mapped to real entry points, plus publicbuildTransaction/simulate/invokeso an unwrapped method is reachable without waiting for a wrapper. Read-only calls simulate (no fee, no signer); state-changing calls simulate → sign → submit → poll.src/scval.ts— encoding and decoding for the places Rust and JS do not line up:Option::NoneisScVal::Voidand decodes toundefinedrather than0, fieldless enums are one-element vectors (order matters — Soroban also encodes by index), tuples are vectors, andsymbol_short!is capped at 9 characters where generalSymbolallows 32.src/errors.ts— 9 typed error classes with acodediscriminator, so callers branch on a value instead of matching message strings.ContractCallErrorresolves a numeric code against the catalogue incounter/src/errors.rs, turningError(Contract, #500)intoKYCVerificationRequired (#500).src/config.ts—rpcUrl,networkPassphrase,contractIdandpublicKeyare required with no defaults. A default passphrase is a signing accident waiting to happen: a transaction signed for the wrong network is a valid transaction for that network. Plain HTTP is refused except on loopback.i128/u128/u64arebigintin and out; anumberis rejected rather than silently truncated above 2^53.Demo —
examples/swap-democomponents.tsxis presentational,workflow.tsis the entire SDK boundary,config.tsis the only reader ofimport.meta.env,signer.tsis the only signing decision.src/signer.tsimportsbrowserWalletSigneronly.keypairSigneris never imported into anything Vite bundles, so the demo has no secret-key code path at all — setting aVITE_*_SECRET_KEYhas no effect because nothing reads it. Vite inlines everyVITE_-prefixed value into the shipped JavaScript, so a key supplied that way is published, not configured.Tooling
scripts/localnet_deploy.sh— reduces the 12-step localnet setup to one command.scripts/verify_localnet.ts— drives the real SDK against a live network. UseskeypairSigner, which is safe here because a Node process is not a public asset, and defaults to--ephemeral: an identity generated, funded by friendbot, and discarded..github/workflows/sdk.yml— additive and path-filtered topackages/**andexamples/**. The three dormant Rust workflows are untouched; re-enabling them is a separate decision.Contract compatibility
swaptrade-contracts/i128/u128/u64values arebigint, notnumberOption::Nonedecodes toundefined, not0Contract entry points this relies on:
counter/src/lib.rs—initialize,mint,balance_of,get_portfolio,place_limit_order,get_order,get_user_orders,execute_due_orders,cancel_order,swap,safe_swap,set_max_slippage_bps,set_price,get_current_price,kyc_submit,kyc_is_verified,kyc_get_status,kyc_update_status,kyc_add_operator,pause_trading,resume_trading,get_user_tier,get_contract_version.Validation
Real output, run on Windows 11 with Node 22:
Secret scan against the freshly built bundle, the same three greps CI runs:
Localnet
scripts/verify_localnet.ts)The full SDK pipeline ran against a real network and the resulting transaction was independently confirmed:
stellar/quickstart:latest,Standalone Network ; February 2017CBKMKKR73AOFQBJSOY55BJVDBMPDIY2XHI5WDHEFX6LVXAFJ3TK4DINHsimulate()"pong"invoke()"pong", statusSUCCESS, ledger 128155d1ddabefb46aa4810cdb1c7d41b48bcb64f08ed8b8ac762401c7c987a94c15getTransactionon that hash →SUCCESS, ledger 1281,applicationOrder: 1That is the meaningful result: a fake RPC server cannot catch a malformed resource footprint, a wrong passphrase, or a signature over the wrong hash. Those only fail against a real node, and they did not fail here.
Not verified
Stating the gaps rather than letting the section above read as a stronger claim than it is.
counterdoes not compile on a clean checkout.cargo check --workspacereports 152 pre-existing errors at1b76016with an emptygit status— unrelated to this change, and this PR touches no.rsorCargo.tomlfile. The consequence is concrete: the localnet walkthrough was executed againstsoroban-ping, the one workspace member that builds. Everycounter-specific method is verified against the contract source and by unit tests, not on-chain.perform_swapis a stub.counter/src/swap.rsruns its safety checks and returnsOk(0)with a// ... rest of swap codecomment.swap()andsafeSwap()are bound in the SDK, but the contract does not yet move balances.create_swap/fund_swap/accept_swapexists. The workflow is mapped ontoplace_limit_order,mintandexecute_due_orders. The demo's step table inexamples/swap-demo/README.mddocuments the mapping.cargo fmt --checkfails pre-existing, for two reasons unrelated to this PR:nftis declared at bothnft.rsandnft/mod.rs, andtrading_comprehensive_test.rs:57has an unknownpositiveprefix.Tests
129 tests total — 121 unit with no network access, 8 in a real browser. Mocks sit at boundaries, not in the middle, so the real code runs:
RpcServerLike(SDK tests)globalThis.freighterApi(browser tests)Tests decode the built XDR to assert on the exact method name and argument list that would reach the contract — a test that mocked the encoder could not catch an argument in the wrong position. Signature tests verify cryptographically: they parse the signed envelope, recompute the transaction hash, and check the signature against the public key.
The browser mock wallet holds no key. It records the request and declines, which is enough to prove the path is wired and that a refusal surfaces to the user.
Checklist
VITE_*secret variable, no key input field, nokeypairSignerimport underexamples/.envfile is committed (.env.examplewith placeholders is fine)node_modules/,dist/,playwright-report/,target/)git diffbefore requesting reviewNo credential-shaped literal exists anywhere in the diff. Tests that need a keypair derive one from a fixed seed (
Keypair.fromRawEd25519Seed(Buffer.alloc(32, 7))), and the Playwright config derives only the public half.Notes for reviewers
Two modified files, both purely additive.
.gitignoregains Node/Playwright entries plus scoped!negations re-including the JSON files the workspace needs to install — the repo's existing*.jsonrule is deliberately broad for generated Soroban artifacts, so a new workspace JSON file needs a negation added.README.mdgains one pointer section and a 4-command quickstart. Everything else is new.On the absent secret-key path. It would have been less code to read a key from a
VITE_variable on localnet, and I had that working before removing it. The reason it is gone: Vite inlines those values into the bundle, so the pattern publishes a key rather than configuring one — and an example that demonstrates a pattern teaches it. Making the path absent rather than discouraged is what makes the guarantee checkable, which is why CI greps the demo's source for anything reading such a variable, not just the bundle. That third grep matters because the first two pass by accident whenever the variable happens to be unset in CI.Where docs go beyond the code.
docs/LOCALNET.mdhas a "What was actually verified" section separating observed results from wired-but-unexecuted behaviour, anddocs/IMPACT.mdends with "What this does not claim". Every number inIMPACT.mdis a count of something in this repository; there are no adoption or performance figures, because none were measured.Reviewable independently: the SDK stands alone and is useful without the demo. If you would rather land it in two passes, the demo, its tests and its docs are separable.