@antseed/node 0.2.85 → 0.2.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/node.d.ts +1 -0
  6. package/dist/node.d.ts.map +1 -1
  7. package/dist/node.js +21 -2
  8. package/dist/node.js.map +1 -1
  9. package/dist/payments/buyer-payment-manager.d.ts +4 -0
  10. package/dist/payments/buyer-payment-manager.d.ts.map +1 -1
  11. package/dist/payments/buyer-payment-manager.js +107 -91
  12. package/dist/payments/buyer-payment-manager.js.map +1 -1
  13. package/dist/payments/chain-config.d.ts +2 -0
  14. package/dist/payments/chain-config.d.ts.map +1 -1
  15. package/dist/payments/chain-config.js +2 -0
  16. package/dist/payments/chain-config.js.map +1 -1
  17. package/dist/payments/evm/emissions-client.d.ts +1 -0
  18. package/dist/payments/evm/emissions-client.d.ts.map +1 -1
  19. package/dist/payments/evm/emissions-client.js +6 -0
  20. package/dist/payments/evm/emissions-client.js.map +1 -1
  21. package/dist/payments/seller-payment-manager.d.ts +46 -5
  22. package/dist/payments/seller-payment-manager.d.ts.map +1 -1
  23. package/dist/payments/seller-payment-manager.js +319 -69
  24. package/dist/payments/seller-payment-manager.js.map +1 -1
  25. package/dist/reputation/on-chain-reputation.d.ts +46 -35
  26. package/dist/reputation/on-chain-reputation.d.ts.map +1 -1
  27. package/dist/reputation/on-chain-reputation.js +174 -67
  28. package/dist/reputation/on-chain-reputation.js.map +1 -1
  29. package/dist/seller-request-handler.d.ts.map +1 -1
  30. package/dist/seller-request-handler.js +13 -6
  31. package/dist/seller-request-handler.js.map +1 -1
  32. package/dist/types/peer.d.ts +13 -26
  33. package/dist/types/peer.d.ts.map +1 -1
  34. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"seller-payment-manager.d.ts","sourceRoot":"","sources":["../../src/payments/seller-payment-manager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"seller-payment-manager.d.ts","sourceRoot":"","sources":["../../src/payments/seller-payment-manager.ts"],"names":[],"mappings":"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"}
@@ -9,6 +9,8 @@ const DEFAULT_MIN_BUDGET_PER_REQUEST = '500000';
9
9
  /** ~200× typical Base settle gas cost at 0.006 gwei. */
10
10
  export const DEFAULT_MIN_SETTLE_DELTA_STR = '2000';
11
11
  const DEFAULT_MIN_SETTLE_DELTA = BigInt(DEFAULT_MIN_SETTLE_DELTA_STR);
12
+ const TOP_UP_THRESHOLD_NOT_MET_SELECTOR = '0x1ea4506b';
13
+ const INSUFFICIENT_BALANCE_SELECTOR = '0xf4d678b8';
12
14
  /**
13
15
  * Manages seller-side payment sessions.
14
16
  * The buyer sends a single SpendingAuth signature with a monotonically
@@ -26,8 +28,14 @@ export class SellerPaymentManager {
26
28
  * this resolves to the underlying channels contract via the facade's
27
29
  * `channelsAddress()` view. Otherwise equals the configured address. */
28
30
  _resolvedAddresses = null;
29
- /** In-memory cache of active buyer peerIds for fast has-session checks. */
31
+ /**
32
+ * In-memory cache of buyer peerIds with an active payment session. Hydrated
33
+ * sessions are included for existing hasSession() semantics; `_hydratedChannelIds`
34
+ * lets timeout cleanup distinguish restart-only zero-auth zombies.
35
+ */
30
36
  _activeBuyers = new Set();
37
+ /** Channels restored from disk before the buyer has proven it reconnected. */
38
+ _hydratedChannelIds = new Set();
31
39
  /** Per-buyer mutex to prevent concurrent handleSpendingAuth for the same buyer. */
32
40
  _buyerLocks = new Map();
33
41
  /** channelId -> highest accepted cumulativeAmount from buyer's SpendingAuth */
@@ -49,6 +57,14 @@ export class SellerPaymentManager {
49
57
  _acceptedWaiters = new Map();
50
58
  /** channelId -> number of failed close() attempts. In-memory only; resets on node restart. */
51
59
  _closeRetryCount = new Map();
60
+ /** channelId -> deferred topUp params when on-chain topUp failed (e.g. TopUpThresholdNotMet).
61
+ * Retried after the next SpendingAuth raises the settle amount high enough.
62
+ * Latest / largest top-up intent wins: if multiple deferred ReserveAuths arrive
63
+ * before a retry succeeds, we keep the most recent higher ceiling because it
64
+ * subsumes the older request. */
65
+ _pendingTopUp = new Map();
66
+ /** Channels that must not serve more paid work until closed/renegotiated. */
67
+ _blockedChannels = new Set();
52
68
  /** channelIds with an in-flight close() tx/estimate. Prevents duplicate close submissions. */
53
69
  _closingChannels = new Set();
54
70
  /** channelId -> cumulative amount last successfully settled on-chain by this
@@ -88,6 +104,7 @@ export class SellerPaymentManager {
88
104
  const activeChannels = this._channelStore.getActiveChannels('seller');
89
105
  for (const channel of activeChannels) {
90
106
  this._activeBuyers.add(channel.peerId);
107
+ this._hydratedChannelIds.add(channel.sessionId);
91
108
  this._acceptedCumulative.set(channel.sessionId, BigInt(channel.authMax));
92
109
  this._spent.set(channel.sessionId, BigInt(channel.tokensDelivered));
93
110
  // Hydrate reserveMax from previousConsumption (repurposed field)
@@ -164,12 +181,42 @@ export class SellerPaymentManager {
164
181
  this._latestAuth.delete(channelId);
165
182
  this._closeRetryCount.delete(channelId);
166
183
  this._closingChannels.delete(channelId);
184
+ this._hydratedChannelIds.delete(channelId);
167
185
  this._reserveMax.delete(channelId);
186
+ this._pendingTopUp.delete(channelId);
187
+ this._blockedChannels.delete(channelId);
168
188
  this._lastSettledCumulative.delete(channelId);
169
189
  this._releaseAcceptedWaiters(channelId);
170
190
  this._activeBuyers.delete(peerId);
171
191
  debugLog(`[SellerPayment] Evicted stale channel ${channelId.slice(0, 18)}... — ${reason}`);
172
192
  }
193
+ /**
194
+ * Restore a persisted SpendingAuth after in-memory cleanup. This prevents the
195
+ * timeout checker from mistaking an active channel with a durable signed auth
196
+ * for a zero-auth zombie and closing it at the already-settled amount.
197
+ */
198
+ _restorePersistedSpendingAuth(channel) {
199
+ const persistedCumulative = BigInt(channel.authMax || '0');
200
+ if (persistedCumulative <= 0n || !channel.latestSpendingAuthSig) {
201
+ return null;
202
+ }
203
+ this._acceptedCumulative.set(channel.sessionId, persistedCumulative);
204
+ this._latestAuth.set(channel.sessionId, {
205
+ spendingAuthSig: channel.latestSpendingAuthSig,
206
+ cumulativeAmount: persistedCumulative,
207
+ metadataHash: '',
208
+ metadata: channel.latestMetadata ?? '',
209
+ });
210
+ this._spent.set(channel.sessionId, BigInt(channel.tokensDelivered || '0'));
211
+ const storedReserveMax = BigInt(channel.previousConsumption || '0');
212
+ if (storedReserveMax > 0n) {
213
+ this._reserveMax.set(channel.sessionId, storedReserveMax);
214
+ }
215
+ this._hydratedChannelIds.delete(channel.sessionId);
216
+ this._notifyAcceptedUpdate(channel.sessionId, persistedCumulative);
217
+ debugLog(`[SellerPayment] Restored persisted SpendingAuth for ${channel.sessionId.slice(0, 18)}... cumulative=${persistedCumulative}`);
218
+ return persistedCumulative;
219
+ }
173
220
  /**
174
221
  * Block until `acceptedCumulative(channelId)` reaches `target`, or `timeoutMs`
175
222
  * elapses. Returns true if the target was reached, false on timeout. Used by
@@ -374,17 +421,43 @@ export class SellerPaymentManager {
374
421
  // Call topUp() on-chain — includes settle of current cumulative spend
375
422
  const { amount: settleAmount, metadata: settleMetadata, sig: settleSig } = this._getSettleParams(channelId);
376
423
  debugLog(`[SellerPayment] Top-up verified: channel=${channelId.slice(0, 18)}... ceiling ${currentReserveMax} → ${newMaxAmount} (settling cumulative=${settleAmount})`);
377
- await this._channelsClient.topUp(this._signer, channelId, settleAmount, settleMetadata, settleSig, newMaxAmount, BigInt(topUpDeadline), payload.spendingAuthSig);
378
- // Update tracking
379
- this._reserveMax.set(channelId, newMaxAmount);
380
- const session = this._channelStore.getChannel(channelId);
381
- if (session) {
382
- session.previousConsumption = newMaxAmount.toString(); // repurposed: stores reserveMax
383
- session.deadline = topUpDeadline;
384
- session.updatedAt = Date.now();
385
- this._channelStore.upsertChannel(session);
424
+ try {
425
+ await this._channelsClient.topUp(this._signer, channelId, settleAmount, settleMetadata, settleSig, newMaxAmount, BigInt(topUpDeadline), payload.spendingAuthSig);
426
+ // Update tracking
427
+ this._hydratedChannelIds.delete(channelId);
428
+ this._reserveMax.set(channelId, newMaxAmount);
429
+ const session = this._channelStore.getChannel(channelId);
430
+ if (session) {
431
+ session.previousConsumption = newMaxAmount.toString(); // repurposed: stores reserveMax
432
+ session.deadline = topUpDeadline;
433
+ session.updatedAt = Date.now();
434
+ this._channelStore.upsertChannel(session);
435
+ }
436
+ debugLog(`[SellerPayment] Top-up completed: channel=${channelId.slice(0, 18)}... new ceiling=${newMaxAmount}`);
437
+ }
438
+ catch (topUpErr) {
439
+ const failureKind = this._classifyTopUpFailure(topUpErr);
440
+ if (failureKind === 'retryable-threshold') {
441
+ // TopUpThresholdNotMet is a timing/settlement race: keep the
442
+ // ReserveAuth pending and retry after a later SpendingAuth raises
443
+ // the settle amount enough to satisfy the contract's 85% gate.
444
+ debugWarn(`[SellerPayment] Top-up threshold not met: channel=${channelId.slice(0, 18)}... ` +
445
+ `error=${this._formatError(topUpErr)} — ` +
446
+ `deferring topUp (will retry after next SpendingAuth)`);
447
+ this._storePendingTopUp(channelId, {
448
+ newMaxAmount,
449
+ deadline: topUpDeadline,
450
+ reserveAuthSig: payload.spendingAuthSig,
451
+ });
452
+ return 'accepted';
453
+ }
454
+ debugWarn(`[SellerPayment] Top-up on-chain failed permanently: channel=${channelId.slice(0, 18)}... ` +
455
+ `kind=${failureKind} error=${this._formatError(topUpErr)} — closing latest auth and rejecting topUp`);
456
+ this._pendingTopUp.delete(channelId);
457
+ this._blockedChannels.add(channelId);
458
+ await this.settleSession(buyerPeerId);
459
+ return 'rejected';
386
460
  }
387
- debugLog(`[SellerPayment] Top-up completed: channel=${channelId.slice(0, 18)}... new ceiling=${newMaxAmount}`);
388
461
  return 'accepted';
389
462
  }
390
463
  else {
@@ -416,15 +489,20 @@ export class SellerPaymentManager {
416
489
  `cumulative=${cumulativeAmount} < spent=${spent} channel=${channelId.slice(0, 18)}...`);
417
490
  return 'rejected';
418
491
  }
419
- // Reject if cumulative exceeds on-chain deposit — the contract would revert
420
- // and we'd lose the last valid auth signature that close() could use.
492
+ // Reject if cumulative exceeds the on-chain deposit. Pending topUps do
493
+ // not count here: until topUp() succeeds, the extra funds are not
494
+ // locked, and accepting an over-reserve SpendingAuth would leave the
495
+ // seller with an auth the contract cannot settle.
421
496
  const currentReserveMax = this._reserveMax.get(channelId) ?? 0n;
497
+ const pendingTopUpForCheck = this._pendingTopUp.get(channelId);
422
498
  if (currentReserveMax > 0n && cumulativeAmount > currentReserveMax) {
423
499
  debugWarn(`[SellerPayment] Rejecting SpendingAuth exceeding deposit ceiling: ` +
424
- `cumulative=${cumulativeAmount} > reserveMax=${currentReserveMax} channel=${channelId.slice(0, 18)}...`);
500
+ `cumulative=${cumulativeAmount} > reserveMax=${currentReserveMax}` +
501
+ `${pendingTopUpForCheck ? ` (pending topUp to ${pendingTopUpForCheck.newMaxAmount})` : ''} channel=${channelId.slice(0, 18)}...`);
425
502
  return 'rejected';
426
503
  }
427
504
  // Update tracking
505
+ this._hydratedChannelIds.delete(channelId);
428
506
  this._acceptedCumulative.set(channelId, cumulativeAmount);
429
507
  this._latestAuth.set(channelId, {
430
508
  spendingAuthSig: payload.spendingAuthSig,
@@ -444,6 +522,12 @@ export class SellerPaymentManager {
444
522
  this._channelStore.upsertChannel(session);
445
523
  }
446
524
  debugLog(`[SellerPayment] Budget updated: channel=${channelId.slice(0, 18)}... cumulative=${cumulativeAmount}`);
525
+ // Retry any deferred topUp now that we have a higher settle amount.
526
+ const pendingTopUp = this._pendingTopUp.get(channelId);
527
+ if (pendingTopUp) {
528
+ const { amount: retrySettleAmount, metadata: retryMetadata, sig: retrySig } = this._getSettleParams(channelId);
529
+ await this._retryPendingTopUp(buyerPeerId, channelId, pendingTopUp, retrySettleAmount, retryMetadata, retrySig);
530
+ }
447
531
  return 'accepted';
448
532
  }
449
533
  }
@@ -452,6 +536,139 @@ export class SellerPaymentManager {
452
536
  return 'rejected';
453
537
  }
454
538
  }
539
+ _storePendingTopUp(channelId, pending) {
540
+ const existing = this._pendingTopUp.get(channelId);
541
+ if (!existing || pending.newMaxAmount >= existing.newMaxAmount) {
542
+ this._pendingTopUp.set(channelId, pending);
543
+ }
544
+ }
545
+ _formatError(err) {
546
+ const text = this._flattenErrorText(err);
547
+ const formatted = text.length > 0 ? text : String(err);
548
+ return formatted.length > 500 ? `${formatted.slice(0, 500)}…` : formatted;
549
+ }
550
+ _classifyTopUpFailure(err) {
551
+ const text = this._flattenErrorText(err).toLowerCase();
552
+ if (text.includes('topupthresholdnotmet') || text.includes(TOP_UP_THRESHOLD_NOT_MET_SELECTOR)) {
553
+ return 'retryable-threshold';
554
+ }
555
+ if (text.includes('insufficientbalance') || text.includes(INSUFFICIENT_BALANCE_SELECTOR)) {
556
+ return 'insufficient-balance';
557
+ }
558
+ return 'non-retryable';
559
+ }
560
+ _flattenErrorText(value, seen = new Set(), depth = 0) {
561
+ if (value == null || depth > 5)
562
+ return '';
563
+ if (typeof value === 'string')
564
+ return value;
565
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint')
566
+ return String(value);
567
+ if (typeof value !== 'object')
568
+ return '';
569
+ if (seen.has(value))
570
+ return '';
571
+ seen.add(value);
572
+ const parts = [];
573
+ if (value instanceof Error) {
574
+ parts.push(value.name, value.message);
575
+ if ('cause' in value) {
576
+ parts.push(this._flattenErrorText(value.cause, seen, depth + 1));
577
+ }
578
+ }
579
+ for (const key of Object.getOwnPropertyNames(value)) {
580
+ if (key === 'stack')
581
+ continue;
582
+ const nested = value[key];
583
+ parts.push(key);
584
+ parts.push(this._flattenErrorText(nested, seen, depth + 1));
585
+ }
586
+ return parts.filter(Boolean).join(' ');
587
+ }
588
+ async _settleLatestAuth(channelId, reason, { respectMinSettleDelta = true } = {}) {
589
+ const { amount, metadata, sig } = this._getSettleParams(channelId);
590
+ if (amount <= 0n || sig === '0x') {
591
+ debugLog(`[SellerPayment] Skipping settle after ${reason}: channel=${channelId.slice(0, 18)}... no signed spend`);
592
+ return;
593
+ }
594
+ let delta = null;
595
+ if (respectMinSettleDelta) {
596
+ // Skip the getSession RPC entirely when our local cumulative hasn't
597
+ // moved since we last settled this channel — the contract would revert
598
+ // with InvalidAmount (strict `>` check) and we'd waste an RPC round-trip.
599
+ const lastSettled = this._lastSettledCumulative.get(channelId);
600
+ if (lastSettled !== undefined && amount <= lastSettled) {
601
+ debugLog(`[SellerPayment] Skip settle ${channelId.slice(0, 18)}... — cumulative unchanged since last settle (${amount})`);
602
+ return;
603
+ }
604
+ // Cache miss (e.g. after restart) or local cumulative has advanced —
605
+ // confirm against on-chain state in case another process settled.
606
+ let onChainSettled;
607
+ try {
608
+ const onChain = await this._channelsClient.getSession(channelId);
609
+ onChainSettled = onChain.settled;
610
+ }
611
+ catch (err) {
612
+ debugWarn(`[SellerPayment] getSession failed for ${channelId.slice(0, 18)}...: ${err instanceof Error ? err.message : err} — attempting settle anyway`);
613
+ onChainSettled = 0n;
614
+ }
615
+ delta = amount - onChainSettled;
616
+ if (delta <= 0n) {
617
+ // Resync the cache so we stop hitting the RPC on every idle tick.
618
+ this._lastSettledCumulative.set(channelId, onChainSettled);
619
+ debugLog(`[SellerPayment] Skip settle ${channelId.slice(0, 18)}... — already settled on-chain (local=${amount}, onChain=${onChainSettled})`);
620
+ return;
621
+ }
622
+ if (delta < this._minSettleDelta) {
623
+ // Mark this cumulative as a no-op so the next tick short-circuits
624
+ // without re-querying getSession until amount actually advances.
625
+ this._lastSettledCumulative.set(channelId, amount);
626
+ debugLog(`[SellerPayment] Skip settle ${channelId.slice(0, 18)}... — delta=${delta} below minSettleDelta=${this._minSettleDelta}`);
627
+ return;
628
+ }
629
+ }
630
+ const deltaText = delta === null ? '' : ` delta=${delta}`;
631
+ debugLog(`[SellerPayment] Settling channel ${channelId.slice(0, 18)}... cumulative=${amount}${deltaText} (${reason})`);
632
+ try {
633
+ await this._channelsClient.settle(this._signer, channelId, amount, metadata, sig);
634
+ this._lastSettledCumulative.set(channelId, amount);
635
+ debugLog(`[SellerPayment] Settled channel ${channelId.slice(0, 18)}... — channel remains open`);
636
+ }
637
+ catch (err) {
638
+ debugWarn(`[SellerPayment] Failed to settle channel after ${reason}: ${err instanceof Error ? err.message : err}`);
639
+ }
640
+ }
641
+ async _retryPendingTopUp(buyerPeerId, channelId, pendingTopUp, settleAmount, settleMetadata, settleSig) {
642
+ this._pendingTopUp.delete(channelId);
643
+ debugLog(`[SellerPayment] Retrying deferred topUp: channel=${channelId.slice(0, 18)}... settling=${settleAmount} newMax=${pendingTopUp.newMaxAmount}`);
644
+ try {
645
+ await this._channelsClient.topUp(this._signer, channelId, settleAmount, settleMetadata, settleSig, pendingTopUp.newMaxAmount, BigInt(pendingTopUp.deadline), pendingTopUp.reserveAuthSig);
646
+ this._reserveMax.set(channelId, pendingTopUp.newMaxAmount);
647
+ const topUpSession = this._channelStore.getChannel(channelId);
648
+ if (topUpSession) {
649
+ topUpSession.previousConsumption = pendingTopUp.newMaxAmount.toString();
650
+ topUpSession.deadline = pendingTopUp.deadline;
651
+ topUpSession.updatedAt = Date.now();
652
+ this._channelStore.upsertChannel(topUpSession);
653
+ }
654
+ debugLog(`[SellerPayment] Deferred topUp succeeded: channel=${channelId.slice(0, 18)}... new ceiling=${pendingTopUp.newMaxAmount}`);
655
+ return 'succeeded';
656
+ }
657
+ catch (retryErr) {
658
+ const failureKind = this._classifyTopUpFailure(retryErr);
659
+ if (failureKind === 'retryable-threshold') {
660
+ debugWarn(`[SellerPayment] Deferred topUp threshold not met: channel=${channelId.slice(0, 18)}... ` +
661
+ `error=${this._formatError(retryErr)} — keeping pending`);
662
+ this._storePendingTopUp(channelId, pendingTopUp);
663
+ return 'retryable-failure';
664
+ }
665
+ debugWarn(`[SellerPayment] Deferred topUp failed permanently: channel=${channelId.slice(0, 18)}... ` +
666
+ `kind=${failureKind} error=${this._formatError(retryErr)} — closing latest auth and dropping pending topUp`);
667
+ this._blockedChannels.add(channelId);
668
+ await this.settleSession(buyerPeerId);
669
+ return 'permanent-failure';
670
+ }
671
+ }
455
672
  async _recoverOnChainSession(buyerPeerId, buyerEvmAddr, payload, cumulativeAmount, paymentMux, channelsDomain) {
456
673
  const channelId = payload.channelId;
457
674
  const onChainState = classifyOnChainChannel(await this._channelsClient.getSession(channelId));
@@ -512,6 +729,7 @@ export class SellerPaymentManager {
512
729
  }
513
730
  _activateSession(session, buyerPeerId, cumulativeAmount, reserveMaxAmount, spent, latestAuth) {
514
731
  this._channelStore.upsertChannel(session);
732
+ this._hydratedChannelIds.delete(session.sessionId);
515
733
  this._acceptedCumulative.set(session.sessionId, cumulativeAmount);
516
734
  this._reserveMax.set(session.sessionId, reserveMaxAmount);
517
735
  this._spent.set(session.sessionId, spent);
@@ -562,6 +780,13 @@ export class SellerPaymentManager {
562
780
  debugWarn(`[SellerPayment] validateAndAcceptAuth: cumulative decreased from ${existingCumulative} to ${newCumulative}`);
563
781
  return false;
564
782
  }
783
+ const reserveMax = this._reserveMax.get(channelId) ?? 0n;
784
+ if (reserveMax > 0n && newCumulative > reserveMax) {
785
+ debugWarn(`[SellerPayment] validateAndAcceptAuth: cumulative exceeds reserve ceiling ` +
786
+ `(${newCumulative} > ${reserveMax}) channel=${channelId.slice(0, 18)}...`);
787
+ return false;
788
+ }
789
+ this._hydratedChannelIds.delete(channelId);
565
790
  // Update if strictly greater
566
791
  if (newCumulative > existingCumulative) {
567
792
  this._acceptedCumulative.set(channelId, newCumulative);
@@ -642,50 +867,7 @@ export class SellerPaymentManager {
642
867
  debugLog(`[SellerPayment] Zero-cumulative channel ${channelId.slice(0, 18)}... — deferring to timeout checker`);
643
868
  }
644
869
  else if (settleOnly) {
645
- if (amount === 0n)
646
- return;
647
- // Skip the getSession RPC entirely when our local cumulative hasn't
648
- // moved since we last settled this channel — the contract would revert
649
- // with InvalidAmount (strict `>` check) and we'd waste an RPC round-trip.
650
- const lastSettled = this._lastSettledCumulative.get(channelId);
651
- if (lastSettled !== undefined && amount <= lastSettled) {
652
- debugLog(`[SellerPayment] Skip settle ${channelId.slice(0, 18)}... — cumulative unchanged since last settle (${amount})`);
653
- return;
654
- }
655
- // Cache miss (e.g. after restart) or local cumulative has advanced —
656
- // confirm against on-chain state in case another process settled.
657
- let onChainSettled;
658
- try {
659
- const onChain = await this._channelsClient.getSession(channelId);
660
- onChainSettled = onChain.settled;
661
- }
662
- catch (err) {
663
- debugWarn(`[SellerPayment] getSession failed for ${channelId.slice(0, 18)}...: ${err instanceof Error ? err.message : err} — attempting settle anyway`);
664
- onChainSettled = 0n;
665
- }
666
- const delta = amount - onChainSettled;
667
- if (delta <= 0n) {
668
- // Resync the cache so we stop hitting the RPC on every idle tick.
669
- this._lastSettledCumulative.set(channelId, onChainSettled);
670
- debugLog(`[SellerPayment] Skip settle ${channelId.slice(0, 18)}... — already settled on-chain (local=${amount}, onChain=${onChainSettled})`);
671
- return;
672
- }
673
- if (delta < this._minSettleDelta) {
674
- // Mark this cumulative as a no-op so the next tick short-circuits
675
- // without re-querying getSession until amount actually advances.
676
- this._lastSettledCumulative.set(channelId, amount);
677
- debugLog(`[SellerPayment] Skip settle ${channelId.slice(0, 18)}... — delta=${delta} below minSettleDelta=${this._minSettleDelta}`);
678
- return;
679
- }
680
- debugLog(`[SellerPayment] Settling channel ${channelId.slice(0, 18)}... cumulative=${amount} delta=${delta} (keeping open)`);
681
- try {
682
- await this._channelsClient.settle(this._signer, channelId, amount, metadata, sig);
683
- this._lastSettledCumulative.set(channelId, amount);
684
- debugLog(`[SellerPayment] Settled channel ${channelId.slice(0, 18)}... — channel remains open`);
685
- }
686
- catch (err) {
687
- debugWarn(`[SellerPayment] Failed to settle channel: ${err instanceof Error ? err.message : err}`);
688
- }
870
+ await this._settleLatestAuth(channelId, 'idle settle', { respectMinSettleDelta: true });
689
871
  return;
690
872
  }
691
873
  else {
@@ -722,7 +904,11 @@ export class SellerPaymentManager {
722
904
  this._latestAuth.delete(channelId);
723
905
  this._closeRetryCount.delete(channelId);
724
906
  this._closingChannels.delete(channelId);
907
+ this._reserveMax.delete(channelId);
908
+ this._pendingTopUp.delete(channelId);
909
+ this._blockedChannels.delete(channelId);
725
910
  this._lastSettledCumulative.delete(channelId);
911
+ this._hydratedChannelIds.delete(channelId);
726
912
  this._releaseAcceptedWaiters(channelId);
727
913
  this._activeBuyers.delete(buyerPeerId);
728
914
  }
@@ -750,17 +936,22 @@ export class SellerPaymentManager {
750
936
  // ── Stale session cleanup ────────────────────────────────────
751
937
  /**
752
938
  * Check for stale sessions and attempt to close them.
753
- * The seller can only close() with a valid SpendingAuth — it cannot
754
- * requestClose or withdraw (those are buyer-only on-chain).
755
- * If the seller has no auths, the session remains open until the buyer
756
- * calls requestClose → withdraw on-chain.
939
+ * - Channels with a buyer SpendingAuth go through `settleSession()`.
940
+ * - Zombie channels (buyer gone, no auth, deadline elapsed) can be closed
941
+ * on-chain with `finalAmount == channel.settled`, which intentionally
942
+ * skips SpendingAuth verification and lets the seller release the lock,
943
+ * decrement activeChannelCount, and advance channel stats without buyer
944
+ * cooperation.
757
945
  * Called periodically and on startup for recovery.
758
946
  */
759
947
  async checkTimeouts() {
760
948
  const nowSecs = Math.floor(Date.now() / 1000);
761
949
  const activeChannels = this._channelStore.getActiveChannels('seller');
762
950
  for (const channel of activeChannels) {
763
- const accepted = this._acceptedCumulative.get(channel.sessionId) ?? 0n;
951
+ let accepted = this._acceptedCumulative.get(channel.sessionId) ?? 0n;
952
+ if (accepted === 0n) {
953
+ accepted = this._restorePersistedSpendingAuth(channel) ?? accepted;
954
+ }
764
955
  try {
765
956
  // Validate on-chain state — evict if channel no longer exists
766
957
  const onChainState = classifyOnChainChannel(await this._channelsClient.getSession(channel.sessionId));
@@ -768,16 +959,28 @@ export class SellerPaymentManager {
768
959
  this._evictStaleChannel(channel.sessionId, channel.peerId, `periodic check: on-chain status=${onChainState.exists ? onChainState.status : 'missing'}`);
769
960
  continue;
770
961
  }
962
+ const buyerDisconnected = !this._activeBuyers.has(channel.peerId);
963
+ const hydratedZeroAuthExpired = accepted === 0n
964
+ && this._hydratedChannelIds.has(channel.sessionId)
965
+ && nowSecs > channel.deadline;
771
966
  // If we have auths and the buyer is disconnected, try to close
772
- if (accepted > 0n && !this._activeBuyers.has(channel.peerId)) {
967
+ if (accepted > 0n && buyerDisconnected) {
773
968
  debugLog(`[SellerPayment] Channel ${channel.sessionId.slice(0, 18)}... buyer disconnected — attempting close`);
774
969
  await this.settleSession(channel.peerId);
970
+ continue;
775
971
  }
776
- // If no auths and buyer disconnected, nothing the seller can do on-chain.
777
- // The buyer must call requestClose → withdraw. We just clean up locally
778
- // after a reasonable period (e.g. deadline passed).
779
- if (accepted === 0n && !this._activeBuyers.has(channel.peerId) && nowSecs > channel.deadline) {
780
- this._evictStaleChannel(channel.sessionId, channel.peerId, 'no auths, past deadline', 'timeout');
972
+ // No auths, buyer gone, deadline elapsed: close with the current
973
+ // on-chain settled amount. The contract skips signature verification
974
+ // when finalAmount == settled, so this safely cleans up zombie
975
+ // channels without claiming any unproven spend.
976
+ if (accepted === 0n && (buyerDisconnected || hydratedZeroAuthExpired) && nowSecs > channel.deadline) {
977
+ if (onChainState.status !== 'active') {
978
+ // 'unknown' means the RPC returned partial data. Evict locally
979
+ // rather than risking a close() against an ambiguous channel.
980
+ this._evictStaleChannel(channel.sessionId, channel.peerId, 'no auths, past deadline, on-chain status unknown', 'timeout');
981
+ continue;
982
+ }
983
+ await this._closeWithoutAuth(channel.sessionId, channel.peerId, onChainState.channel.settled);
781
984
  }
782
985
  }
783
986
  catch (err) {
@@ -785,6 +988,38 @@ export class SellerPaymentManager {
785
988
  }
786
989
  }
787
990
  }
991
+ /**
992
+ * Close a zombie channel (buyer gone, no signed auth) using the current
993
+ * on-chain settled cumulative as finalAmount. This intentionally settles no
994
+ * additional spend, but still marks the channel closed and releases the
995
+ * remaining reserved deposit back to the buyer.
996
+ */
997
+ async _closeWithoutAuth(channelId, peerId, onChainSettled) {
998
+ if (this._closingChannels.has(channelId)) {
999
+ debugLog(`[SellerPayment] Close already in flight for ${channelId.slice(0, 18)}... — skipping zombie close`);
1000
+ return;
1001
+ }
1002
+ const retries = this._closeRetryCount.get(channelId) ?? 0;
1003
+ if (retries >= SellerPaymentManager.MAX_CLOSE_RETRIES) {
1004
+ debugWarn(`[SellerPayment] Zombie close failed ${retries} times for ${channelId.slice(0, 18)}... — falling back to local eviction`);
1005
+ this._evictStaleChannel(channelId, peerId, 'no auths, past deadline, close retries exhausted', 'timeout');
1006
+ return;
1007
+ }
1008
+ this._closingChannels.add(channelId);
1009
+ debugLog(`[SellerPayment] Closing zombie channel ${channelId.slice(0, 18)}... finalAmount=${onChainSettled} (attempt ${retries + 1}/${SellerPaymentManager.MAX_CLOSE_RETRIES})`);
1010
+ try {
1011
+ await this._channelsClient.close(this._signer, channelId, onChainSettled, '0x', '0x');
1012
+ this._closeRetryCount.delete(channelId);
1013
+ this._evictStaleChannel(channelId, peerId, 'zombie closed on-chain', 'settled');
1014
+ }
1015
+ catch (err) {
1016
+ this._closeRetryCount.set(channelId, retries + 1);
1017
+ debugWarn(`[SellerPayment] Zombie close failed (attempt ${retries + 1}): ${err instanceof Error ? err.message : err}`);
1018
+ }
1019
+ finally {
1020
+ this._closingChannels.delete(channelId);
1021
+ }
1022
+ }
788
1023
  // ── Queries ───────────────────────────────────────────────────
789
1024
  hasSession(buyerPeerId) {
790
1025
  return this._activeBuyers.has(buyerPeerId);
@@ -805,6 +1040,18 @@ export class SellerPaymentManager {
805
1040
  getReserveMax(sessionId) {
806
1041
  return this._reserveMax.get(sessionId) ?? 0n;
807
1042
  }
1043
+ /** Get the effective reserve max for serving decisions. Pending topUps do not count until confirmed on-chain. */
1044
+ getEffectiveReserveMax(sessionId) {
1045
+ return this.getReserveMax(sessionId);
1046
+ }
1047
+ /** Whether this channel is blocked from serving more paid work. */
1048
+ isChannelBlocked(sessionId) {
1049
+ return this._blockedChannels.has(sessionId);
1050
+ }
1051
+ /** Whether a topUp is pending (on-chain call deferred). */
1052
+ hasPendingTopUp(sessionId) {
1053
+ return this._pendingTopUp.has(sessionId);
1054
+ }
808
1055
  static DEFAULT_SUGGESTED_AMOUNT = 1000000n; // $1.00 — matches contract FIRST_SIGN_CAP and buyer default
809
1056
  /**
810
1057
  * Build the PaymentRequired payload for a buyer that doesn't have a session.
@@ -863,7 +1110,10 @@ export class SellerPaymentManager {
863
1110
  this._closeRetryCount.delete(channelId);
864
1111
  this._closingChannels.delete(channelId);
865
1112
  this._reserveMax.delete(channelId);
1113
+ this._pendingTopUp.delete(channelId);
1114
+ this._blockedChannels.delete(channelId);
866
1115
  this._lastSettledCumulative.delete(channelId);
1116
+ this._hydratedChannelIds.delete(channelId);
867
1117
  this._releaseAcceptedWaiters(channelId);
868
1118
  // Find and remove buyer from active set
869
1119
  const channel = this._channelStore.getChannel(channelId);