Skip to content

feat(core): sponsor-wallet state-machine helpers (closes #22)#23

Merged
whoabuddy merged 3 commits into
mainfrom
feat/sponsor-wallet-helpers
Apr 15, 2026
Merged

feat(core): sponsor-wallet state-machine helpers (closes #22)#23
whoabuddy merged 3 commits into
mainfrom
feat/sponsor-wallet-helpers

Conversation

@whoabuddy
Copy link
Copy Markdown
Contributor

Summary

  • Adds pure runtime helpers around the existing sponsor-wallet schemas so downstream services (relay, settle, sponsor) stop re-implementing nonce-conflict logic inline.
  • New helpers: classifyOccupant, decideBroadcast, adoptOrphan, quarantine, reconcile. All pure, injectable now.
  • New schemas: HiroSponsorTxView, SponsorLedger, SponsorLedgerEntry, LastOccupant, QuarantinedNonce, OccupantClassification, BroadcastDecision. WalletCapacity gains optional quarantinedNonces + possibleNextNonce (back-compatible).
  • README gains a "Consuming sponsor wallet helpers" section with the canonical classify → decide → apply → reconcile cycle.

Closes #22. Gates the aibtcdev/x402-sponsor-relay refactor on a new minor (0.8.0) once release-please picks this up.

Design notes

  • Ledger lives in the relay; this package ships the shape so every consumer converges on the same representation. Ledger keys are integer-nonce strings (z.record with regex-validated keys).
  • Hiro shape is defined as a minimal .passthrough() zod schema — structurally compatible with @stacks/blockchain-api-client's Transaction / MempoolTransaction without adding a runtime dep.
  • Orphan without a price (no fee_rate, no caller fee hint) is quarantined rather than silently RBF'd at 1 µSTX — caller surfaces it explicitly.
  • Back-compat: quarantinedNonces and possibleNextNonce are optional so existing WalletCapacity payloads still parse.

Test plan

  • npm run typecheck
  • npm test — 197 passing (25 new workflow tests covering the 7 acceptance cases in the issue)
  • npm run build
  • Release-please ships 0.8.0 on merge; relay PR consumes it.

Ships runtime helpers around the existing sponsor-wallet schemas so every
AIBTC service (relay, settle, sponsor) drives off the same primitives
instead of re-implementing nonce-conflict logic inline.

- Schemas: HiroSponsorTxView, SponsorLedger, SponsorLedgerEntry,
  LastOccupant, QuarantinedNonce, OccupantClassification,
  BroadcastDecision. Extend WalletCapacity with optional
  quarantinedNonces and possibleNextNonce.
- Helpers (pure, injectable clock): classifyOccupant, decideBroadcast,
  adoptOrphan, quarantine, reconcile.
- Workflow tests cover the seven acceptance cases from issue #22:
  in-ledger RBF, orphan adopt, foreign quarantine, ghost untraceable,
  fee_too_low rebid, max-attempts quarantine, reconciliation drift.
- README gains a "Consuming sponsor wallet helpers" section.

Closes #22.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 15, 2026 15:55
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a small, pure(ish) “sponsor wallet state machine” runtime layer on top of the existing core schemas so downstream services can converge on the same nonce-conflict / RBF / quarantine workflow.

Changes:

  • Introduces new runtime helpers (classifyOccupant, decideBroadcast, adoptOrphan, quarantine, reconcile) plus supporting schemas/types (Hiro view + sponsor ledger).
  • Extends WalletCapacity with optional quarantine tracking (quarantinedNonces, possibleNextNonce) and adds lastOccupant metadata to occupied nonces.
  • Adds workflow-focused tests and README documentation for the canonical classify → decide → apply → reconcile cycle.

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/sponsor-wallet-machine.test.ts New workflow tests covering the issue’s acceptance cases for classification/decision/adoption/quarantine/reconcile.
src/core/wallet-capacity.ts Adds LastOccupant, quarantined nonce schemas, and optional fields to WalletCapacity.
src/core/sponsor-wallet-machine.ts Implements the new sponsor-wallet helper functions and their Zod discriminated unions.
src/core/sponsor-ledger.ts Adds SponsorLedger/SponsorLedgerEntry schema and getLedgerEntry helper.
src/core/index.ts Exports the new core modules from the package’s core entrypoint.
src/core/hiro-tx.ts Adds minimal Hiro tx “view” schema (.passthrough()) for helpers.
package.json Adds exports subpaths for the new core modules.
package-lock.json Lockfile refresh (includes removals of some optional deps).
README.md Documents the canonical consumption pattern and imports for the helpers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/core/sponsor-ledger.ts Outdated
Comment thread src/core/sponsor-wallet-machine.ts Outdated
Comment thread src/core/sponsor-wallet-machine.ts
Comment thread src/core/wallet-capacity.ts
Comment thread README.md Outdated
whoabuddy and others added 2 commits April 15, 2026 10:52
- Sponsor ledger schema: add superRefine to enforce `Number(key) === entry.nonce`,
  so helpers that trust the record key can't drift from the entry payload.
- classifyOccupant: attribute foreign sponsored txs to the sponsor (who holds
  the sponsor nonce) rather than the sender.
- reconcile: stop silently adopting orphans at `fee: "0"` when `fee_rate` is
  missing. Surface them on a new `unpriceableOrphans` list instead so callers
  can quarantine/alert — matches the `decideBroadcast` guard.
- wallet-capacity comment: replace the misleading "default to []/assignmentHead"
  wording with a description of the back-compat contract consumers implement.
- README: correct the "no clock reads" claim to reflect that helpers default to
  `new Date()` when `now` is omitted.

Adds three new tests covering key/nonce drift rejection, foreign-sponsor
attribution, and the unpriceable-orphan path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The initial feature branch commit inadvertently stripped transitive
peer-dep entries (@emnapi/*) from the lockfile — residue from a
temporary dev dep that was installed and removed locally. No dependency
changes were introduced by this PR, so the lockfile should be identical
to main. `npm ci` now succeeds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@whoabuddy whoabuddy merged commit 75ed8bb into main Apr 15, 2026
1 check passed
@whoabuddy whoabuddy deleted the feat/sponsor-wallet-helpers branch April 15, 2026 18:35
Copy link
Copy Markdown

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

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

Centralises sponsor-wallet nonce-conflict logic into pure, composable helpers — exactly the right fix for the inline duplication that was spreading across relay, settle, and sponsor services.

What works well:

  • The classify → decide → apply → reconcile cycle is clean and well-documented. The README example gives consumers a working mental model immediately.
  • HiroSponsorTxView.passthrough() is the right call — it lets consumers pass the raw Hiro response without coupling to @stacks/blockchain-api-client as a runtime dep.
  • SponsorLedgerSchema.superRefine cross-validating key↔entry nonce is defensive in exactly the right place — prevents key/value drift bugs that would be silent otherwise.
  • Back-compat via optional quarantinedNonces + possibleNextNonce means existing WalletCapacity payloads parse without migration.
  • 25 new workflow tests covering all 7 acceptance cases from issue #22. The fixture helpers (makeWallet, makeLedger, hiroTx) make the tests easy to read.

[suggestion] bump adds only 1 µSTX — may be insufficient for real RBF (src/core/sponsor-wallet-machine.ts)

The bump function increments by exactly 1 µSTX. Stacks mempool policy may require a minimum percentage fee increase for RBF to be accepted. The caller can add their own bump on top, but the current API makes this non-obvious — a caller who uses the returned fee directly will likely get a fee_too_low rejection. Consider accepting a minimum floor or documenting clearly that the caller must add their own margin on top of the returned fee.

[question] adoptOrphan defaults abandonAfter to now (src/core/sponsor-wallet-machine.ts:306)

const abandonAfter = options.abandonAfter ?? now;

When abandonAfter is omitted, it's set to the current timestamp, meaning the relay can consider the nonce immediately eligible for abandonment on first check. This may be intentional (the orphan is already old), but it's a sharp edge: callers who don't pass abandonAfter will get an already-expired window for new occupied entries. Is the intent that relay always passes abandonAfter explicitly, or that "adopt at now" is a valid default?

[suggestion] reconcile drops detection is narrow (src/core/sponsor-wallet-machine.ts:415-420)

The dropped-detection loop only runs for nonces present in mempoolReadByNonce. A ledger entry whose nonce wasn't included in the mempool read is silently ignored. This is noted as "address-filtered" by design, but a caller who queries a partial nonce range could miss dropped entries. Worth adding a JSDoc line noting that dropped only covers keys present in mempoolReadByNonce.

[nit] decideSponsorConflict has an unlabeled fallback RBF path (src/core/sponsor-wallet-machine.ts:271-274)

if (!occupant) {
  if (ledgerEntry) {
    return { kind: "rbf_with_fee", nonce, fee: bump(ledgerEntry.fee) };
  }
  ...
}

This triggers when Hiro returns no mempool hit but the ledger has an entry — effectively "we think we broadcast this, but Hiro can't see it." Returning rbf_with_fee here means the relay will attempt to RBF without mempool confirmation. A comment would help readers understand this is the "assumed still in mempool" path, not a classified-occupant path.

Code quality notes:

  • bump would read more clearly as bumpFee to distinguish from array/index bumping elsewhere in the file.
  • The reconcile function does two distinct passes (adopt orphans, detect drops) that could be factored into named helpers for readability if the function grows, but current length is fine.
  • No dead code or unused imports introduced.

Operational context:
We've seen nonce gaps [2920, 2921] on the x402-relay sponsor wallet (tracked in our MEMORY). This PR is the missing primitive that makes those gaps recoverable — specifically, the quarantine + possibleNextNonce advancement lets the relay hop over a stuck nonce without manual intervention. The reconcile function also addresses the case we've observed where a sponsor tx appears in the mempool but wasn't recorded in the local ledger (the "orphan" class). Shipping 0.8.0 from this will unblock the relay refactor we've been waiting on.

Reviewed post-merge (already MERGED at time of this review). No action needed; commenting for the record.

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.

Add runtime helpers + workflow tests for sponsor wallet state transitions

3 participants