Skip to content

[Feat][Docs] Create shield-swap cli + add runnable shield-swap examples - #123

Merged
iamalwaysuncomfortable merged 28 commits into
mainfrom
feat/trader-scripts
Aug 6, 2026
Merged

[Feat][Docs] Create shield-swap cli + add runnable shield-swap examples#123
iamalwaysuncomfortable merged 28 commits into
mainfrom
feat/trader-scripts

Conversation

@iamalwaysuncomfortable

Copy link
Copy Markdown
Member

The trader scripts move out of the SDK into their own package with a
shield-swap binary, and the DEX examples are rewritten against the current
API.

Why a separate package

The scripts shipped as raw TypeScript inside @provablehq/shield-swap-sdk and
ran with npx tsx from a path inside node_modules. Putting a bin on the SDK
instead would install a node_modules/.bin/shield-swap symlink for every
consumer, so a frontend that only wanted the client would get a CLI it never
asked for. @provablehq/shield-swap-cli is a separate install; the SDK tarball
is now dist plus the runbook markdown, and ships no scripts at all.

npx @provablehq/shield-swap-cli setup --new
npx @provablehq/shield-swap-cli swap --from USDCx --to ETH --amount 1.5 --execute

Each script became a module exporting main(argv). src/registry.ts declares
the subcommands and imports them lazily, so shield-swap pools never loads the
proving stack. swap-history is now history; every other name, all flags, and
the --execute / --json contracts are unchanged.

The gap this closes

The package tsconfig includes src only, so ~3,900 lines of shipped script
were never covered by tsc --noEmit. CI never typechecked workspace packages at
all — only whatever a test happened to import. Same hole in examples/, where
shield-swap-swap.ts and shield-swap-liquidity.ts had been reading
wrapper_program on a type that carries amm_token_program, compiling nowhere
and running nowhere.

Adds a Typecheck packages step and a scoped Typecheck examples step, and
wires the new examples into live-api.yml so their read tiers run against live
testnet. That is what makes the move worth anything — otherwise the code just
moved.

Two bugs 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. Caught by a smoke test, which migrated a live
    account before I noticed.
  • --json is now set by the dispatcher, not by each command. A command could
    previously report progress before reaching its own setJsonMode and break the
    one-object-on-stdout promise.

Examples

Eight single-subject files under examples/shield-swap/. setup-client.ts walks
the 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 call client.planSwap(),
client.swap() and the rest directly rather than through wrappers, so they 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. Test scaffolding lives there rather than
inside the examples, so what a reader copies is what runs.

Verified live against testnet: pool-state, quote, balances, and
swap-history all pass.

Commits

  1. Extract the trader scripts — the move, the binary, the CI typechecks.
  2. Worked examples — the eight files, tiered runner, CI wiring.
  3. SDK workpreviewMint, options-object Q128 helpers, getOwnedPositions
    reporting records with no mapping entry, resolveTokentokenData. This
    predates the CLI work but the CLI and examples depend on it, so it cannot land
    separately.

Review notes

  • resolveTokentokenData is a hard rename of a published 0.6.0 export
    with no deprecated alias, in the same change that carefully kept deprecated
    positional overloads for the Q128 helpers. Worth a deliberate call.
  • silenceSdkLogs() in packages/provable-sdk sets setLogLevel('silent') on
    every loadNetwork with no opt-out. It solves a CLI legibility problem in a
    package the CLI depends on, which .agents/contributors.md speaks against; a
    logLevel option would leave the lever in place for someone debugging scanner
    retries.
  • Known duplication left alone. The claim-retry loop appears in three commands,
    and pair→pool resolution, the token index, share(), and the resolveDexImports
    pair 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.
  • setup bypasses the shared plumbing — its own arg parser, no --json, no
    unknown-flag rejection, so a typo'd --invit-code bootstraps without redeeming.
    Pre-existing; the usage block now says so rather than advertising flags it does
    not honour.

…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.
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
veil-loyalty-dapp Ready Ready Preview Aug 6, 2026 4:26am

Request Review

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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-cli with 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-devnode via 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.

Comment thread packages/shield-swap/src/actions/swap/planSwap.ts Outdated
Comment thread packages/shield-swap-cli/src/commands/swap.ts
Comment thread packages/shield-swap-cli/src/commands/swap-concurrent.ts
Comment thread examples/shield-swap/mint.ts
Comment thread packages/provable-sdk/src/index.ts
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.
@iamalwaysuncomfortable iamalwaysuncomfortable changed the title [Feat] Extract the trader scripts into a shield-swap CLI, and rewrite the DEX examples [Feat][Docs] Create shield-swap cli + add runnable shield-swap examples Aug 6, 2026
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.
@iamalwaysuncomfortable
iamalwaysuncomfortable merged commit cda4f20 into main Aug 6, 2026
6 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.

2 participants