Skip to content

fix(batch): re-read a channel whose authorization is refused with a plain 402 - #91

Merged
VickyXAI merged 1 commit into
mainfrom
fix/batch-recheck-refused-channel
Oct 27, 2026
Merged

VickyXAI merged 1 commit into
mainfrom
fix/batch-recheck-refused-channel

Conversation

@VickyXAI

Copy link
Copy Markdown
Contributor

Closes the known gap noted in 3.24.8 (and the P2 from the #90 review).

Problem

The SDK reads the channel on-chain only before a top-up. A reclaimed channel (PayAI closes channels idle for 72h) whose local record still has deposit to spare gets an authorization signed into it on every call. The gateway rejects it at verify and answers with a plain 402 with no receipt (blockrun-sol src/lib/batch/payment.ts, fail()). The SDK classifies that as not_charged / payment_required and pays exact, so every later call pays exact (1.5–4.5×) until the channel file is deleted by hand.

Change

  • payWith: when the first send of an authorization is refused with a plain 402 (payment_required, no admission cooldown), the new recheckRefusedChannel() reads that channel:
    • open, and this wallet's → kept; the next call pays into it again
    • closing / closed / foreign at finalized → channel_unusable re-read (closed → record dropped)
    • gone at finalized and at confirmed → queueReclaimed(), the anchored wait from fix(batch): drop a reclaimed channel on the SDK's own chain evidence, not the 402's openSlot #90, now shared with prepareTopUp: exact for about 2 minutes, then the record is dropped and the next call opens a fresh channel
    • only at confirmed (not finalized yet) → kept
  • resync() runs right away, so a process that makes one call per run still finishes the wait.
  • The scheme rewrites the record without reclaimAnchorHeight while it builds and settles a payment. So buildPayment() reads the saved anchor first and carries it on SentPayment.reclaimAnchor. A test caught this: without it, every short-lived run started a new wait.
  • Best effort: a failed read changes nothing, the call already pays exact, and the next refusal checks again. Admission refusals, 429s and deposits keep their existing paths.

Tests

  • A refused reclaimed channel pays exact during the wait, nothing is signed into it, and it reopens at +301 blocks. One getBlockHeight.
  • A process that makes one call per run still drops the channel, using the saved anchor.
  • A live channel refused for another reason is kept and paid into again, with no resync and no anchor.
  • A channel the finalized chain shows closed is dropped right away (no wait).
  • Three of the four fail on main. npm test: 954 passed, 12 skipped. Build, lint and typecheck are clean. grep -c router-core dist/index.d.ts → 0.

Docs: docs/solana-batch.md, CLAUDE.md batch section, CHANGELOG (Unreleased).

…lain 402

The chain was read only before a top-up, so a reclaimed channel whose record
still had deposit to spare got an authorization signed into it on every call;
the gateway refuses those at verify with a plain 402 (blockrun-sol
src/lib/batch/payment.ts fail()) and every call paid exact, forever.

On a first-send authorization answered not_charged / payment_required (no
admission cooldown), recheckRefusedChannel() reads the channel: open and the
wallet's → kept; closing/closed/foreign → channel_unusable re-read; gone at
finalized and confirmed → queueReclaimed() (the anchored wait now shared with
prepareTopUp). resync() runs at once, so one-call-per-run processes finish
the wait. The scheme rewrites the record without reclaimAnchorHeight while it
builds and settles a payment, so buildPayment() captures the saved anchor
first and carries it on SentPayment.

Tests: refused reclaimed channel waits then reopens; one call per process
still drops it; a live channel refused for another reason is kept and paid
into again; a closed channel is dropped with no wait. Three fail on main.
@VickyXAI
VickyXAI merged commit 15d86ce into main Oct 27, 2026
3 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.

1 participant