Repository navigation
docs(testnet): pre-flight checks, error mapping, idempotency guide, and Postman collection - #752
Merged
phertyameen merged 1 commit intoSep 27, 2026
Conversation
…idempotency, and a Postman collection
Four integrator-facing deliverables for the shared testnet deployment. All
four are derived from the contract source, because no Bridgelet contract is
currently deployed: testnet/registry/contract-status.md records all four as
undeployed with IDs "TBD" and Last Verified "Never". Nothing below claims to
have been observed on-chain, and each document says so explicitly, because
the issue for the idempotency guide asked for behaviour "from observed
testnet behavior" and the honest answer is that it has not been observed yet
and a live test is required to confirm it.
testnet/examples/checking-account-status.md
is_expired() and can_sweep() as pre-flight checks, built around the insight
that both mislead for an uninitialized account and the failure modes differ:
- is_expired() returns false when the instance is not initialized, so a
false is not proof the account is live - and false is also exactly what a
healthy account returns. get_status() has the same trap, returning Active
for an uninitialized instance.
- can_sweep() is a SweepController method that takes the child account
address and calls get_info() internally, and get_info() returns
Result<AccountInfo, Error>, so an uninitialized account is an error rather
than a false. A bare false from can_sweep() collapses three causes.
So the example is a gate rather than two calls: is_expired() for the cheap
time check, can_sweep() for composite readiness, and get_info() for the
initialization proof neither of the other two gives. A "what each check proves
and does not prove" table makes that explicit, matching the sibling README's
existing warning that get_status() is not proof of initialization.
The >= boundary gets explicit treatment, because is_expired() compares
current_ledger >= expiry_ledger, so an account expiring this ledger already
reports expired - which is exactly where fee-wasting races happen. The fee
argument carries an honest limit: a successful simulation is not a
reservation, so this narrows failure modes rather than eliminating them. The
CLI flag is --simulate-only, so every probe costs nothing and mutates
nothing.
testnet/examples/error-handling.md
Every error variant in all four contracts mapped to a discriminant, the
condition that fires it, and what an integrator should actually do - retry,
re-fetch state and re-sign, or stop and escalate. The "what to do" column is
the deliverable. Cross-referenced to docs/api-reference.md as the
companion, not a second source of truth, per the issue. Rust throughout,
matching the SDK examples in docs/api-reference.md and
testnet/integration/type-mapping.md.
It also covers errors that are not enum variants, because a document mapping
only enums would mislead anyone who only ever sees host-function traps - and
on this codebase that is most people:
- A bare ed25519_verify trap is usually nonce contention, not a signing
bug, per testnet/runbooks/failed-sweep-signature.md and
testnet/security/README.md.
- The signature asymmetry: SweepController::verify_sweep_auth performs real
Ed25519 verification, while EphemeralAccount::verify_sweep_authorization
ignores its auth_signature entirely, so the two InvalidSignature variants
mean different things.
NonEnum is kept distinct from every enum variant in the sample code so a
host trap can never be decoded as a contract code.
testnet/examples/idempotency-considerations.md
What a duplicate sweep submission does, worked out from the source and
labelled as needing live confirmation. The signature covers
SHA256(destination.to_xdr() || nonce as 8-byte big-endian u64 ||
controller_id.to_xdr()), and the nonce is incremented only after successful
authorization, so a replayed signature is bound to a consumed nonce.
The valuable part is the distinction the issue needs in order to be useful:
retrying the same signature and re-signing against a fresh nonce have
completely different outcomes, and only one of them is a no-op. Re-signing
with the old nonce produces a signature the contract will reject. claim() is
a different animal again - it takes no signature, so a duplicate claim is a
state question rather than a signature question, and conflating the two
would mislead anyone writing retry logic.
A decision table ("did I get a result" / "is the state consistent" / "what
may I safely do next") is the actual deliverable, plus replay-proof
hazards that are state-guarded rather than nonce-guarded.
testnet/examples/postman-collection.json
A Postman Collection v2.1 artifact - the only machine-readable deliverable
in this batch - with folder-per-contract structure, 21 collection variables,
and a saved response example on every one of the 14 requests so the
collection is useful before anyone runs it.
The simulateTransaction envelope is a collection variable with a documented
build-it-yourself recipe rather than a committed literal: Soroban RPC has no
convenience contract-call endpoint, so a real base64 TransactionEnvelope
cannot be hand-written, and faking one would be a fabrication. Every request
requires an inactive or read-only source, so no request in the collection
can change state - which is also why none needs a key. The collection uses
the Testnet passphrase and says so, since testnet/docs/changelog.md records
that the passphrase check currently expects Standalone and would reject
testnet calls.
Also adds one row per deliverable to the ## Example index table in
testnet/examples/README.md, which is what that table is for.
Closes bridgelet-org#638
Closes bridgelet-org#639
Closes bridgelet-org#640
Closes bridgelet-org#641
|
@brightMedina5050 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Four integrator-facing deliverables for the shared testnet deployment: two worked examples, a full error mapping, an idempotency guide, and a Postman collection.
testnet/examples/checking-account-status.mdtestnet/examples/error-handling.mdtestnet/examples/idempotency-considerations.mdtestnet/examples/postman-collection.jsonPlus one additive row per deliverable in the
## Example indextable intestnet/examples/README.md.All four are derived from the contract source, because no Bridgelet contract is currently deployed —
testnet/registry/contract-status.mdrecords all four as undeployed. Nothing claims on-chain observation. The idempotency guide's issue explicitly asked for behaviour "from observed testnet behavior", and the honest answer is that it has not been observed; the document derives it from source, says so, and lists what a live test would need to confirm.checking-account-status.md— pre-flight checksThe document's backbone is the insight that both checks mislead for an uninitialized account, and the failure modes differ:
is_expired()returnsfalsewhen the instance is not initialized — so afalseis not proof the account is live, andfalseis also exactly what a healthy account returns.get_status()has the same trap, returningActivefor an uninitialized instance.can_sweep()is aSweepControllermethod that takes the child account address and callsget_info()internally;get_info()returnsResult<AccountInfo, Error>, so an uninitialized account is an error, not afalse. A barefalsecollapses three distinct causes.So the example is a gate, not two calls:
is_expired()for the cheap time check,can_sweep()for composite readiness, andget_info()for the initialization proof neither of the other two gives. A "what each check proves and what it does not" table makes that explicit, building on the sibling README's existing warning thatget_status()is not proof of initialization.The
>=boundary gets explicit treatment becauseis_expired()comparescurrent_ledger >= expiry_ledger— an account expiring this ledger already reports expired, which is precisely where fee-wasting races happen. The fee argument carries an honest limit: a successful simulation is not a reservation, so this narrows failure modes rather than eliminating them. Every probe uses--simulate-only, so it costs nothing and mutates nothing — which is the entire point of a pre-flight.error-handling.md— variant → handlingEvery variant in all four contracts mapped to discriminant, triggering condition, and what an integrator should do (retry / re-fetch state and re-sign / stop and escalate). That last column is the deliverable. Rust throughout, matching the SDK examples in
docs/api-reference.mdandtestnet/integration/type-mapping.md. Cross-referenced todocs/api-reference.mdas its companion, not a second source of truth, per the issue.It also covers errors that are not enum variants, because a document mapping only enums would mislead anyone who only ever sees host-function traps — and on this codebase that is most people:
ed25519_verifytrap is usually nonce contention, not a signing bug.SweepController::verify_sweep_authperforms real Ed25519 verification, whileEphemeralAccount::verify_sweep_authorizationignores itsauth_signatureentirely — so the twoInvalidSignaturevariants mean different things.In the sample code,
NonEnumis kept distinct from every enum variant so a host trap can never be decoded as a contract code.idempotency-considerations.md— duplicate sweepsThe signature covers
SHA256(destination.to_xdr() ‖ nonce as 8-byte big-endian u64 ‖ controller_id.to_xdr()), and the nonce is incremented only after successful authorization, so a replayed signature is bound to a consumed nonce.The valuable part is a distinction the issue needs in order to be useful at all: retrying the same signature and re-signing against a fresh nonce have completely different outcomes, and only one of them is a no-op. Re-signing with the old nonce produces a signature the contract will reject.
claim()is a different animal again — it takes no signature, so a duplicateclaimis a state question rather than a signature question, and conflating them would mislead anyone writing retry logic.A decision table ("did I get a result" / "is the state consistent" / "what may I safely do next") is the actual deliverable, alongside the replay-proof hazards that are state-guarded rather than nonce-guarded.
postman-collection.json— machine-readableA Postman Collection v2.1 artifact (verified to parse, with the v2.1.0 schema URL), folder-per-contract, 21 collection variables, and a saved response example on all 14 requests so the collection is useful before anyone runs it.
Two design decisions worth stating:
simulateTransactionenvelope is a collection variable with a documented build-it-yourself recipe, not a committed literal. Soroban RPC has no convenience contract-call endpoint, so a real base64TransactionEnvelopecannot be hand-written — and faking one would be a fabrication. Every request requires an inactive or read-only source, so no request in the collection can change state, which is also why none of them needs a key.testnet/docs/changelog.mdrecords that the check currently expects Standalone and would reject testnet calls.Code/document defects found, documented rather than papered over
The two I already knew (the changelog's error-namespacing claim, and
UnauthorizedDestination = 13leaving a gap at 12) are covered explicitly. This batch surfaced several more:claimdoes not increment the nonce.get_nonce's own doc comment andtestnet/config/nonce-tracking.mdboth say the nonce "increments after every successfulexecute_sweep()/claim()" — butclaimcallsauthorize_claimdirectly and never reachessweep_account, which holds the onlyincrement_noncecall. This is the highest-value find in the batch: it breaks any "count the sweeps" reconciliation an integrator might attempt. Flagged as needing a live test.SweepControllererror variants are unreachable.ed25519_verifytraps, so there is noErrto convert.InvalidAccount(1),InsufficientBalance(4),AccountExpired(6),InvalidSignature(8),SignatureVerificationFailed(9) andInvalidNonce(11) are never constructed — yetdocs/api-reference.mdandtestnet/runbooks/failed-sweep-signature.mdboth list Create API Reference Documentation #9 as the expected bad-signature symptom. The same pattern appears on the other two contracts.record_paymentreturnsDuplicateAsset(13), neverPaymentAlreadyReceived(3). The rule is one payment per asset (up to 10), not one per account.testnet/docs/getting-started.mdlists#1000/#1001/#1012… — the source codes plus a flat+999— and maps#1007→Unauthorized, while the changelog cites#1007as the passphrase rejection. Two meanings, one number, two files.Error(Contract, #1007)cannot happen anyway — theinitializepassphrase check the changelog describes is not in the source.testnet/docs/soroban-cli-quickref.mdshowscan_sweep --account <EPH>; the parameter isephemeral_account.docs/api-reference.md'sEphemeralAccounterror table stops at code 14, omittingNotUpgradeAdmin(15).bridgelet_shared::passphrase::TESTNET_PASSPHRASEdoes not exist (contracts/shared/src/has onlylib.rsandtypes.rs), thoughtestnet/docs/network-config.mdadvertises it. The passphrase is also given as November 2015 intestnet/integration/type-mapping.mdagainst September 2015 innetwork-config.md.AccountFactoryhas noerrors.rsat all, so "mapping its error variants" means something different — handled explicitly rather than silently omitted, including thatinitialize's.unwrap()on the WASM hash panics rather than returning an error.Each is documented in the relevant file rather than fixed, since fixing them means editing files outside this batch's scope.
Verification
scripts/,tools/,contracts/,docs/,deployments/untouched.testnet/examples/README.mdchange is 4 insertions, 0 deletions.postman-collection.jsonparses as strict JSON with the v2.1.0 schema; 6 folders, 14 requests, 15 saved responses.ls-verified. No secret material, including inside the JSON.Closes #638
Closes #639
Closes #640
Closes #641