@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.
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +1 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +21 -2
- package/dist/node.js.map +1 -1
- package/dist/payments/buyer-payment-manager.d.ts +4 -0
- package/dist/payments/buyer-payment-manager.d.ts.map +1 -1
- package/dist/payments/buyer-payment-manager.js +107 -91
- package/dist/payments/buyer-payment-manager.js.map +1 -1
- package/dist/payments/chain-config.d.ts +2 -0
- package/dist/payments/chain-config.d.ts.map +1 -1
- package/dist/payments/chain-config.js +2 -0
- package/dist/payments/chain-config.js.map +1 -1
- package/dist/payments/evm/emissions-client.d.ts +1 -0
- package/dist/payments/evm/emissions-client.d.ts.map +1 -1
- package/dist/payments/evm/emissions-client.js +6 -0
- package/dist/payments/evm/emissions-client.js.map +1 -1
- package/dist/payments/seller-payment-manager.d.ts +46 -5
- package/dist/payments/seller-payment-manager.d.ts.map +1 -1
- package/dist/payments/seller-payment-manager.js +319 -69
- package/dist/payments/seller-payment-manager.js.map +1 -1
- package/dist/reputation/on-chain-reputation.d.ts +46 -35
- package/dist/reputation/on-chain-reputation.d.ts.map +1 -1
- package/dist/reputation/on-chain-reputation.js +174 -67
- package/dist/reputation/on-chain-reputation.js.map +1 -1
- package/dist/seller-request-handler.d.ts.map +1 -1
- package/dist/seller-request-handler.js +13 -6
- package/dist/seller-request-handler.js.map +1 -1
- package/dist/types/peer.d.ts +13 -26
- package/dist/types/peer.d.ts.map +1 -1
- 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":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EACV,mBAAmB,EACnB,sBAAsB,EACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAU1D,OAAO,EAAE,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAGtE,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,uBAAuB,EAAE,MAAM,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,2EAA2E;IAC3E,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAKD,wDAAwD;AACxD,eAAO,MAAM,4BAA4B,SAAS,CAAC;
|
|
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
|
-
/**
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
session
|
|
383
|
-
session
|
|
384
|
-
|
|
385
|
-
|
|
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
|
|
420
|
-
//
|
|
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}
|
|
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
|
-
|
|
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
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
//
|
|
777
|
-
//
|
|
778
|
-
//
|
|
779
|
-
|
|
780
|
-
|
|
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);
|