Onboard router: one-method onboarding with on-chain capability discovery (CAP-68 + CAP-73)#16
Open
willemneal wants to merge 34 commits into
Open
Onboard router: one-method onboarding with on-chain capability discovery (CAP-68 + CAP-73)#16willemneal wants to merge 34 commits into
willemneal wants to merge 34 commits into
Conversation
…ding + e2e Adds the design for: a new SDK `buildTrustTx` (CAP-73 `SAC.trust(holder)`) for open assets, adding testnet USDC to the pinned registry, making USDC the live asset on testnet only (mainnet stays EURCV), and two real-testnet e2e layers (Node/SDK + Playwright browser) plus Vitest unit tests. Spec: docs/superpowers/specs/2026-06-08-usdc-e2e-and-open-asset-onboarding-design.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Task-by-task TDD plan derived from the approved design spec: Vitest tooling, testnet USDC registry entry, buildTrustTx (CAP-73 SAC.trust), dApp capability branch + e2e wallet seam, SAC deploy helper, .env.e2e, Node + Playwright real-testnet e2e, and CI wiring. Concrete values (testnet USDC SAC/issuer/flags) verified during planning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
|
Reviewed the design — mechanism is verified-sound; a few concrete changes before code lands. I ran the open-asset path on real testnet against the repo's resolved SDK (
Must-fix
Test gating
Spec clarity
Nice-to-have
Mechanism, ABI, and decode are verified on-chain, so the path to merge is mostly the registry entry + env wiring + CI gating. Nice spec. |
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… assets Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-in) Adds playwright.config.ts + tests/e2e/usdc-activation.spec.ts for a browser-driven real-testnet test that signs with an injected keypair via page.exposeFunction/__AUTHLINE_E2E__ seam (no wallet extension needed). Registers playwright.config.ts in tsconfig.node.json include so the eslint project-service can lint it. Adds allowDefaultProject for src/*.test.ts in eslint.config.js to resolve a pre-existing project- service gap for vitest files under src/. Adds test:e2e script to package.json. Offline gates green: tsc -b, vitest (8p+1s), eslint. Live run requires testnet network access (opt-in/CI). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the no-op `npm test --if-present` step with a real `Unit tests` step running `npm test` (Vitest units). Add an `e2e-testnet` job guarded by `if: github.event_name == 'workflow_dispatch'` so testnet flakiness never blocks PRs. Add `workflow_dispatch:` to the `on:` block to allow manual dispatch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One router method onboard(sac, holder): CAP-68 get_address_executable + SAC.admin() discover the asset's capability at execution time, replacing the client-side open/permissioned branching (buildTrustTx vs 3-arg onboard). Decisions: Abort->TrustlineOnly + SEP typed-error rule, OnboardStatus enum, delete old paths outright, pivot PR #16 in place. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…asks, TDD) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…8 executable semantics Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…stTx; onboard->router
…s, typed-error rule Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…superseded wrapper note Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…uncurated codes too
…UTER wiring Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…uter CTA Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Horizon.Server was constructed bare (allowHttp defaulting to false) in status.ts and exchange.ts, while every rpc.Server threaded `allowHttp ?? defaultAllowHttp(url)`. So a local quickstart (http localhost) Horizon threw "Cannot connect to insecure horizon server" from getActivationStatus on wallet connect, surfacing in the dApp as "Couldn't create trustline". Thread allowHttp through every Horizon.Server (defaulting to the same localhost-only defaultAllowHttp), export defaultAllowHttp from the SDK, and derive the dApp's NETWORK.allowHttp from the configured URLs instead of hardcoding false so the rpc submit path also works against a local node. Remote endpoints stay https-only. New status.test.ts pins the threading (localhost http allowed; remote https stays secure; remote http stays refused). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ainnet
The live-asset CODE defaulted to "EURCV" regardless of network, so a dApp
pointed at testnet/local without PUBLIC_ASSET_CODE resolved mainnet EURCV
— which has no testnet issuer/SAC — and failed ("EURCV isn't available on
testnet"). Make the default network-aware: PUBLIC keeps EURCV (the
production target); every other network defaults to the pinned testnet
token (USDC), which has a real testnet issuer/SAC and activates cleanly.
NET_TAG is hoisted above CODE so it can drive the default.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dApp's only Horizon use was getActivationStatus reading classic trustline flags. Stellar RPC — the same endpoint that builds and submits the onboard tx — serves the same data via getLedgerEntries(LedgerKeyTrustLine), so the whole Horizon dependency, and the insecure-horizon footgun class, goes away. One endpoint, one allowHttp. - getActivationStatus reads the trustline ledger entry over RPC and decodes AUTHORIZED_FLAG (horizonUrl -> rpcUrl). - delete assetAuthRequired (dead since the router discovers capability on-chain; the client no longer pre-reads auth_required). - buildSponsoredOnboardTx fetches its source account via rpc.getAccount (horizonUrl -> rpcUrl); the Horizon import is removed from the SDK. - drop horizonUrl from the dApp NETWORK + getActivationStatus calls and the now-dead PUBLIC_STELLAR_HORIZON_URL from .env.e2e. Both real-chain testnet e2e still pass (USDC -> Authorized; TLO AUTH_REQUIRED -> created AND authorized, trustline status read via RPC). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…back) The SDK React hook (useActivation / ActivateButton, the public @theaha/authline/react surface) reported "Activated" on any tx SUCCESS, ignoring the router's OnboardStatus — so it claimed full activation even when the router returned TrustlineOnly (trustline created but not authorized). Decode the return value and surface a trustlineOnly flag; ActivateButton renders "Trustline created" vs "Activated ✓". Extract the decode into a shared decodeOnboardStatus helper (handles the unit-enum vec and bare-symbol shapes; returns null when absent or undecodable) and use it in both the hook and the dApp, replacing the dApp's inline decode. On an unknown/undecodable outcome the dApp now falls back to the asset's static capability (\!IS_OPEN) so it never renders the stronger "authorized" claim for a possibly-trustline-only asset. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The build job had the Rust toolchain (from the scaffold build) but never ran the contract unit tests, where the router's core correctness lives (discovery error-classification, CAP-68 gates) — a logic regression could pass CI green. Add a "Contract tests" step (npm run test:contracts) and extend that script to cover authorizer-stub too. test:e2e:testnet was hardcoded to the USDC file, orphaning the TLO discovery e2e (the PR's flagship on-chain proof). Point it at the whole tests/e2e dir so both node e2e run on dispatch; the RUN_TESTNET_E2E gate still skips them by default. Add a concurrency guard to the manual e2e-testnet job — its runs share fixed testnet ids (USDC/TLO SACs) and must not interleave. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…claims Review findings + migration follow-ups across the docs: - SDK: bump to 0.3.0; README reframed around the discovery router and OnboardStatus (decode the return value rather than treating tx SUCCESS as full activation); fix the dangling ARCHITECTURE.md link and the buildSponsoredOnboardTx "provided separately" mislabel; testnet example (no mainnet router is pinned yet). - SEP: record the v0.3 discovery-router run under "Proven on testnet" and remove the now-false Protocol-26 JS-SDK decode caveat (the JS SDK builds, simulates, submits, and decodes the discovery onboard end-to-end); drop references to the deleted assetAuthRequired helper (capability is discovered on-chain) and list decodeOnboardStatus in the integrator surface. - mark the pre-pivot 2026-06-08 plan doc as SUPERSEDED. - .env.example: drop the dead PUBLIC_STELLAR_HORIZON_URL and the stale pre-pivot PUBLIC_TRUSTLINE_ONBOARD_CONTRACT_ID / PUBLIC_TEST_* blocks. - discovery.ts doc example: add the SEP-required VERSION field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Preserve the pre-migration Horizon behavior: the connect-time "already activated?" pre-check must not turn a transient RPC blip into a dead-end error screen. A missing/unfunded account or a read error now reads as not-activated, exactly as before; the activate() flow still surfaces real submit errors, and a misconfigured insecure-http endpoint still throws at construction. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The service was renamed (soroban-rpc -> stellar-rpc); the code already uses the modern `rpc` namespace from @stellar/stellar-sdk (never the deprecated SorobanRpc alias). Update the lingering comment/doc mentions for consistency. The testnet endpoint hostname (soroban-testnet.stellar.org) is unchanged — that is still its official URL. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- react.tsx: the public SDK hook claimed "Activated" on an undecodable/absent OnboardStatus return; now it only claims full activation on a chain-confirmed Authorized (unknown -> trustline-only), matching the dApp's truthful guard. - examples/exchange-withdrawal: migrate both demos off the deleted assetAuthRequired + Horizon — demo-open.mjs no longer imports the removed export (it crashed at module load), and both pass rpcUrl to the migrated getActivationStatus / buildSponsoredOnboardTx; Horizon.Server is kept only as the demos' own classic-submission transport. - docs: the SEP Backend-1 sequence diagram no longer calls the deleted assetAuthRequired() (-> discover()); docs/authline-sdk.md drops it and uses the 2-arg onboard(sac, holder); the README build example is flagged as a standalone testnet illustration. - ops: drop the now-dead PUBLIC_STELLAR_HORIZON_URL from both deploy workflows and fix the environments.toml comment that pointed at a removed env var. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Replaces the asset-dependent two-path onboarding flow with a single Soroban "discovery router" method that learns an asset's trust-establishment capability on-chain, so the client never branches on per-asset config. Built TDD from the committed design spec + plan (in
docs/superpowers/; the older two-path design is retained there, marked superseded).The pivot
One method does it all:
holder.require_auth()— the holder signs once, over the whole auth tree.sac.executable()— refuse asacthat isn't a real Stellar Asset Contract (on-chain anti-copycat), then detect whether the SAC admin is a Wasm contract.trust()— idempotent; creates the trustline (no-op if it exists). Open asset (!AUTH_REQUIRED) → already authorized →Authorized.try_authorize_trustline(holder)on the discovered admin and classify:Ok+ authorized →Authorized;Ok+ still unauthorized →Err(NotAuthorized); a typed contract error →Err(AuthorizationRefused); any other recoverable error (no export / panic / non-contract admin) →Ok(TrustlineOnly).OnboardStatus = { Authorized, TrustlineOnly };Error = { NotSac=1, TrustFailed=2, AuthorizationRefused=3, NotAuthorized=4 }. The try-call classification is verified againstsoroban-env-host26.1.3 (a 6-test spike pins the env semantics).Why on-chain discovery: only a SAC's admin can
set_authorized, so "the admin exportsauthorize_trustline" is the definition of one-step capability — the oldauthorizerconfig/toml field was a spoofable copy ofsac.admin(). Discovering it on-chain removes the trust assumption.Changes
contracts/trustline-onboard) — the 2-arg discovery router; 16 Rust tests (10 scenario + 6 env-classification), incl. typed-rejection revert, no-export/panicking admin →TrustlineOnly, post-conditionNotAuthorized, impostor-SAC rejection, real G-account holder.# Securitydocuments the holder-signed auth tree over the asset-chosen admin's sub-invocations.packages/authline-sdk, 0.3.0) —buildOnboardTxtargets the router;buildTrustTx+ the old 3-argonboarddeleted. Per-networkROUTERSsingleton pinned + StrKey-validated;reconcileWithRegistrychecks the advertised router against the pin even for uncurated codes (asset-independent). ShareddecodeOnboardStatusdecodes the router's return value; the React hook +ActivateButtonnow reportTrustlineOnlytruthfully instead of "Activated" on every tx success.getActivationStatusreads the trustline straight from the ledger (getLedgerEntries);assetAuthRequireddeleted (capability is discovered on-chain);buildSponsoredOnboardTxusesrpc.getAccount. One endpoint, oneallowHttp(localhost-only by default), and the prior "insecure horizon server" failure class is gone.src/) — singleactivate()path through the router; truthful success state decoded fromOnboardStatus("trustline created" vs "trustline authorized"); a missing/undecodable outcome falls back to the static capability so it never over-claims authorization; missing-router gates the CTA. Live asset defaults network-aware (mainnet → EURCV, every other network → the pinned testnet USDC) so a dev on testnet gets a working asset.sep/SEP-XXXX-…) — v0.3: verbatim contract code, the typed-error-MUST rule, theOnboardStatus/router model; records the v0.3 discovery-router testnet run.buildjob now runsnpm run test:contracts(the router's correctness lives in those Rust tests); the workflow-dispatche2e-testnetjob runs the fulltests/e2esuite (no longer orphaning the TLO discovery test) with a concurrency guard.CABVVUYHXS6UVN2VYYXKEUO2XEJIAGMTEYF2BOWGUUJVOO2IGPRWZAX4). Mainnet deploy is a tracked follow-up, gated on a fresh on-chain EURCVsac.admin()read.Test Plan
npm run lint·npm run typecheck·npm run build— greennpx vitest run— 26 passed, 2 real-chain e2e skipped by default (RUN_TESTNET_E2Egate)npm run test:contracts— 17 Rust tests pass (16 router + 1 authorizer-stub;cargo test -p trustline-onboard -p authorizer-stub)npm run test:e2e:testnet— 2 passed on real testnet: USDC open →Authorized; TLOAUTH_REQUIRED→ trustline created AND authorized via on-chain admin discovery ({ hasTrustline: true, isAuthorized: true }, read back over RPC)npm run test:e2e— Playwright browser e2e 1 passed on real testnet (directory → connect → activate → authorized USDC trustline)Notes
workflow_dispatch-only.🤖 Generated with Claude Code