[Feat][Docs] Create shield-swap cli + add runnable shield-swap examples - #123
Merged
Conversation
…harge Groundwork for the runnable trader scripts, and it removes a real mainnet trap. `SHIELD_WRAPPERS` named testnet's `test_`-prefixed underlyings while the wrapper program ids are identical on both networks — verified against both deployments: `shield_swap_arc20_wrapped_usdcx.aleo` wraps `test_usdcx_stablecoin.aleo` on testnet and `usdcx_stablecoin.aleo` on mainnet, and the testnet names are 404 there. Record selection reads that table before any network round-trip, so a mainnet caller would have looked for records in a program that does not exist. It is now `SHIELD_WRAPPERS_BY_NETWORK` plus `shieldWrappersFor(network)`, with the old export deprecated and pointing at testnet. Nothing inside the SDK used it, so the blast radius is consumers only. `session.ts` is network-scoped throughout: `resolveNetwork` refuses anything but testnet and mainnet and never defaults to mainnet, state lives under `.shield-swap/<network>/`, and the legacy single-file layout still loads for testnet so existing keys survive. The blinded identity store is per network for a stronger reason than tidiness — a reservation is only meaningful against the chain whose `used_blinded_addresses` it was checked against, so one shared file would hand out identities the other chain has already consumed. `loadSession` now configures that store, which means every swap through these scripts reserves and records automatically. That deletes six helpers the SDK subsumed — `appendSwapHandle`, `removeSwapHandle`, `serializeHandle`, `deserializeHandle`, `isMultiHopHandle`, and the `swapHandles` state field — each of which had a one-for-one replacement. The three runbooks that used them are rewritten, and they get shorter: the claim sweep in collecting.md is now `getUnclaimedSwaps` over the chain instead of a hand-rolled loop over stored handles, and the concurrent-swap recipe in swapping.md drops its manual counter walk for plain `client.swap()` calls. setup.ts takes `--network` and refuses to airdrop on mainnet, where there is no faucet — quietly skipping would leave an account configured but unable to trade, discovered only when a swap could not select a record.
A sweep of session.ts for anything with a direct SDK equivalent, which turned out to be most of what it still held. `buildDexImports` was `resolveDexImports` byte for byte — same token-program loop, same static-import regex — and that is a client action since #122. `getHoldings` was `getBalances`, which already reconciles the registry against records and public balances and carries symbol and decimals, so the wrapper only reshaped it into an array. `credentialStoreFor` is `fileCredentialStore` from the SDK's node entry point. `PROVER_URL` and `SCANNER_URL` were re-exports of defaults the client applies anyway. `jsonSafe` existed for the bigints in swap handles, which the identity store now owns. session.ts drops from 349 lines to 291 with nothing lost. Credentials move out of the state file into `provable-credentials.json` beside it, per network. setup.ts migrates an existing `state.provableApi` into that file rather than letting the client register a replacement — an API key is issued once and cannot be reissued, so a fresh consumer would abandon the old one. Both prover and scanner URLs are now unset, so each takes the network from the client instead of a constant that could drift from it — which matters more now that mainnet is in scope. Correcting something I claimed earlier: setup.ts already called `authenticateProvableApi`, so Provable API credentials were already verified eagerly rather than on first prove. Only the storage was open. All four runbooks are updated to the SDK actions, including reshaping their snippets from the array `getHoldings` returned to the token-keyed map `getBalances` returns.
…ally `TrackedPosition`, the `positions` array, and `appendPosition` are gone. `getOwnedPositions` returns a superset of what they held — positionTokenId, poolKey, both token ids, the tick range, the withdrawal address, the frozen flag, and the joined chain state — discovered from the records the account holds, so a local list could only ever be a stale copy. collecting.md already conceded the point: its loop over `state.positions` was preceded by a comment explaining how to rebuild the array when it was "missing or stale". It now iterates discovered positions directly and derives token programs from the registry by token id, which the array was storing to avoid. liquidity.md no longer persists anything after a mint, because there is nothing a later run cannot rediscover. SKILL.md's one-script-at-a-time rule stands but has little left to protect: handles belong to the identity store, which serializes its own writes. session.ts is 277 lines, down from 349 at the start of this branch, and the state file now holds only what genuinely cannot be recovered from chain — the private key and the DEX grants tied to it.
`resolveToken` and `listTokens` take what a person types — `USDCx`, any case — or what an action takes, a token id, and resolve against the client's own network, because symbols are per network and testnet's registry is not mainnet's. Cached per API client so twelve scripts asking the same question cost one request, and a failed lookup is deliberately not cached so one transient error cannot poison the process. Ambiguous symbols are refused rather than guessed, and an exact id beats a symbol collision. `cli.ts` holds the conventions every script shares and nothing else: unknown flags are an error rather than a silent default, `--json` prints one object and silences progress, `--network mainnet` is never implicit, and `confirmed()` prints the plan and stops unless `--execute` was passed, so a first run is always safe. Progress narration is not decoration — these scripts wait tens of seconds on proving and confirmation, and without it a human cannot tell slow from hung. Four read scripts, each self-contained and safe to run: pools.ts joins the index with chain state because the API can list a pool the contract refuses to trade, balances.ts shows both sides and says which one funds trades, positions.ts discovers positions from records, and swaps.ts covers status, --reconcile, and --claim. Two honest-messaging bugs found by running them rather than reading them. positions.ts labelled a null chain state "pending finalize", which is the exact ambiguity documented on getOwnedPositions: a burned position reads the same way while the scanner still serves its record, and it can do so for minutes. And swaps.ts told an empty store that every swap it knew about had been claimed — "nothing tracked" and "nothing owed" mean very different things to someone hunting for missing funds. Verified live on testnet: setup bootstraps into a network-scoped state dir with credentials in their own 0600 file, pools lists five chain-verified pools, balances renders three tokens at the right decimals, positions flags eight records with no mapping entry, and --json emits one object with no progress on stdout.
…ters Running the new swap script against testnet reverted a transaction, and the cause was in the SDK rather than the script. `ApiClient.getRoute` typed `amount_in` as `bigint`, implying the base units everything else here takes; the endpoint wants a decimal string in the input token's units and answers in the output token's. Measured both ways against the live API: `amount_in=0.5` quotes `0.000268655644950769` ETH, and `amount_in=500000` — the same half-token in base units — quotes `1.030419082712717843`, the pool's entire depth. Following the type therefore builds a slippage floor three orders of magnitude above any real fill, and the swap reverts on finalize with the fee consumed. That is what it cost. The agent tool was wrong twice over: the handler called `BigInt()` on the value, which throws on `'0.5'`, and the schema instructed agents to pass "raw base units (u128)" — the exact instruction that produces the revert. An agent following our own documentation would have lost money. `parseUnits` and `formatUnits` do the conversion, named after viem's equivalents and working on the string, because a double cannot hold the 18 significant decimals these quotes carry. `planSwap` composes the whole thing: resolve both tokens, route through the API, check every hop on chain for tradeability and liquidity rather than trusting the index, convert the quote, derive the floor, and assemble the imports for every token the route touches. Its test asserts the request carries `'0.5'`, which is the regression that would have caught this. swap.ts drives it end to end and claims in the same run, because leaving the claim for later is how proceeds get forgotten. Verified live: 0.5 USDCx to 0.000268655644950769 ETH, claimed on the first attempt, received exactly the quote.
The whole point of the reservation work: two `client.swap()` calls in parallel, with nothing to coordinate. Each reserves its own blinded identity from the store and reservations serialize, so the collision that used to revert the second swap cannot happen. The hazard the store does not cover is records, so the script refuses two legs that sell the same token rather than letting the chain reject one as a double-spend — selection picks one record per swap, not the sum of several. Every leg is planned before any is submitted, so an unroutable or unaffordable leg surfaces while nothing has been spent, and submission uses allSettled rather than all, because one rejection must not abandon the outputs that landed. Verified live: 0.2 USDCx → ETH and 0.5 ALEO → ETH submitted together, both landed, both claimed on the first attempt. The second routed through two hops, so planSwap's multi-hop path and swapMultiHop are exercised as well.
…cile `reconcileSwapHistory` matches claim history against addresses the store already holds, so against an empty store it finds nothing — which defeats the case it was meant to serve. The missing half is discovery, and it belongs in the script rather than the action: sync stays cheap and single-purpose, and the SDK already exports every primitive the walk needs. A blinded identity is derived, not recorded. Nothing on chain lists an account's identities and the account cannot enumerate its own, so the only way back is to re-derive. `discoverIdentities` walks counters forward from the store's tip, deriving each address and asking `used_blinded_addresses` whether this account spent it. Hits extend the window, because gaps are normal — a reverted or dropped swap burns a counter without consuming its address — and a ceiling stops a wrong view key from walking forever. `--reconcile` now does discovery first, then the history walk, so one command rebuilds a store from nothing. Verified by deleting a store with four records and rebuilding it: 36 identities recovered by derivation, 22 swap ids attached from 8 pages of history, 14 consumed identities correctly reported as unlocatable, and the run advised a larger --pages because history was not exhausted. Counter 33 was rightly absent — it had been reserved but never spent, so no address of it exists on chain. Handles are not recoverable: a claim call names the swap but not the whole preimage a claim consumes. So a recovered identity with unclaimed proceeds is visible but not claimable, which collecting.md now states as the reason to keep a durable store rather than lean on recovery.
…the ledger Four changes, three of them from watching it run against a real account. Renamed from swaps.ts: the script's subject is the account's swap history, and `swap.ts` sitting beside `swaps.ts` invited exactly the mistake of running the wrong one. The history walk fetches a page's claims concurrently and retries what the node refused for being busy. Pages stay sequential because each needs the previous cursor, but the per-claim transaction fetches are independent and are the bulk of the work — a page of 50 means 50 requests. A 429 or a 5xx says nothing about the request and a long walk is the traffic shape that trips one, so those back off and retry rather than discarding the page; a 404 is an answer and is not retried. Measured: 972 calls across 21 pages in under 12 seconds. The ledger now prints whatever else happened. A failed claim or an exhausted page budget still leaves a picture worth seeing, and someone hunting for funds needs every identity rather than a summary — including a NO HANDLE marker, since a handle is what makes an unclaimed swap claimable. And a bug of mine: the tracked-identity count was read at startup, before `--reconcile` could populate the store, so one run reported "this store tracks no identities" beside a live count of 14 unresolvable ones. It is now read after discovery. The same output also claimed "all settled" when some identities could not be checked at all — unknown is not settled, and it now says which. Discovery already worked as intended and is unchanged: consult the store, start from the last known counter, and keep probing forward while hits keep arriving.
…e-searching Three changes, and the third exists because the first two would otherwise make every run pay for the same answer. The history walk is now unbounded by default. That account's entire program history is 21 pages, which the concurrent walk covers in about 11 seconds — and walking all of it recovered 32 claims where the previous 8-page default found 22. `--pages` still bounds it. A cursor the endpoint hands back unchanged now ends the walk, because unbounded paging that is not advancing would otherwise not return. Claims are persisted as they are found, and the script collates them into a summary: swaps per pair, totals received per token, and refunds of unfilled input. Persisted rather than recomputed because a claim deletes the `swap_outputs` entry it settles, so the claim transaction is the only remaining record of what a swap moved — and re-deriving that would mean re-walking history on every run. Which is the third change. A record whose claim a *complete* walk did not find is marked `claimSearched`, because that is an answer — the swap was never claimed — and the script now searches only when something is genuinely missing. Measured on the same account: 11s when four ids were outstanding, 5.9s on the next run with nothing left to look for. `--reconcile` forces a re-search anyway, `--no-search` refuses one. The advice attached to unresolved identities depends on which happened. After a complete walk it says those swaps were never claimed and their output cannot be claimed without the handle; after a bounded one it says to finish the walk before concluding anything. Telling someone to "look further back" when there is no further back is worse than saying nothing.
Replaces the per-identity list with a table of what each swap actually did: pair, sold, received, block, status. Columns are sized to their contents and the numeric ones right-aligned, because a 6-decimal USDCx amount beside an 18-decimal ETH one is unreadable otherwise. Sold comes from the stored handle and received from the stored claim, and either can be absent — a recovered identity has neither. Both are nullable rather than defaulted to zero, which would read as "sold nothing" instead of "not known". A partial refund shows inline as `(+n back)` rather than as its own column, since most swaps fill completely and an always-empty column costs width on every row. The note column carries the two states worth acting on: `no handle` when a swap is unclaimed and cannot be claimed from here, and `never claimed` once a complete history walk has confirmed no claim exists. Run against a rebuilt store, the sold column is empty for all 36 rows — the store was wiped, so its handles went with it, which is the case for keeping one rather than relying on recovery.
Answering "why does it say swapped but never claimed": `used_blinded_addresses` is written by `finalize_swap`, not by the claim, so those identities are swaps that landed and were never collected. Their output is still in `swap_outputs`. I had said they were unreachable because a claim needs the whole handle. That was wrong, and the contract source says so: a swap request publishes `pool`, `zero_for_one`, `amount_in`, `amount_out_min`, `sqrt_price_limit`, `nonce`, `deadline`, and both token ids as public inputs, with the swap id as a public output. Multi-hop publishes its SwapHop structs the same way. The only private piece is the blinding factor, which is derived locally from the view key and counter the store already holds. The history walk now reads requests alongside claims and rebuilds a handle from each, in the same pass. It also records `soldAmountIn` — the amount sold, which no claim reports and which the table showed as an empty column until now. Two bugs of mine along the way. Multi-hop positions differ from single-hop, and reading the blinded address before branching found the record input instead, which has no value and silently dropped every multi-hop request. And `--claim` with an optional value is ambiguous to parseArgs, so `--claim --execute` errored rather than claiming; it is now a boolean with `--swap-id` to narrow it. Verified end to end by rebuilding a store from nothing and claiming what it found: 36 identities recovered, and four abandoned swaps collected — 0.143939 USDCx and 4.068448 ALEO, two of them multi-hop.
…turn Reading swap requests made every `swap` call a fetch candidate, and a call the listing names but whose transaction comes back empty threw on `.execution`. A missing transaction tells us nothing about the identity, so the walk skips it rather than abandoning the page — which is also what two tests were asserting by supplying calls without bodies.
The reported symptom was a history table with most rows blank — pair, sold, received and block all empty on identities marked `claimed`. The cause: the walk selected candidates by status, skipping anything already `claimed`. A record marked claimed carries no economics if it was marked by a claim this process made, or by a version that predated persisting them, so those rows could never be filled in: the walk would not look at them again. Candidates are now chosen by what is actually absent — no `claim` means look for its claim, no handle and no `soldAmountIn` means look for its request — and the two are tracked separately, because a request says what a swap cost while only a claim says whether it settled. A swap id already on file wins over one from a request, since a claim is the authority on which swap settled. `claimSearched` now excludes a record from both searches and is written for anything a complete walk did not find, whatever its status. Without that, a reserved counter that was never spent would send every later run through the whole history on its behalf. Verified against the exact reported state — 36 identities marked claimed with no economics and no handles: the walk recovered 36 claims and 36 requests, and every row of the table now carries its pair, amount sold, amount received, and block. It had previously recovered 32, skipping the four it had claimed itself. Also renames the probe line to say "past the last known swap id", which is what the window counts from.
The scripts under packages/shield-swap/skills/scripts/ shipped as raw TypeScript inside the SDK tarball and ran with `npx tsx` from a path inside node_modules. They are now subcommands of a `shield-swap` binary in its own package, compiled and typechecked like the rest of the workspace. Separate package rather than a `bin` on the SDK: a `bin` entry installs a node_modules/.bin symlink for every consumer, so a frontend that only wanted the client would get a CLI it never asked for. @provablehq/shield-swap-sdk no longer ships skills/scripts/ at all — its tarball is dist plus the runbook markdown. Each script became a module exporting main(argv); src/registry.ts declares the subcommands and lazily imports them, so `shield-swap pools` never loads the proving stack. swap-history is now `history`; every other name, all flags, and the --execute and --json contracts are unchanged. Two behaviour fixes found while moving: - `setup --help` bootstrapped the account instead of printing help. It parses its own arguments and had no --help branch, so the first thing it did was read and migrate the state file. - --json is now set by the dispatcher rather than by each command, so a command cannot report progress before reaching its own setJsonMode and break the one-object-on-stdout promise. The package tsconfig includes src only, so these files were never covered by `tsc --noEmit`, and CI never typechecked workspace packages at all — only whatever a test happened to import. Adds a "Typecheck packages" step, which is what makes the move worth anything.
examples/shield-swap-swap.ts and examples/shield-swap-liquidity.ts still read `wrapper_program` on a type that has carried `amm_token_program` for some time. Nothing caught it: no package tsconfig includes examples/, and the files are gated on credentials CI never sets, so they neither compiled nor ran. Replaces them with eight single-subject files. setup-client.ts walks the whole bootstrap — account, Provable API credentials, client, DEX session, invite code, faucet — and the rest are one flow each: pool-state, quote, balances, swap-history, swap, mint, liquidity. They call client.planSwap(), client.swap() and the rest directly rather than through wrappers, so the examples read as the API instead of a layer over it. examples.test.ts runs them in three tiers — reads needing no account, reads needing an account but spending nothing, and the ones that move funds behind a separate VEIL_EXAMPLES_SPEND opt-in. The test scaffolding lives there rather than inside the examples, so what a reader copies is what runs. Wires them into both CI paths, which is the point: examples/shield-swap/ typechecks on every PR, and the read tiers run against live testnet in live-api.yml so the next field rename fails a build instead of sitting in the docs. The typecheck is scoped to this directory because the older examples at the root of examples/ still have pre-existing errors.
…eads Pre-existing work on this branch, committed separately from the CLI extraction because it stands on its own — though the CLI and the new examples depend on it and neither typechecks without it. - previewMint: what a mint would open, priced against live pool state — where the range lands after alignment, the liquidity the deposit backs, and how much of each side the range actually consumes. - Q128 helpers take options objects, with the positional overloads kept and deprecated rather than removed. - getOwnedPositions reports records with no positions-mapping entry, so a mint mid-finalize and a burned position are distinguishable from one another. - resolveToken is now tokenData. - Delegated-proving and scanner logs are silenced at load.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
0.11.6 adds a consensus version. The devnode height lists have to match the SDK's count exactly and mirror each other, so DEVNODE_CONSENSUS_HEIGHTS in provable-sdk and the CONSENSUS_VERSION_HEIGHTS default in devnode both grow from 17 entries to 18. A short list panics with an opaque wasm `unreachable` inside getOrInitConsensusVersionTestHeights, which is how this surfaced: six createDevnodeClient unit tests failed with no other signal. The 0.2.0 devnode binary could not start against 18 versions, so the toolchain had to move too. aleo-devnode now ships as the @provablehq/aleo-devnode npm package (0.2.3), which is a better fit than a GitHub release: pnpm install provides it, the version is pinned in package.json like any other dependency, and CI stops downloading a zip. Installing it while the composite action also put a release build on PATH would have left precedence implicit, so the action no longer installs it at all and the DEVNODE_RELEASE pin is gone. leo still comes from a release. startDevnode still resolves the binary from PATH and still accepts devnodePath, so a consumer pointing at their own build is unaffected. Also fixes the lockfile that was failing every CI job at `pnpm install --frozen-lockfile`: dropping peerDependencies from shield-swap-cli left pnpm-lock.yaml claiming a specifier the manifest no longer had. Verified: unit suite green, and the four devnode e2e files pass against devnode 0.2.3 with leo 4.3.4.
The Typecheck packages step failed on packages/codegen with "Cannot find module '@provablehq/veil-core'". codegen and react do not extend the root tsconfig, so they get none of its `paths` and resolve workspace imports through node_modules to dist — which does not exist on a clean checkout. It passed locally only because dist was already built. Reproduced by clearing packages/*/dist, which produced the same 16 errors, and confirmed `pnpm -r build` clears them.
leo 4.4.0 removes `self.caller`, `self.signer`, and `self.address` in favour of `std::ctx::caller()`, `std::ctx::signer()`, and `std::ctx::addr()`. The inline Leo in the devnode e2e tests and the arc-0020 example programs all used the removed spellings and fail to compile on 4.4.0. The new syntax is accepted by 4.3.4 as well — it landed earlier as the forward-compatible form and 4.4.0 only dropped the old one — so this migration is backward compatible and does not force anyone off the pinned compiler. Verified: the devnode suite passes on both 4.3.4 and 4.4.0 with these sources. LEO_RELEASE stays at 4.3.4 deliberately. The amm-v3 contracts carry 293 `self.*` usages including the deployed src/main.leo, so a repo-wide move to 4.4.0 would make the AMM lifecycle e2e uncompilable for anyone following the pin — and CI would not catch it, because that job skips without the amm-v3 checkout. Bump the pin once amm-v3 has migrated.
claude-update-deps.yml built against leo-lang-v4.3.2 while ci.yml pinned 4.3.4. That workflow's job is to verify dependency bumps against the devnode suite, so compiling on an older compiler than CI uses can green a bump that CI then rejects.
planSwap takes a client, hits the network, and produces what a write would do — the same shape as previewMint, which already sits in actions/liquidity. utils/ is where the pure helpers live (tick math, unit conversion, q128), so a client-bound composite reads as misfiled there. Pure move: the file is unchanged apart from import depth, and the public export stays a named re-export from src/index.ts, so nothing changes for a consumer. The test moves alongside it.
There was a problem hiding this comment.
Pull request overview
This PR splits the Shield Swap “trader scripts” out of @provablehq/shield-swap-sdk into a new @provablehq/shield-swap-cli package (shipping a shield-swap binary), rewrites the Shield Swap examples to match the current SDK/API surface, and tightens CI so packages/examples are actually typechecked and exercised against live testnet where intended.
Changes:
- Introduce
@provablehq/shield-swap-cliwith a lazy-loaded subcommand registry, shared CLI safety plumbing (--execute,--json), and initial commands (swap, concurrent swap, balances, positions, etc.). - Add/adjust Shield Swap SDK utilities and actions (
planSwap,tokenData/listTokens,parseUnits/formatUnits,previewMint, closed-position detection) and update agent schemas/handlers for route-quote decimal units. - Update CI/tooling: typecheck workspace packages + Shield Swap examples, wire live-api runs for the new examples, and switch devnode installation/pinning to
@provablehq/aleo-devnodevia npm.
Reviewed changes
Copilot reviewed 95 out of 97 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| vitest.config.ts | Add aliases so tests resolve Shield Swap SDK sources (and /node subpaths) instead of stale dist. |
| tsconfig.json | Add paths entries for Shield Swap SDK and Veil Aleo SDK /node entrypoints. |
| scripts/check-upstream-versions.sh | Update devnode version checks to use npm package pin/installed binary. |
| README.md | Document the new @provablehq/shield-swap-cli package in the repo overview. |
| pnpm-lock.yaml | Lockfile updates for @provablehq/aleo-devnode and @provablehq/sdk@0.11.6. |
| packages/shield-swap/test/utils/units.test.ts | Add tests for string-based unit parsing/formatting. |
| packages/shield-swap/test/utils/tokens.test.ts | Add tests for token registry mapping, caching, and token resolution. |
| packages/shield-swap/test/agent/agent.test.ts | Fix and test agent route quoting to keep decimal units as strings. |
| packages/shield-swap/test/actions/swap/planSwap.test.ts | Add tests for planSwap routing, quoting, imports, and failures. |
| packages/shield-swap/test/actions/reads/ownedPositions.test.ts | Add tests for “closed/burned position” detection logic. |
| packages/shield-swap/test/actions/blinding/reconcileSwapHistory.test.ts | Expand swap-history reconciliation tests (handles, paging, retries, concurrency). |
| packages/shield-swap/src/utils/units.ts | Add parseUnits/formatUnits helpers for precise decimal ↔ base unit conversions. |
| packages/shield-swap/src/utils/tokens.ts | Add cached token registry utilities (listTokens, tokenData). |
| packages/shield-swap/src/utils/records.ts | Extend listPositionNFTs to support scanning spent records. |
| packages/shield-swap/src/utils/blinding/store.ts | Persist claim economics + “claim searched” metadata for reconciliation stability. |
| packages/shield-swap/src/index.ts | Export new helpers/actions and remove the unused SHIELD_WRAPPERS export. |
| packages/shield-swap/src/decorators/shieldSwapActions.ts | Expose new actions/utilities (tokenData, listTokens, planSwap, previewMint). |
| packages/shield-swap/src/constants.ts | Remove the SHIELD_WRAPPERS table. |
| packages/shield-swap/src/api/client.ts | Correct getRoute to accept decimal amount_in as a string + document units. |
| packages/shield-swap/src/agent/schemas.ts | Update agent schema wording to reflect decimal route-quote units. |
| packages/shield-swap/src/agent/handlers.ts | Update handler to pass decimal amount_in through as string (no BigInt). |
| packages/shield-swap/src/actions/swap/planSwap.ts | New planSwap action to build an executable swap plan from API+chain checks. |
| packages/shield-swap/src/actions/reads/getOwnedPositions.ts | Add includeClosed option and “closed” detection via spent-record scan + missing mapping. |
| packages/shield-swap/skills/swapping.md | Update runbook snippets to use CLI session helpers and new SDK surfaces. |
| packages/shield-swap/skills/startup.md | Update startup instructions to use npx @provablehq/shield-swap-cli setup. |
| packages/shield-swap/skills/SKILL.md | Update skill doc to reflect CLI + store-based persistence model. |
| packages/shield-swap/skills/liquidity.md | Update liquidity runbook snippets to use CLI session helpers + resolveDexImports. |
| packages/shield-swap/skills/developing.md | Point “runnable examples”/onboarding references to the new examples + CLI. |
| packages/shield-swap/skills/collecting.md | Rewrite collecting sweep docs around getUnclaimedSwaps + store reconciliation paths. |
| packages/shield-swap/README.md | Add CLI mention, link to rewritten examples, and document previewMint. |
| packages/shield-swap/package.json | Adjust published files list and bump @provablehq/sdk peer/dev dependency range. |
| packages/shield-swap/AGENTS.md | Add contributor constraint: CLI updates must ship with SDK action/signature changes. |
| packages/shield-swap/.gitignore | Ensure .shield-swap/ state/store isn’t accidentally published with the package. |
| packages/shield-swap-cli/tsup.config.ts | New build config for CLI bundling (esm + dts). |
| packages/shield-swap-cli/tsconfig.json | New CLI tsconfig extending the root config. |
| packages/shield-swap-cli/test/shared.test.ts | Add tests for shared CLI conventions (--execute, --json, unknown flags, etc.). |
| packages/shield-swap-cli/test/registry.test.ts | Add tests ensuring every declared command is loadable and exports main(argv). |
| packages/shield-swap-cli/src/registry.ts | New command registry + top-level usage text. |
| packages/shield-swap-cli/src/index.ts | New CLI dispatcher that sets JSON mode before command execution. |
| packages/shield-swap-cli/src/commands/swap.ts | Implement swap subcommand (plan → submit → optional claim) using planSwap. |
| packages/shield-swap-cli/src/commands/swap-concurrent.ts | Implement concurrent swaps subcommand with safety checks. |
| packages/shield-swap-cli/src/commands/positions.ts | Implement positions discovery/printing using getOwnedPositions + token registry. |
| packages/shield-swap-cli/src/commands/balances.ts | Implement balances printing with decimal-point-aligned columns. |
| packages/shield-swap-cli/README.md | Document CLI usage, rules, command set, and operational caveats. |
| packages/shield-swap-cli/package.json | New published package metadata (bin + session export). |
| packages/provable-sdk/test/integration/devnodeWrite.e2e.test.ts | Update Leo source for new caller APIs (std::ctx::*). |
| packages/provable-sdk/test/integration/devnodeE2e.test.ts | Update test expectations/comments to match std::ctx::caller(). |
| packages/provable-sdk/src/index.ts | Silence upstream SDK logs on load/switch and extend consensus heights list. |
| packages/provable-sdk/package.json | Bump @provablehq/sdk dependency to ^0.11.6. |
| packages/devnode/src/index.ts | Extend devnode consensus version heights default to match new SDK count. |
| package.json | Pin @provablehq/aleo-devnode in root devDependencies. |
| examples/shield-swap/tsconfig.json | Add examples tsconfig for dedicated typechecking. |
| examples/shield-swap/swap.ts | New end-to-end swap example using planSwap. |
| examples/shield-swap/swap-history.ts | New outstanding-swaps example using getUnclaimedSwaps. |
| examples/shield-swap/setup-client.ts | New bootstrap example (account + provable creds + session + access + faucet). |
| examples/shield-swap/quote.ts | New quote-only example using planSwap. |
| examples/shield-swap/pool-state.ts | New read-only pool state example combining API discovery + chain controls. |
| examples/shield-swap/mint.ts | New mint example using previewMint then mint. |
| examples/shield-swap/liquidity.ts | New liquidity lifecycle example (decrease → poll → collect). |
| examples/shield-swap/examples.test.ts | Tiered live-testnet runner for the new examples. |
| examples/shield-swap/balances.ts | New balances example demonstrating private/public holdings. |
| examples/shield-swap-swap.ts | Remove old Shield Swap swap example (replaced by new examples set). |
| examples/shield-swap-liquidity.ts | Remove old Shield Swap liquidity example (replaced by new examples set). |
| examples/arc-0020/wrapped_token_registry/src/main.leo | Update Leo code to use std::ctx::* APIs. |
| examples/arc-0020/wrapped_credits/src/main.leo | Update Leo code to use std::ctx::* APIs. |
| examples/arc-0020/token_registry/src/main.leo | Update Leo code to use std::ctx::* APIs. |
| examples/arc-0020/dummy_exchange/src/main.leo | Update Leo code to use std::ctx::* APIs. |
| AGENTS.md | Update repo-level package table entry for the CLI and devnode install behavior. |
| .github/workflows/live-api.yml | Run the new Shield Swap examples test against live testnet. |
| .github/workflows/claude-update-deps.yml | Align workflow with new devnode/npm distribution approach; update Leo pin. |
| .github/workflows/ci.yml | Add package/examples typecheck steps; remove devnode release pin usage. |
| .github/actions/setup-devnode/action.yml | Update devnode setup action to install leo only; use npm-provided devnode. |
| .claude/skills/update-dependencies/SKILL.md | Update dependency bump guidance for devnode now being npm-distributed. |
| .changeset/shield-swap-cli-package.md | Changeset for new CLI package + SDK packaging change. |
| .changeset/route-quote-decimals.md | Changeset documenting route quote unit fix + new helpers/actions. |
| .changeset/remove-shield-wrappers.md | Changeset documenting SHIELD_WRAPPERS removal. |
| .changeset/recover-abandoned-swaps.md | Changeset documenting enhanced swap-history reconciliation. |
| .changeset/q128-options-objects.md | Changeset documenting Q128 helper API shape changes + liquidityForAmount. |
| .changeset/preview-mint.md | Changeset documenting new previewMint. |
| .changeset/config.json | Register new CLI package in changesets config. |
| .changeset/bump-provable-sdk-0-11-6.md | Changeset documenting @provablehq/sdk bump and devnode height implications. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The README told contributors to build first and run dist. tsx honours the root tsconfig's paths, so the SDK resolves to source and no build is needed — edits take effect immediately. Adds a `pnpm shield-swap` script for that path. Notes the banner trap: `pnpm run` writes two lines to stdout before the command's own output, which is harmless for a person and breaks `--json` for anything parsing it. `pnpm -s`, or calling tsx directly, both stay clean.
… commit `packages/shield-swap/docs/market-maker-guide.md` arrived in 538c1f2 alongside the CLI extraction, but it is not documentation for this package: it is a set of revised code sections written against a market maker's own integration guide. It was the only file in `packages/shield-swap/docs/`, so that directory goes with it. Nothing referenced the file, so no link breaks. It carries no credentials — the one line mentioning a secret describes where an API token is shown, rather than containing one.
`changeset status` planned a MAJOR bump for all nine published packages, which would have released 1.0.0 instead of 0.7.0. Nothing asked for it: of the twenty pending changesets, fourteen are minor and three patch on shield-swap, four minor on veil-aleo-sdk, two on veil-core, one each on devnode and the CLI. The cause is the peer ranges. `veil-aleo-devnode`, `veil-aleo-sdk` and `veil-aleo-wallet-adapter` peer-depend on `veil-core` at `workspace:^`, and on a 0.x version a caret admits only patches — so taking core to 0.7.0 puts it out of range for its own peer dependents. Changesets bumps an out-of-range peer dependent as major, and the `fixed` group of all nine carries that to every package, including ones with no changeset at all. The ranges are now `workspace:>=0.6.0 <1.0.0`: a 0.x minor stays inside them, so `onlyUpdatePeerDependentsWhenOutOfRange` (already enabled) leaves the dependents alone and the declared minors apply. Verified by running `changeset version` against a snapshot and restoring: the plan is now minor for all nine and lands on 0.7.0, where before it produced 1.0.0. The floor is the current version rather than the next one, so the range is satisfiable both before and after the bump; it only needs raising when a package actually requires a newer core. The ceiling is what keeps 1.0.0 a deliberate decision instead of a side effect.
Colour carries meaning rather than decoration, applied where a reader would
otherwise have to parse the text: progress lines recede (dim), a completed step
marks green, warnings yellow, failures red, and `MAINNET — real funds` takes red
and bold together — the one thing that must not be skimmed past. Status cells
follow the same rule: `tradeable`/`open` green, `pending` yellow, `FROZEN` red,
`closed` and absent values dim. Amounts stay uncoloured; a green number invites a
reading ("good", "gain") the figure does not carry.
No dependency. `util.styleText` strips codes when the stream is not a TTY and
honours NO_COLOR and FORCE_COLOR, so pipes, CI, and `--json` come out plain with
no check of our own. `--no-color` covers a TTY that wants none.
`table()` had to stop using padEnd/padStart: an escape code occupies bytes but no
columns, so colouring a status cell would have shifted every column right of it by
the width of the codes. Widths are now measured with the styling stripped, which
is verified by rendering with and without colour and asserting the two are
byte-identical once codes are removed.
`-h` now works on every subcommand. `parseArgs` rejects undeclared short options,
so it printed the help on the dispatcher and failed with exit 64 on all eleven
commands — declaring `short: 'h'` on the shared flag fixes them together. Help
screens are coloured structurally at print time rather than in twelve usage
strings, so those stay greppable, diffable, and safe to embed in a JSON error.
A bare `shield-swap` now exits 0 rather than 64. Listing the commands is what
someone running it with nothing wants, and EX_USAGE made the wrapper report
`ELIFECYCLE Command failed`, which reads as a broken install. A wrong command
still exits 64.
Two renderings were wrong rather than merely plain:
- `pools` with a filter that matched nothing printed a bare header and rule,
which reads as a rendering fault. It now says so, and names the filter.
- `balances` amounts are flush left. They had been padded on the decimal point,
which in a left-aligned column becomes a visible indent — the points lined up
while the numbers started at three different places.
Command summaries reworded, and the history status column renamed: `claimed` is
green and an unclaimed settled swap reads `pending claim` in yellow. `reserved`
is deliberately NOT folded in — nothing was ever spent on it, so calling it
pending would assert funds are waiting where none are.
`planSwap` computed its floor as `expectedOut * BigInt(10_000 - slippageBps)`, which fails three separate ways on a value the CLI could hand it, all reachable from `Number(someFlag)`: - `NaN` and any fraction throw a RangeError from `BigInt` — a mistyped `--slippage abc` surfaced as a type error rather than as a bad flag; - above 10000 the multiplier goes negative, so `minOut` does too; - below 0 the floor rises ABOVE the quote, so every swap reverts for demanding more than the pool offers — the one case that plans cleanly and fails at finalize. The check runs at the top of `planSwap`, ahead of the token lookup and the route fetch, since neither is worth spending to reject a flag. `--slippage` is also validated in the CLI through one shared `basisPoints` helper, so `swap` and `swap-concurrent` cannot drift on what counts as valid and neither builds a session first. `parseArgs` still intercepts a bare `--slippage -10` as a short option; `=-10` reaches the validator. Tests cover the four bad values — asserting the route was never requested — and the 0/10000 boundaries. Also fixes examples/shield-swap/mint.ts, which printed the amounts the range consumes and then minted with the budget it had previewed against (`amount0Desired` rather than `amount0`), contradicting both its own output and previewMint's documented usage. Passing the consumed amounts caps the mint at what was printed even if the price moves between the read and the finalize. Raised by Copilot on #123.
…correct SKILL.md The runbooks invoked `npx @provablehq/shield-swap-cli <command>`, which names the package rather than the binary and so resolves against the registry on every call: the version can change between two commands in one session that is holding funds, and it does not work without a network. `startup.md` now installs the command first and every example reads `shield-swap`, with `npx ` or `pnpm ` as the prefix for a project-local install or the repo. The same substitution applies in SKILL.md, collecting.md, the CLI README, and the migration note in the changeset, where it is the instruction people will actually follow. (Nothing published yet either way — `npm view @provablehq/shield-swap-cli` is a 404, so every one of those lines would have failed outright.) SKILL.md also said the command "ships beside" the SDK. It does not: they are separate installs, and the SDK no longer carries `skills/scripts/` at all. It now names both packages and what each is for. And it framed building as a fallback — "a scratch script, when a flow needs something the flags do not express". Implementation is a first-class path, so there are now three: the command for operating an account, a script sharing the session for a sequence the flags cannot express, and an integration that owns its own client and does not depend on the CLI. A new section shows the client construction with the identity-store warning attached, and the browser contrast. That snippet was wrong on first writing — `privateKeyToAccount` and `createRemoteScanner` are not top-level exports, because the WASM is per network and both come off the `loadNetwork` handle — so it is taken from session.ts, which is working code, and compile-checked under `tsc --strict` as written.
`shield-swap setup` failed with `DEX API 404 on /auth/challenge: response 404 (backend NotFound)`, which reads as a service outage. It was not: the testnet state file carried `apiUrl: https://amm-api-staging.dev.provable.com`, a retired deployment, migrated in from the pre-network-scoped state file along with its `positions` and `swapHandles` fields. A pinned host overrides the per-network default for every call, and nothing in the failure pointed at it. An authentication failure with a host pinned now says which host, whether the pin came from `SHIELD_SWAP_API_URL` or from `apiUrl` in the named state file, and how to re-pin or clear it. The underlying error stays attached as `cause`. Unpinned sessions are untouched — there is nothing extra to say about the default.
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.
The trader scripts move out of the SDK into their own package with a
shield-swapbinary, and the DEX examples are rewritten against the currentAPI.
Why a separate package
The scripts shipped as raw TypeScript inside
@provablehq/shield-swap-sdkandran with
npx tsxfrom a path insidenode_modules. Putting abinon the SDKinstead would install a
node_modules/.bin/shield-swapsymlink for everyconsumer, so a frontend that only wanted the client would get a CLI it never
asked for.
@provablehq/shield-swap-cliis a separate install; the SDK tarballis now
distplus the runbook markdown, and ships no scripts at all.Each script became a module exporting
main(argv).src/registry.tsdeclaresthe subcommands and imports them lazily, so
shield-swap poolsnever loads theproving stack.
swap-historyis nowhistory; every other name, all flags, andthe
--execute/--jsoncontracts are unchanged.The gap this closes
The package
tsconfigincludessrconly, so ~3,900 lines of shipped scriptwere never covered by
tsc --noEmit. CI never typechecked workspace packages atall — only whatever a test happened to import. Same hole in
examples/, whereshield-swap-swap.tsandshield-swap-liquidity.tshad been readingwrapper_programon a type that carriesamm_token_program, compiling nowhereand running nowhere.
Adds a Typecheck packages step and a scoped Typecheck examples step, and
wires the new examples into
live-api.ymlso their read tiers run against livetestnet. That is what makes the move worth anything — otherwise the code just
moved.
Two bugs found while moving
setup --helpbootstrapped the account instead of printing help. It parsesits own arguments and had no
--helpbranch, so the first thing it did was readand migrate the state file. Caught by a smoke test, which migrated a live
account before I noticed.
--jsonis now set by the dispatcher, not by each command. A command couldpreviously report progress before reaching its own
setJsonModeand break theone-object-on-stdout promise.
Examples
Eight single-subject files under
examples/shield-swap/.setup-client.tswalksthe whole bootstrap — account, Provable credentials, client, DEX session, invite
code, faucet — and the rest are one flow each:
pool-state,quote,balances,swap-history,swap,mint,liquidity. They callclient.planSwap(),client.swap()and the rest directly rather than through wrappers, so they readas the API instead of a layer over it.
examples.test.tsruns them in three tiers — reads needing no account, readsneeding an account but spending nothing, and the ones that move funds behind a
separate
VEIL_EXAMPLES_SPENDopt-in. Test scaffolding lives there rather thaninside the examples, so what a reader copies is what runs.
Verified live against testnet:
pool-state,quote,balances, andswap-historyall pass.Commits
previewMint, options-object Q128 helpers,getOwnedPositionsreporting records with no mapping entry,
resolveToken→tokenData. Thispredates the CLI work but the CLI and examples depend on it, so it cannot land
separately.
Review notes
resolveToken→tokenDatais a hard rename of a published 0.6.0 exportwith no deprecated alias, in the same change that carefully kept deprecated
positional overloads for the Q128 helpers. Worth a deliberate call.
silenceSdkLogs()inpackages/provable-sdksetssetLogLevel('silent')onevery
loadNetworkwith no opt-out. It solves a CLI legibility problem in apackage the CLI depends on, which
.agents/contributors.mdspeaks against; alogLeveloption would leave the lever in place for someone debugging scannerretries.
and pair→pool resolution, the token index,
share(), and theresolveDexImportspair boilerplate are each repeated across command files. All of it predates this
PR and moved unchanged — extracting it inside a rename commit would have made the
move unreviewable. Worth a follow-up.
setupbypasses the shared plumbing — its own arg parser, no--json, nounknown-flag rejection, so a typo'd
--invit-codebootstraps without redeeming.Pre-existing; the usage block now says so rather than advertising flags it does
not honour.