@antseed/node 0.2.27 → 0.2.28
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/README.md +14 -13
- package/dist/buyer-request-handler.d.ts +41 -0
- package/dist/buyer-request-handler.d.ts.map +1 -0
- package/dist/buyer-request-handler.js +254 -0
- package/dist/buyer-request-handler.js.map +1 -0
- package/dist/discovery/announcer.d.ts +5 -4
- package/dist/discovery/announcer.d.ts.map +1 -1
- package/dist/discovery/announcer.js +11 -18
- package/dist/discovery/announcer.js.map +1 -1
- package/dist/discovery/index.d.ts +0 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/index.js +0 -1
- package/dist/discovery/index.js.map +1 -1
- package/dist/discovery/metadata-codec.d.ts +2 -2
- package/dist/discovery/metadata-codec.d.ts.map +1 -1
- package/dist/discovery/metadata-codec.js +47 -72
- package/dist/discovery/metadata-codec.js.map +1 -1
- package/dist/discovery/metadata-validator.js +6 -6
- package/dist/discovery/metadata-validator.js.map +1 -1
- package/dist/discovery/peer-lookup.d.ts.map +1 -1
- package/dist/discovery/peer-lookup.js +1 -2
- package/dist/discovery/peer-lookup.js.map +1 -1
- package/dist/discovery/peer-metadata.d.ts +3 -5
- package/dist/discovery/peer-metadata.d.ts.map +1 -1
- package/dist/discovery/peer-metadata.js +1 -1
- package/dist/discovery/reputation-verifier.d.ts +2 -25
- package/dist/discovery/reputation-verifier.d.ts.map +1 -1
- package/dist/discovery/reputation-verifier.js +2 -48
- package/dist/discovery/reputation-verifier.js.map +1 -1
- package/dist/discovery/stats-verifier.d.ts +27 -0
- package/dist/discovery/stats-verifier.d.ts.map +1 -0
- package/dist/discovery/stats-verifier.js +38 -0
- package/dist/discovery/stats-verifier.js.map +1 -0
- package/dist/index.d.ts +10 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -5
- package/dist/index.js.map +1 -1
- package/dist/metering/index.d.ts +1 -0
- package/dist/metering/index.d.ts.map +1 -1
- package/dist/metering/index.js +1 -0
- package/dist/metering/index.js.map +1 -1
- package/dist/metering/receipt-generator.d.ts +4 -4
- package/dist/metering/receipt-generator.d.ts.map +1 -1
- package/dist/metering/receipt-verifier.d.ts +6 -6
- package/dist/metering/receipt-verifier.d.ts.map +1 -1
- package/dist/metering/receipt-verifier.js +1 -1
- package/dist/metering/seller-session-tracker.d.ts +91 -0
- package/dist/metering/seller-session-tracker.d.ts.map +1 -0
- package/dist/metering/seller-session-tracker.js +261 -0
- package/dist/metering/seller-session-tracker.js.map +1 -0
- package/dist/metering/storage.d.ts +11 -5
- package/dist/metering/storage.d.ts.map +1 -1
- package/dist/metering/storage.js +28 -80
- package/dist/metering/storage.js.map +1 -1
- package/dist/node.d.ts +69 -117
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +240 -1269
- package/dist/node.js.map +1 -1
- package/dist/p2p/connection-auth.d.ts +2 -1
- package/dist/p2p/connection-auth.d.ts.map +1 -1
- package/dist/p2p/connection-auth.js +6 -6
- package/dist/p2p/connection-auth.js.map +1 -1
- package/dist/p2p/connection-manager.d.ts +3 -2
- package/dist/p2p/connection-manager.d.ts.map +1 -1
- package/dist/p2p/connection-manager.js +6 -6
- package/dist/p2p/connection-manager.js.map +1 -1
- package/dist/p2p/identity.d.ts +22 -15
- package/dist/p2p/identity.d.ts.map +1 -1
- package/dist/p2p/identity.js +66 -51
- package/dist/p2p/identity.js.map +1 -1
- package/dist/p2p/index.d.ts +1 -1
- package/dist/p2p/index.d.ts.map +1 -1
- package/dist/p2p/index.js +1 -1
- package/dist/p2p/index.js.map +1 -1
- package/dist/p2p/payment-codec.d.ts +4 -8
- package/dist/p2p/payment-codec.d.ts.map +1 -1
- package/dist/p2p/payment-codec.js +27 -57
- package/dist/p2p/payment-codec.js.map +1 -1
- package/dist/p2p/payment-mux.d.ts +4 -10
- package/dist/p2p/payment-mux.d.ts.map +1 -1
- package/dist/p2p/payment-mux.js +11 -33
- package/dist/p2p/payment-mux.js.map +1 -1
- package/dist/payments/balance-manager.d.ts +2 -2
- package/dist/payments/balance-manager.d.ts.map +1 -1
- package/dist/payments/balance-manager.js +5 -5
- package/dist/payments/balance-manager.js.map +1 -1
- package/dist/payments/buyer-payment-manager.d.ts +154 -21
- package/dist/payments/buyer-payment-manager.d.ts.map +1 -1
- package/dist/payments/buyer-payment-manager.js +540 -166
- package/dist/payments/buyer-payment-manager.js.map +1 -1
- package/dist/payments/buyer-payment-negotiator.d.ts +84 -0
- package/dist/payments/buyer-payment-negotiator.d.ts.map +1 -0
- package/dist/payments/buyer-payment-negotiator.js +624 -0
- package/dist/payments/buyer-payment-negotiator.js.map +1 -0
- package/dist/payments/chain-config.d.ts +10 -4
- package/dist/payments/chain-config.d.ts.map +1 -1
- package/dist/payments/chain-config.js +19 -9
- package/dist/payments/chain-config.js.map +1 -1
- package/dist/payments/channel-session-state.d.ts +13 -0
- package/dist/payments/channel-session-state.d.ts.map +1 -0
- package/dist/payments/channel-session-state.js +25 -0
- package/dist/payments/channel-session-state.js.map +1 -0
- package/dist/payments/channel-store.d.ts +87 -0
- package/dist/payments/channel-store.d.ts.map +1 -0
- package/dist/payments/channel-store.js +276 -0
- package/dist/payments/channel-store.js.map +1 -0
- package/dist/payments/evm/ants-token-client.d.ts +1 -1
- package/dist/payments/evm/ants-token-client.d.ts.map +1 -1
- package/dist/payments/evm/ants-token-client.js +3 -4
- package/dist/payments/evm/ants-token-client.js.map +1 -1
- package/dist/payments/evm/base-evm-client.d.ts +10 -1
- package/dist/payments/evm/base-evm-client.d.ts.map +1 -1
- package/dist/payments/evm/base-evm-client.js +34 -1
- package/dist/payments/evm/base-evm-client.js.map +1 -1
- package/dist/payments/evm/channels-client.d.ts +51 -0
- package/dist/payments/evm/channels-client.d.ts.map +1 -0
- package/dist/payments/evm/channels-client.js +101 -0
- package/dist/payments/evm/channels-client.js.map +1 -0
- package/dist/payments/evm/deposits-client.d.ts +30 -0
- package/dist/payments/evm/deposits-client.d.ts.map +1 -0
- package/dist/payments/evm/deposits-client.js +78 -0
- package/dist/payments/evm/deposits-client.js.map +1 -0
- package/dist/payments/evm/emissions-client.d.ts +3 -4
- package/dist/payments/evm/emissions-client.d.ts.map +1 -1
- package/dist/payments/evm/emissions-client.js +11 -30
- package/dist/payments/evm/emissions-client.js.map +1 -1
- package/dist/payments/evm/identity-client.d.ts +10 -23
- package/dist/payments/evm/identity-client.d.ts.map +1 -1
- package/dist/payments/evm/identity-client.js +43 -100
- package/dist/payments/evm/identity-client.js.map +1 -1
- package/dist/payments/evm/keypair.d.ts +3 -14
- package/dist/payments/evm/keypair.d.ts.map +1 -1
- package/dist/payments/evm/keypair.js +4 -20
- package/dist/payments/evm/keypair.js.map +1 -1
- package/dist/payments/evm/sessions-client.d.ts +30 -0
- package/dist/payments/evm/sessions-client.d.ts.map +1 -0
- package/dist/payments/evm/sessions-client.js +61 -0
- package/dist/payments/evm/sessions-client.js.map +1 -0
- package/dist/payments/evm/signatures.d.ts +43 -12
- package/dist/payments/evm/signatures.d.ts.map +1 -1
- package/dist/payments/evm/signatures.js +62 -45
- package/dist/payments/evm/signatures.js.map +1 -1
- package/dist/payments/evm/staking-client.d.ts +24 -0
- package/dist/payments/evm/staking-client.d.ts.map +1 -0
- package/dist/payments/evm/staking-client.js +54 -0
- package/dist/payments/evm/staking-client.js.map +1 -0
- package/dist/payments/evm/stats-client.d.ts +20 -0
- package/dist/payments/evm/stats-client.d.ts.map +1 -0
- package/dist/payments/evm/stats-client.js +25 -0
- package/dist/payments/evm/stats-client.js.map +1 -0
- package/dist/payments/index.d.ts +17 -10
- package/dist/payments/index.d.ts.map +1 -1
- package/dist/payments/index.js +15 -8
- package/dist/payments/index.js.map +1 -1
- package/dist/payments/pricing.d.ts +25 -0
- package/dist/payments/pricing.d.ts.map +1 -0
- package/dist/payments/pricing.js +33 -0
- package/dist/payments/pricing.js.map +1 -0
- package/dist/payments/readiness.d.ts +4 -3
- package/dist/payments/readiness.d.ts.map +1 -1
- package/dist/payments/readiness.js +11 -18
- package/dist/payments/readiness.js.map +1 -1
- package/dist/payments/seller-payment-manager.d.ts +72 -47
- package/dist/payments/seller-payment-manager.d.ts.map +1 -1
- package/dist/payments/seller-payment-manager.js +558 -275
- package/dist/payments/seller-payment-manager.js.map +1 -1
- package/dist/payments/session-store.d.ts +3 -0
- package/dist/payments/session-store.d.ts.map +1 -1
- package/dist/payments/session-store.js +31 -2
- package/dist/payments/session-store.js.map +1 -1
- package/dist/payments/types.d.ts +5 -3
- package/dist/payments/types.d.ts.map +1 -1
- package/dist/proxy/proxy-mux.d.ts.map +1 -1
- package/dist/proxy/proxy-mux.js +3 -2
- package/dist/proxy/proxy-mux.js.map +1 -1
- package/dist/proxy/request-codec.d.ts.map +1 -1
- package/dist/proxy/request-codec.js +3 -0
- package/dist/proxy/request-codec.js.map +1 -1
- package/dist/reputation/rating-manager.d.ts.map +1 -1
- package/dist/reputation/rating-manager.js +2 -4
- package/dist/reputation/rating-manager.js.map +1 -1
- package/dist/reputation/report-manager.d.ts.map +1 -1
- package/dist/reputation/report-manager.js +2 -4
- package/dist/reputation/report-manager.js.map +1 -1
- package/dist/routing/default-router.d.ts.map +1 -1
- package/dist/routing/default-router.js +4 -9
- package/dist/routing/default-router.js.map +1 -1
- package/dist/seller-request-handler.d.ts +54 -0
- package/dist/seller-request-handler.d.ts.map +1 -0
- package/dist/seller-request-handler.js +359 -0
- package/dist/seller-request-handler.js.map +1 -0
- package/dist/storage/migrate.d.ts +13 -0
- package/dist/storage/migrate.d.ts.map +1 -0
- package/dist/storage/migrate.js +28 -0
- package/dist/storage/migrate.js.map +1 -0
- package/dist/storage/migrations/channels/001_create_tables.d.ts +3 -0
- package/dist/storage/migrations/channels/001_create_tables.d.ts.map +1 -0
- package/dist/storage/migrations/channels/001_create_tables.js +45 -0
- package/dist/storage/migrations/channels/001_create_tables.js.map +1 -0
- package/dist/storage/migrations/channels/002_add_auth_sig_columns.d.ts +3 -0
- package/dist/storage/migrations/channels/002_add_auth_sig_columns.d.ts.map +1 -0
- package/dist/storage/migrations/channels/002_add_auth_sig_columns.js +19 -0
- package/dist/storage/migrations/channels/002_add_auth_sig_columns.js.map +1 -0
- package/dist/storage/migrations/channels/index.d.ts +3 -0
- package/dist/storage/migrations/channels/index.d.ts.map +1 -0
- package/dist/storage/migrations/channels/index.js +4 -0
- package/dist/storage/migrations/channels/index.js.map +1 -0
- package/dist/storage/migrations/metering/001_create_tables.d.ts +3 -0
- package/dist/storage/migrations/metering/001_create_tables.d.ts.map +1 -0
- package/dist/storage/migrations/metering/001_create_tables.js +80 -0
- package/dist/storage/migrations/metering/001_create_tables.js.map +1 -0
- package/dist/storage/migrations/metering/index.d.ts +3 -0
- package/dist/storage/migrations/metering/index.d.ts.map +1 -0
- package/dist/storage/migrations/metering/index.js +3 -0
- package/dist/storage/migrations/metering/index.js.map +1 -0
- package/dist/types/capability.d.ts +1 -1
- package/dist/types/metering.d.ts +1 -1
- package/dist/types/peer.d.ts +10 -11
- package/dist/types/peer.d.ts.map +1 -1
- package/dist/types/peer.js +7 -3
- package/dist/types/peer.js.map +1 -1
- package/dist/types/protocol.d.ts +22 -70
- package/dist/types/protocol.d.ts.map +1 -1
- package/dist/types/protocol.js +1 -3
- package/dist/types/protocol.js.map +1 -1
- package/dist/types/rating.d.ts +1 -1
- package/dist/types/rating.d.ts.map +1 -1
- package/dist/types/report.d.ts +1 -1
- package/dist/types/report.d.ts.map +1 -1
- package/dist/utils/response-usage.d.ts +10 -0
- package/dist/utils/response-usage.d.ts.map +1 -0
- package/dist/utils/response-usage.js +34 -0
- package/dist/utils/response-usage.js.map +1 -0
- package/package.json +3 -3
|
@@ -1,293 +1,528 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
import { verifyTypedData } from 'ethers';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { SPENDING_AUTH_TYPES, makeEscrowDomain, buildReceiptMessage, buildAckMessage, signMessageEd25519, verifyMessageEd25519, } from './evm/signatures.js';
|
|
6
|
-
import { bytesToHex, hexToBytes } from '../utils/hex.js';
|
|
2
|
+
import { ChannelsClient } from './evm/channels-client.js';
|
|
3
|
+
import { SPENDING_AUTH_TYPES, RESERVE_AUTH_TYPES, makeChannelsDomain, encodeMetadata, ZERO_METADATA, } from './evm/signatures.js';
|
|
7
4
|
import { debugLog, debugWarn } from '../utils/debug.js';
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
import { peerIdToAddress } from '../types/peer.js';
|
|
6
|
+
import { classifyOnChainChannel, matchesChannelParties } from './channel-session-state.js';
|
|
7
|
+
/** Default minimum budget per request: $0.50 USDC (base units). */
|
|
8
|
+
const DEFAULT_MIN_BUDGET_PER_REQUEST = '500000';
|
|
10
9
|
/**
|
|
11
|
-
* Manages seller-side payment sessions
|
|
12
|
-
*
|
|
10
|
+
* Manages seller-side payment sessions.
|
|
11
|
+
* The buyer sends a single SpendingAuth signature with a monotonically
|
|
12
|
+
* increasing cumulativeAmount on every request.
|
|
13
|
+
* The seller tracks spending locally and settles/closes via the contract at session end.
|
|
13
14
|
*/
|
|
14
15
|
export class SellerPaymentManager {
|
|
15
16
|
_identity;
|
|
16
17
|
_signer;
|
|
17
|
-
|
|
18
|
+
_channelsClient;
|
|
18
19
|
_config;
|
|
19
|
-
|
|
20
|
+
_channelStore;
|
|
20
21
|
/** In-memory cache of active buyer peerIds for fast has-session checks. */
|
|
21
22
|
_activeBuyers = new Set();
|
|
22
|
-
/** Debounce: track sessions that already sent a top-up request. */
|
|
23
|
-
_topUpRequested = new Set();
|
|
24
|
-
/** Cached seller tokenRate (fetched once from escrow, used for top-up threshold). */
|
|
25
|
-
_tokenRate = null;
|
|
26
|
-
/** Cached FIRST_SIGN_CAP from escrow contract. */
|
|
27
|
-
_firstSignCap = null;
|
|
28
23
|
/** Per-buyer mutex to prevent concurrent handleSpendingAuth for the same buyer. */
|
|
29
24
|
_buyerLocks = new Map();
|
|
30
|
-
|
|
25
|
+
/** channelId -> highest accepted cumulativeAmount from buyer's SpendingAuth */
|
|
26
|
+
_acceptedCumulative = new Map();
|
|
27
|
+
/** channelId -> total USDC spent so far (sum of recordSpend calls) */
|
|
28
|
+
_spent = new Map();
|
|
29
|
+
/** channelId -> on-chain reserveMaxAmount (budget ceiling from ReserveAuth) */
|
|
30
|
+
_reserveMax = new Map();
|
|
31
|
+
/** channelId -> latest buyer-signed auth (both sigs + cumulative values + metadata) for settle/close */
|
|
32
|
+
_latestAuth = new Map();
|
|
33
|
+
/** channelId -> number of failed close() attempts. In-memory only; resets on node restart. */
|
|
34
|
+
_closeRetryCount = new Map();
|
|
35
|
+
/** Max close() retries before giving up (buyer must requestClose on-chain) */
|
|
36
|
+
static MAX_CLOSE_RETRIES = 3;
|
|
37
|
+
constructor(identity, config, channelStore) {
|
|
31
38
|
this._identity = identity;
|
|
32
39
|
this._config = config;
|
|
33
|
-
this._signer =
|
|
34
|
-
this.
|
|
40
|
+
this._signer = identity.wallet;
|
|
41
|
+
this._channelsClient = new ChannelsClient({
|
|
35
42
|
rpcUrl: config.rpcUrl,
|
|
36
|
-
contractAddress: config.
|
|
37
|
-
usdcAddress: config.usdcAddress,
|
|
43
|
+
contractAddress: config.channelsContractAddress,
|
|
38
44
|
});
|
|
39
|
-
this.
|
|
40
|
-
// Hydrate
|
|
41
|
-
const
|
|
42
|
-
for (const
|
|
43
|
-
this._activeBuyers.add(
|
|
45
|
+
this._channelStore = channelStore;
|
|
46
|
+
// Hydrate from persisted channels
|
|
47
|
+
const activeChannels = this._channelStore.getActiveChannels('seller');
|
|
48
|
+
for (const channel of activeChannels) {
|
|
49
|
+
this._activeBuyers.add(channel.peerId);
|
|
50
|
+
this._acceptedCumulative.set(channel.sessionId, BigInt(channel.authMax));
|
|
51
|
+
this._spent.set(channel.sessionId, BigInt(channel.tokensDelivered));
|
|
52
|
+
// Hydrate reserveMax from previousConsumption (repurposed field)
|
|
53
|
+
const storedReserveMax = BigInt(channel.previousConsumption || '0');
|
|
54
|
+
if (storedReserveMax > 0n) {
|
|
55
|
+
this._reserveMax.set(channel.sessionId, storedReserveMax);
|
|
56
|
+
}
|
|
57
|
+
// Hydrate latest auth sigs so close() works after restart
|
|
58
|
+
if (channel.latestSpendingAuthSig) {
|
|
59
|
+
this._latestAuth.set(channel.sessionId, {
|
|
60
|
+
spendingAuthSig: channel.latestSpendingAuthSig,
|
|
61
|
+
cumulativeAmount: BigInt(channel.authMax),
|
|
62
|
+
metadataHash: '',
|
|
63
|
+
metadata: channel.latestMetadata ?? '',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
44
66
|
}
|
|
45
67
|
}
|
|
46
|
-
get
|
|
47
|
-
return this.
|
|
68
|
+
get channelsClient() {
|
|
69
|
+
return this._channelsClient;
|
|
48
70
|
}
|
|
49
|
-
// ── SpendingAuth handler
|
|
71
|
+
// ── SpendingAuth handler ─────────────────────────────────────
|
|
50
72
|
/**
|
|
51
73
|
* Handle incoming SpendingAuth from a buyer.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* 3. Reserve new session on-chain
|
|
55
|
-
* 4. Store and send AuthAck
|
|
74
|
+
* First auth: verify SpendingAuth, reserve on-chain, send AuthAck.
|
|
75
|
+
* Subsequent: verify SpendingAuth signature, validate monotonic increase, persist.
|
|
56
76
|
*/
|
|
57
|
-
async handleSpendingAuth(buyerPeerId,
|
|
77
|
+
async handleSpendingAuth(buyerPeerId, payload, paymentMux) {
|
|
58
78
|
// Per-buyer mutex: serialize concurrent auths for the same buyer
|
|
59
79
|
const existing = this._buyerLocks.get(buyerPeerId);
|
|
60
|
-
|
|
80
|
+
let result = 'rejected';
|
|
81
|
+
const lock = (existing ?? Promise.resolve()).then(async () => {
|
|
82
|
+
result = await this._handleSpendingAuthInner(buyerPeerId, payload, paymentMux);
|
|
83
|
+
});
|
|
61
84
|
this._buyerLocks.set(buyerPeerId, lock.catch(() => { }));
|
|
62
|
-
|
|
85
|
+
await lock;
|
|
86
|
+
return result;
|
|
63
87
|
}
|
|
64
|
-
async _handleSpendingAuthInner(buyerPeerId,
|
|
88
|
+
async _handleSpendingAuthInner(buyerPeerId, payload, paymentMux) {
|
|
89
|
+
const buyerEvmAddr = peerIdToAddress(buyerPeerId);
|
|
65
90
|
try {
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
debugWarn(`[SellerPayment] Invalid SpendingAuth signature: recovered=${recoveredAddr} expected=${buyerEvmAddr}`);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
debugLog(`[SellerPayment] SpendingAuth verified for buyer ${buyerPeerId.slice(0, 12)}...`);
|
|
83
|
-
// 2. Settle prior session if exists
|
|
84
|
-
const priorSession = this._sessionStore.getActiveSessionByPeer(buyerPeerId, 'seller');
|
|
85
|
-
if (priorSession && priorSession.status === 'active') {
|
|
86
|
-
const buyerClaimed = BigInt(payload.previousConsumption);
|
|
87
|
-
const sellerDelivered = BigInt(priorSession.tokensDelivered);
|
|
88
|
-
// Settle with buyer's claimed value — proof chain requires
|
|
89
|
-
// previousConsumption == settledTokenCount on-chain.
|
|
90
|
-
// We do NOT use max(seller, buyer) because that would let the seller
|
|
91
|
-
// overcharge the buyer for tokens the buyer may not have received.
|
|
92
|
-
try {
|
|
93
|
-
debugLog(`[SellerPayment] Settling prior session ${priorSession.sessionId.slice(0, 18)}... tokens=${buyerClaimed} (seller delivered=${sellerDelivered})`);
|
|
94
|
-
await this._escrowClient.settle(this._signer, priorSession.sessionId, buyerClaimed);
|
|
95
|
-
this._sessionStore.updateSessionStatus(priorSession.sessionId, 'settled', buyerClaimed.toString());
|
|
96
|
-
this._topUpRequested.delete(priorSession.sessionId);
|
|
91
|
+
const channelId = payload.channelId;
|
|
92
|
+
const cumulativeAmount = BigInt(payload.cumulativeAmount);
|
|
93
|
+
const existingCumulative = this._acceptedCumulative.get(channelId);
|
|
94
|
+
const channelsDomain = makeChannelsDomain(this._config.chainId, this._config.channelsContractAddress);
|
|
95
|
+
if (existingCumulative === undefined) {
|
|
96
|
+
const hasReserveFields = payload.reserveSalt != null
|
|
97
|
+
|| payload.reserveMaxAmount != null
|
|
98
|
+
|| payload.reserveDeadline != null;
|
|
99
|
+
if (!hasReserveFields) {
|
|
100
|
+
const recovered = await this._recoverOnChainSession(buyerPeerId, buyerEvmAddr, payload, cumulativeAmount, paymentMux, channelsDomain);
|
|
101
|
+
if (recovered) {
|
|
102
|
+
return 'accepted';
|
|
103
|
+
}
|
|
97
104
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
// ── First SpendingAuth: verify ReserveAuth and reserve on-chain ──
|
|
106
|
+
// The buyer signs ReserveAuth(channelId, maxAmount, deadline) to bind escrow terms.
|
|
107
|
+
const reserveMaxAmount = payload.reserveMaxAmount ? BigInt(payload.reserveMaxAmount) : cumulativeAmount;
|
|
108
|
+
const reserveDeadline = payload.reserveDeadline ?? (Math.floor(Date.now() / 1000) + 3600);
|
|
109
|
+
const reserveMsg = {
|
|
110
|
+
channelId,
|
|
111
|
+
maxAmount: reserveMaxAmount,
|
|
112
|
+
deadline: BigInt(reserveDeadline),
|
|
113
|
+
};
|
|
114
|
+
const reserveRecovered = verifyTypedData(channelsDomain, RESERVE_AUTH_TYPES, reserveMsg, payload.spendingAuthSig);
|
|
115
|
+
if (reserveRecovered.toLowerCase() !== buyerEvmAddr.toLowerCase()) {
|
|
116
|
+
debugWarn(`[SellerPayment] Invalid ReserveAuth signature: recovered=${reserveRecovered} expected=${buyerEvmAddr}`);
|
|
117
|
+
return 'rejected';
|
|
118
|
+
}
|
|
119
|
+
debugLog(`[SellerPayment] ReserveAuth verified for buyer ${buyerPeerId.slice(0, 12)}...`);
|
|
120
|
+
debugLog(`[SellerPayment] Reserving channel ${channelId.slice(0, 18)}... on-chain`);
|
|
121
|
+
const reserveSalt = payload.reserveSalt ?? channelId;
|
|
122
|
+
await this._channelsClient.reserve(this._signer, buyerEvmAddr, reserveSalt, reserveMaxAmount, BigInt(reserveDeadline), payload.spendingAuthSig);
|
|
123
|
+
// Store new session (sessionId field stores channelId for backward compat)
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
const sellerEvmAddr = this._identity.wallet.address;
|
|
126
|
+
const session = {
|
|
127
|
+
sessionId: channelId,
|
|
128
|
+
peerId: buyerPeerId,
|
|
129
|
+
role: 'seller',
|
|
130
|
+
sellerEvmAddr,
|
|
131
|
+
buyerEvmAddr,
|
|
132
|
+
nonce: 0,
|
|
133
|
+
authMax: payload.cumulativeAmount,
|
|
134
|
+
previousConsumption: reserveMaxAmount.toString(), // repurposed: stores reserveMax
|
|
135
|
+
deadline: reserveDeadline,
|
|
136
|
+
previousSessionId: '',
|
|
137
|
+
tokensDelivered: '0',
|
|
138
|
+
requestCount: 0,
|
|
139
|
+
reservedAt: now,
|
|
140
|
+
settledAt: null,
|
|
141
|
+
settledAmount: null,
|
|
142
|
+
status: 'active',
|
|
143
|
+
latestBuyerSig: payload.spendingAuthSig,
|
|
144
|
+
latestSpendingAuthSig: payload.spendingAuthSig,
|
|
145
|
+
latestMetadata: payload.metadata,
|
|
146
|
+
createdAt: now,
|
|
147
|
+
updatedAt: now,
|
|
148
|
+
};
|
|
149
|
+
// Note: do NOT store the ReserveAuth sig as spendingAuthSig in _latestAuth.
|
|
150
|
+
// The ReserveAuth uses a different EIP-712 type and will fail
|
|
151
|
+
// _verifySpendingAuth in close(). A real SpendingAuth will arrive
|
|
152
|
+
// per-request and update this entry.
|
|
153
|
+
this._activateSession(session, buyerPeerId, cumulativeAmount, reserveMaxAmount, 0n, {
|
|
154
|
+
spendingAuthSig: '',
|
|
155
|
+
cumulativeAmount,
|
|
156
|
+
metadataHash: payload.metadataHash,
|
|
157
|
+
metadata: payload.metadata,
|
|
158
|
+
});
|
|
159
|
+
// Send AuthAck
|
|
160
|
+
paymentMux.sendAuthAck({
|
|
161
|
+
channelId,
|
|
162
|
+
});
|
|
163
|
+
debugLog(`[SellerPayment] AuthAck sent for channel ${channelId.slice(0, 18)}...`);
|
|
164
|
+
return 'reserved';
|
|
165
|
+
}
|
|
166
|
+
else if (payload.reserveMaxAmount
|
|
167
|
+
&& BigInt(payload.reserveMaxAmount) > (this._reserveMax.get(channelId) ?? 0n)) {
|
|
168
|
+
// ── Top-up: buyer is extending the reserve ceiling ──
|
|
169
|
+
const newMaxAmount = BigInt(payload.reserveMaxAmount);
|
|
170
|
+
const topUpDeadline = payload.reserveDeadline ?? (Math.floor(Date.now() / 1000) + 3600);
|
|
171
|
+
const currentReserveMax = this._reserveMax.get(channelId) ?? 0n;
|
|
172
|
+
// Verify as ReserveAuth (not SpendingAuth)
|
|
173
|
+
const reserveMsg = {
|
|
174
|
+
channelId,
|
|
175
|
+
maxAmount: newMaxAmount,
|
|
176
|
+
deadline: BigInt(topUpDeadline),
|
|
177
|
+
};
|
|
178
|
+
const recovered = verifyTypedData(channelsDomain, RESERVE_AUTH_TYPES, reserveMsg, payload.spendingAuthSig);
|
|
179
|
+
if (recovered.toLowerCase() !== buyerEvmAddr.toLowerCase()) {
|
|
180
|
+
debugWarn(`[SellerPayment] Invalid top-up ReserveAuth signature: recovered=${recovered} expected=${buyerEvmAddr}`);
|
|
181
|
+
return 'rejected';
|
|
102
182
|
}
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
183
|
+
// Call topUp() on-chain
|
|
184
|
+
debugLog(`[SellerPayment] Top-up verified: channel=${channelId.slice(0, 18)}... ceiling ${currentReserveMax} → ${newMaxAmount}`);
|
|
185
|
+
await this._channelsClient.topUp(this._signer, channelId, newMaxAmount, BigInt(topUpDeadline), payload.spendingAuthSig);
|
|
186
|
+
// Update tracking
|
|
187
|
+
this._reserveMax.set(channelId, newMaxAmount);
|
|
188
|
+
const session = this._channelStore.getChannel(channelId);
|
|
189
|
+
if (session) {
|
|
190
|
+
session.previousConsumption = newMaxAmount.toString(); // repurposed: stores reserveMax
|
|
191
|
+
session.deadline = topUpDeadline;
|
|
192
|
+
session.updatedAt = Date.now();
|
|
193
|
+
this._channelStore.upsertChannel(session);
|
|
111
194
|
}
|
|
195
|
+
debugLog(`[SellerPayment] Top-up completed: channel=${channelId.slice(0, 18)}... new ceiling=${newMaxAmount}`);
|
|
196
|
+
return 'accepted';
|
|
112
197
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
198
|
+
else {
|
|
199
|
+
// ── Subsequent SpendingAuth: verify SpendingAuth signature ──
|
|
200
|
+
const metadataMsg = {
|
|
201
|
+
channelId,
|
|
202
|
+
cumulativeAmount,
|
|
203
|
+
metadataHash: payload.metadataHash,
|
|
204
|
+
};
|
|
205
|
+
const metadataRecovered = verifyTypedData(channelsDomain, SPENDING_AUTH_TYPES, metadataMsg, payload.spendingAuthSig);
|
|
206
|
+
if (metadataRecovered.toLowerCase() !== buyerEvmAddr.toLowerCase()) {
|
|
207
|
+
debugWarn(`[SellerPayment] Invalid SpendingAuth signature: recovered=${metadataRecovered} expected=${buyerEvmAddr}`);
|
|
208
|
+
return 'rejected';
|
|
209
|
+
}
|
|
210
|
+
// Validate monotonic (equal = idempotent retransmit)
|
|
211
|
+
if (cumulativeAmount < existingCumulative) {
|
|
212
|
+
debugWarn(`[SellerPayment] Rejecting non-monotonic SpendingAuth: ` +
|
|
213
|
+
`new=${cumulativeAmount} existing=${existingCumulative} channel=${channelId.slice(0, 18)}...`);
|
|
214
|
+
return 'rejected';
|
|
215
|
+
}
|
|
216
|
+
if (cumulativeAmount === existingCumulative) {
|
|
217
|
+
debugLog(`[SellerPayment] Idempotent SpendingAuth (same cumulative=${cumulativeAmount}) — accepted`);
|
|
218
|
+
return 'accepted';
|
|
219
|
+
}
|
|
220
|
+
// Reject if buyer's cumulative doesn't cover what the seller has already spent
|
|
221
|
+
const spent = this._spent.get(channelId) ?? 0n;
|
|
222
|
+
if (cumulativeAmount < spent) {
|
|
223
|
+
debugWarn(`[SellerPayment] Rejecting underfunded SpendingAuth: ` +
|
|
224
|
+
`cumulative=${cumulativeAmount} < spent=${spent} channel=${channelId.slice(0, 18)}...`);
|
|
225
|
+
return 'rejected';
|
|
226
|
+
}
|
|
227
|
+
// Update tracking
|
|
228
|
+
this._acceptedCumulative.set(channelId, cumulativeAmount);
|
|
229
|
+
this._latestAuth.set(channelId, {
|
|
230
|
+
spendingAuthSig: payload.spendingAuthSig,
|
|
231
|
+
cumulativeAmount,
|
|
232
|
+
metadataHash: payload.metadataHash,
|
|
233
|
+
metadata: payload.metadata,
|
|
234
|
+
});
|
|
235
|
+
// Persist latest auth + sigs to ChannelStore
|
|
236
|
+
const session = this._channelStore.getChannel(channelId);
|
|
237
|
+
if (session) {
|
|
238
|
+
session.authMax = payload.cumulativeAmount;
|
|
239
|
+
session.latestBuyerSig = payload.spendingAuthSig;
|
|
240
|
+
session.latestSpendingAuthSig = payload.spendingAuthSig;
|
|
241
|
+
session.latestMetadata = payload.metadata;
|
|
242
|
+
session.updatedAt = Date.now();
|
|
243
|
+
this._channelStore.upsertChannel(session);
|
|
244
|
+
}
|
|
245
|
+
debugLog(`[SellerPayment] Budget updated: channel=${channelId.slice(0, 18)}... cumulative=${cumulativeAmount}`);
|
|
246
|
+
return 'accepted';
|
|
120
247
|
}
|
|
121
|
-
// 4. Reserve new session on-chain
|
|
122
|
-
debugLog(`[SellerPayment] Reserving session ${payload.sessionId.slice(0, 18)}... on-chain`);
|
|
123
|
-
await this._escrowClient.reserve(this._signer, buyerEvmAddr, payload.sessionId, BigInt(payload.maxAmountUsdc), BigInt(payload.nonce), BigInt(payload.deadline), BigInt(payload.previousConsumption), payload.previousSessionId, payload.buyerSig);
|
|
124
|
-
// 5. Store new session
|
|
125
|
-
const now = Date.now();
|
|
126
|
-
const session = {
|
|
127
|
-
sessionId: payload.sessionId,
|
|
128
|
-
peerId: buyerPeerId,
|
|
129
|
-
role: 'seller',
|
|
130
|
-
sellerEvmAddr,
|
|
131
|
-
buyerEvmAddr,
|
|
132
|
-
nonce: payload.nonce,
|
|
133
|
-
authMax: payload.maxAmountUsdc,
|
|
134
|
-
deadline: payload.deadline,
|
|
135
|
-
previousSessionId: payload.previousSessionId,
|
|
136
|
-
previousConsumption: payload.previousConsumption,
|
|
137
|
-
tokensDelivered: '0',
|
|
138
|
-
requestCount: 0,
|
|
139
|
-
reservedAt: now,
|
|
140
|
-
settledAt: null,
|
|
141
|
-
settledAmount: null,
|
|
142
|
-
status: 'active',
|
|
143
|
-
createdAt: now,
|
|
144
|
-
updatedAt: now,
|
|
145
|
-
};
|
|
146
|
-
this._sessionStore.upsertSession(session);
|
|
147
|
-
this._activeBuyers.add(buyerPeerId);
|
|
148
|
-
// 6. Send AuthAck
|
|
149
|
-
paymentMux.sendAuthAck({
|
|
150
|
-
sessionId: payload.sessionId,
|
|
151
|
-
nonce: payload.nonce,
|
|
152
|
-
});
|
|
153
|
-
debugLog(`[SellerPayment] AuthAck sent for session ${payload.sessionId.slice(0, 18)}...`);
|
|
154
248
|
}
|
|
155
249
|
catch (err) {
|
|
156
250
|
debugWarn(`[SellerPayment] Failed to process SpendingAuth: ${err instanceof Error ? err.message : err}`);
|
|
251
|
+
return 'rejected';
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async _recoverOnChainSession(buyerPeerId, buyerEvmAddr, payload, cumulativeAmount, paymentMux, channelsDomain) {
|
|
255
|
+
const channelId = payload.channelId;
|
|
256
|
+
const onChainState = classifyOnChainChannel(await this._channelsClient.getSession(channelId));
|
|
257
|
+
const sellerEvmAddr = this._identity.wallet.address;
|
|
258
|
+
if (!onChainState.exists || onChainState.status !== 'active')
|
|
259
|
+
return false;
|
|
260
|
+
if (!matchesChannelParties(onChainState.channel, buyerEvmAddr, sellerEvmAddr))
|
|
261
|
+
return false;
|
|
262
|
+
const onChain = onChainState.channel;
|
|
263
|
+
const metadataMsg = {
|
|
264
|
+
channelId,
|
|
265
|
+
cumulativeAmount,
|
|
266
|
+
metadataHash: payload.metadataHash,
|
|
267
|
+
};
|
|
268
|
+
const metadataRecovered = verifyTypedData(channelsDomain, SPENDING_AUTH_TYPES, metadataMsg, payload.spendingAuthSig);
|
|
269
|
+
if (metadataRecovered.toLowerCase() !== buyerEvmAddr.toLowerCase()) {
|
|
270
|
+
debugWarn(`[SellerPayment] Invalid recovered SpendingAuth during channel recovery: recovered=${metadataRecovered} expected=${buyerEvmAddr}`);
|
|
271
|
+
return false;
|
|
157
272
|
}
|
|
273
|
+
if (cumulativeAmount < onChain.settled) {
|
|
274
|
+
debugWarn(`[SellerPayment] Rejecting recovered SpendingAuth below on-chain settled amount: ` +
|
|
275
|
+
`cumulative=${cumulativeAmount} settled=${onChain.settled} channel=${channelId.slice(0, 18)}...`);
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
const now = Date.now();
|
|
279
|
+
const session = {
|
|
280
|
+
sessionId: channelId,
|
|
281
|
+
peerId: buyerPeerId,
|
|
282
|
+
role: 'seller',
|
|
283
|
+
sellerEvmAddr,
|
|
284
|
+
buyerEvmAddr,
|
|
285
|
+
nonce: 0,
|
|
286
|
+
authMax: payload.cumulativeAmount,
|
|
287
|
+
previousConsumption: onChain.deposit.toString(),
|
|
288
|
+
deadline: Number(onChain.deadline),
|
|
289
|
+
previousSessionId: '',
|
|
290
|
+
tokensDelivered: onChain.settled.toString(),
|
|
291
|
+
requestCount: 0,
|
|
292
|
+
reservedAt: now,
|
|
293
|
+
settledAt: null,
|
|
294
|
+
settledAmount: onChain.settled > 0n ? onChain.settled.toString() : null,
|
|
295
|
+
status: 'active',
|
|
296
|
+
latestBuyerSig: payload.spendingAuthSig,
|
|
297
|
+
latestSpendingAuthSig: payload.spendingAuthSig,
|
|
298
|
+
latestMetadata: payload.metadata,
|
|
299
|
+
createdAt: now,
|
|
300
|
+
updatedAt: now,
|
|
301
|
+
};
|
|
302
|
+
this._activateSession(session, buyerPeerId, cumulativeAmount, onChain.deposit, onChain.settled, {
|
|
303
|
+
spendingAuthSig: payload.spendingAuthSig,
|
|
304
|
+
cumulativeAmount,
|
|
305
|
+
metadataHash: payload.metadataHash,
|
|
306
|
+
metadata: payload.metadata,
|
|
307
|
+
});
|
|
308
|
+
paymentMux.sendAuthAck({ channelId });
|
|
309
|
+
debugLog(`[SellerPayment] Recovered active on-chain channel ${channelId.slice(0, 18)}... for buyer ${buyerPeerId.slice(0, 12)}...`);
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
_activateSession(session, buyerPeerId, cumulativeAmount, reserveMaxAmount, spent, latestAuth) {
|
|
313
|
+
this._channelStore.upsertChannel(session);
|
|
314
|
+
this._acceptedCumulative.set(session.sessionId, cumulativeAmount);
|
|
315
|
+
this._reserveMax.set(session.sessionId, reserveMaxAmount);
|
|
316
|
+
this._spent.set(session.sessionId, spent);
|
|
317
|
+
this._latestAuth.set(session.sessionId, latestAuth);
|
|
318
|
+
this._activeBuyers.add(buyerPeerId);
|
|
158
319
|
}
|
|
159
|
-
// ──
|
|
320
|
+
// ── Per-request validation ──────────────────────────────────
|
|
160
321
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
322
|
+
* Validate and accept a SpendingAuth attached to an incoming request.
|
|
323
|
+
* Returns true if the buyer has sufficient budget to serve this request.
|
|
163
324
|
*/
|
|
164
|
-
async
|
|
165
|
-
|
|
325
|
+
async validateAndAcceptAuth(buyerPeerId, auth) {
|
|
326
|
+
// Look up active session for this buyer
|
|
327
|
+
const session = this._channelStore.getActiveChannelByPeer(buyerPeerId, 'seller');
|
|
166
328
|
if (!session) {
|
|
167
|
-
debugWarn(`[SellerPayment]
|
|
168
|
-
return;
|
|
329
|
+
debugWarn(`[SellerPayment] validateAndAcceptAuth: no active session for buyer ${buyerPeerId.slice(0, 12)}...`);
|
|
330
|
+
return false;
|
|
169
331
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const authMax = BigInt(session.authMax);
|
|
176
|
-
const tokenRate = this._tokenRate;
|
|
177
|
-
if (tokenRate === null || tokenRate === 0n) {
|
|
178
|
-
throw new Error('Token rate unavailable — cannot send receipt without on-chain rate');
|
|
332
|
+
const channelId = session.sessionId; // sessionId field stores channelId
|
|
333
|
+
const existingCumulative = this._acceptedCumulative.get(channelId);
|
|
334
|
+
if (existingCumulative === undefined) {
|
|
335
|
+
debugWarn(`[SellerPayment] validateAndAcceptAuth: no tracked cumulative for channel ${channelId.slice(0, 18)}...`);
|
|
336
|
+
return false;
|
|
179
337
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
338
|
+
// Verify AntSeed SpendingAuth signature
|
|
339
|
+
const channelsDomain = makeChannelsDomain(this._config.chainId, this._config.channelsContractAddress);
|
|
340
|
+
const metadataMsg = {
|
|
341
|
+
channelId: auth.channelId,
|
|
342
|
+
cumulativeAmount: BigInt(auth.cumulativeAmount),
|
|
343
|
+
metadataHash: auth.metadataHash,
|
|
344
|
+
};
|
|
345
|
+
const buyerEvmAddr = peerIdToAddress(buyerPeerId);
|
|
346
|
+
try {
|
|
347
|
+
const recovered = verifyTypedData(channelsDomain, SPENDING_AUTH_TYPES, metadataMsg, auth.spendingAuthSig);
|
|
348
|
+
if (recovered.toLowerCase() !== buyerEvmAddr.toLowerCase()) {
|
|
349
|
+
debugWarn(`[SellerPayment] validateAndAcceptAuth: invalid SpendingAuth signature`);
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
184
352
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
runningTotal: newTotal.toString(),
|
|
204
|
-
requestCount: newRequestCount,
|
|
205
|
-
responseHash: bytesToHex(new Uint8Array(responseHash)),
|
|
206
|
-
sellerSig: bytesToHex(sellerSig),
|
|
207
|
-
buyerAckSig: null,
|
|
208
|
-
createdAt: Date.now(),
|
|
209
|
-
});
|
|
210
|
-
debugLog(`[SellerPayment] Receipt sent: session=${session.sessionId.slice(0, 18)}... total=${newTotal} count=${newRequestCount}`);
|
|
211
|
-
// TopUpRequest if > 80% of USDC cap consumed (send at most once per session)
|
|
212
|
-
const usdcConsumed = newTotal * tokenRate;
|
|
213
|
-
if (authMax > 0n && usdcConsumed * 100n > authMax * 80n && !this._topUpRequested.has(session.sessionId)) {
|
|
214
|
-
this._topUpRequested.add(session.sessionId);
|
|
215
|
-
const additionalAmount = authMax; // Request same amount again
|
|
216
|
-
paymentMux.sendTopUpRequest({
|
|
217
|
-
sessionId: session.sessionId,
|
|
218
|
-
currentUsed: newTotal.toString(),
|
|
219
|
-
currentMax: authMax.toString(),
|
|
220
|
-
requestedAdditional: additionalAmount.toString(),
|
|
353
|
+
catch {
|
|
354
|
+
debugWarn(`[SellerPayment] validateAndAcceptAuth: SpendingAuth verification failed`);
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
// Check monotonic: strictly greater, or equal (idempotent retransmit)
|
|
358
|
+
const newCumulative = BigInt(auth.cumulativeAmount);
|
|
359
|
+
if (newCumulative < existingCumulative) {
|
|
360
|
+
debugWarn(`[SellerPayment] validateAndAcceptAuth: cumulative decreased from ${existingCumulative} to ${newCumulative}`);
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
// Update if strictly greater
|
|
364
|
+
if (newCumulative > existingCumulative) {
|
|
365
|
+
this._acceptedCumulative.set(channelId, newCumulative);
|
|
366
|
+
this._latestAuth.set(channelId, {
|
|
367
|
+
spendingAuthSig: auth.spendingAuthSig,
|
|
368
|
+
cumulativeAmount: newCumulative,
|
|
369
|
+
metadataHash: auth.metadataHash,
|
|
370
|
+
metadata: auth.metadata,
|
|
221
371
|
});
|
|
222
|
-
|
|
372
|
+
// Persist latest auth + sigs to ChannelStore
|
|
373
|
+
const storedSession = this._channelStore.getChannel(channelId);
|
|
374
|
+
if (storedSession) {
|
|
375
|
+
storedSession.authMax = auth.cumulativeAmount;
|
|
376
|
+
storedSession.latestBuyerSig = auth.spendingAuthSig;
|
|
377
|
+
storedSession.latestSpendingAuthSig = auth.spendingAuthSig;
|
|
378
|
+
storedSession.latestMetadata = auth.metadata;
|
|
379
|
+
storedSession.updatedAt = Date.now();
|
|
380
|
+
this._channelStore.upsertChannel(storedSession);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// Check available budget
|
|
384
|
+
const accepted = this._acceptedCumulative.get(channelId);
|
|
385
|
+
const spent = this._spent.get(channelId) ?? 0n;
|
|
386
|
+
return accepted >= spent;
|
|
387
|
+
}
|
|
388
|
+
// ── Spend tracking ──────────────────────────────────────────
|
|
389
|
+
/**
|
|
390
|
+
* Record USDC consumption after serving a request.
|
|
391
|
+
*/
|
|
392
|
+
recordSpend(sessionId, costUsdc) {
|
|
393
|
+
const current = this._spent.get(sessionId);
|
|
394
|
+
if (current === undefined) {
|
|
395
|
+
debugWarn(`[SellerPayment] recordSpend: unknown channelId ${sessionId.slice(0, 18)}...`);
|
|
396
|
+
return;
|
|
223
397
|
}
|
|
398
|
+
const newSpent = current + costUsdc;
|
|
399
|
+
this._spent.set(sessionId, newSpent);
|
|
400
|
+
// Persist spent amount to ChannelStore (using tokensDelivered field)
|
|
401
|
+
this._channelStore.updateTokensDelivered(sessionId, newSpent.toString(), 0);
|
|
224
402
|
}
|
|
225
|
-
// ──
|
|
226
|
-
|
|
227
|
-
|
|
403
|
+
// ── Settlement ──────────────────────────────────────────────
|
|
404
|
+
/**
|
|
405
|
+
* Close a completed session on-chain using the latest buyer-signed dual signatures.
|
|
406
|
+
* Uses close() for final settlement (releases remaining deposit to buyer).
|
|
407
|
+
*/
|
|
408
|
+
async settleSession(buyerPeerId, { cleanupOnFailure = false } = {}) {
|
|
409
|
+
const session = this._channelStore.getActiveChannelByPeer(buyerPeerId, 'seller');
|
|
228
410
|
if (!session) {
|
|
229
|
-
debugWarn(`[SellerPayment]
|
|
411
|
+
debugWarn(`[SellerPayment] settleSession: no active session for buyer ${buyerPeerId.slice(0, 12)}...`);
|
|
230
412
|
return;
|
|
231
413
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const valid = await verifyMessageEd25519(buyerPublicKey, sigBytes, ackMsg);
|
|
239
|
-
if (!valid) {
|
|
240
|
-
debugWarn(`[SellerPayment] Invalid BuyerAck signature from ${buyerPeerId.slice(0, 12)}...`);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
// Store the ack directly via targeted UPDATE (no-op if no matching receipt)
|
|
244
|
-
this._sessionStore.updateReceiptAck(session.sessionId, payload.runningTotal, payload.requestCount, payload.buyerSig);
|
|
245
|
-
debugLog(`[SellerPayment] BuyerAck received: session=${session.sessionId.slice(0, 18)}... count=${payload.requestCount} total=${payload.runningTotal}`);
|
|
414
|
+
const channelId = session.sessionId;
|
|
415
|
+
const accepted = this._acceptedCumulative.get(channelId) ?? 0n;
|
|
416
|
+
if (accepted === 0n) {
|
|
417
|
+
// Session opened but no requests served — cannot close without a voucher.
|
|
418
|
+
// Leave it for checkTimeouts(); buyer must call requestClose → withdraw on-chain.
|
|
419
|
+
debugLog(`[SellerPayment] Zero-cumulative channel ${channelId.slice(0, 18)}... — deferring to timeout checker`);
|
|
246
420
|
}
|
|
247
|
-
|
|
248
|
-
|
|
421
|
+
else {
|
|
422
|
+
const retries = this._closeRetryCount.get(channelId) ?? 0;
|
|
423
|
+
if (retries >= SellerPaymentManager.MAX_CLOSE_RETRIES) {
|
|
424
|
+
// Exhausted retries — give up on close(), fall back to timeout path
|
|
425
|
+
debugWarn(`[SellerPayment] close() failed ${retries} times for ${channelId.slice(0, 18)}... — falling back to timeout path`);
|
|
426
|
+
// Fall through to general cleanup below; buyer must requestClose on-chain
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
// Close with the latest buyer-signed auth (final settlement)
|
|
430
|
+
const latestAuth = this._latestAuth.get(channelId);
|
|
431
|
+
const hasSpendingAuth = latestAuth && latestAuth.spendingAuthSig.length > 0;
|
|
432
|
+
if (!hasSpendingAuth) {
|
|
433
|
+
// No real SpendingAuth received (only ReserveAuth from session setup).
|
|
434
|
+
// Close with finalAmount=0 so the contract skips signature verification.
|
|
435
|
+
// The seller forfeits unproven spend but the channel is properly closed.
|
|
436
|
+
debugLog(`[SellerPayment] No SpendingAuth for channel ${channelId.slice(0, 18)}... — closing with finalAmount=0 (forfeiting unproven spend)`);
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
debugLog(`[SellerPayment] Closing channel ${channelId.slice(0, 18)}... cumulative=${latestAuth.cumulativeAmount} (attempt ${retries + 1}/${SellerPaymentManager.MAX_CLOSE_RETRIES})`);
|
|
440
|
+
}
|
|
441
|
+
const closeAmount = hasSpendingAuth ? latestAuth.cumulativeAmount : 0n;
|
|
442
|
+
const closeMetadata = hasSpendingAuth
|
|
443
|
+
? (latestAuth.metadata || encodeMetadata(ZERO_METADATA))
|
|
444
|
+
: encodeMetadata(ZERO_METADATA);
|
|
445
|
+
const closeSig = hasSpendingAuth ? latestAuth.spendingAuthSig : '0x';
|
|
446
|
+
try {
|
|
447
|
+
await this._channelsClient.close(this._signer, channelId, closeAmount, closeMetadata, closeSig);
|
|
448
|
+
this._channelStore.updateChannelStatus(channelId, 'settled', closeAmount.toString());
|
|
449
|
+
this._closeRetryCount.delete(channelId);
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
debugWarn(`[SellerPayment] Failed to close channel (attempt ${retries + 1}): ${err instanceof Error ? err.message : err}`);
|
|
453
|
+
this._closeRetryCount.set(channelId, retries + 1);
|
|
454
|
+
if (!cleanupOnFailure) {
|
|
455
|
+
// Keep maps intact so checkTimeouts can retry
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
// Caller requested cleanup even on failure (e.g., disconnect handler)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
249
461
|
}
|
|
462
|
+
// Clean up maps after successful close, zero-cumulative deferral, or exhausted retries
|
|
463
|
+
this._acceptedCumulative.delete(channelId);
|
|
464
|
+
this._spent.delete(channelId);
|
|
465
|
+
this._latestAuth.delete(channelId);
|
|
466
|
+
this._closeRetryCount.delete(channelId);
|
|
467
|
+
this._activeBuyers.delete(buyerPeerId);
|
|
250
468
|
}
|
|
251
469
|
// ── Disconnect handling ───────────────────────────────────────
|
|
252
470
|
onBuyerDisconnect(buyerPeerId) {
|
|
253
|
-
const session = this.
|
|
471
|
+
const session = this._channelStore.getActiveChannelByPeer(buyerPeerId, 'seller');
|
|
254
472
|
if (!session)
|
|
255
473
|
return;
|
|
256
|
-
|
|
257
|
-
|
|
474
|
+
const settleOnDisconnect = this._config.settleOnDisconnect ?? true;
|
|
475
|
+
if (settleOnDisconnect) {
|
|
476
|
+
const accepted = this._acceptedCumulative.get(session.sessionId) ?? 0n;
|
|
477
|
+
if (accepted > 0n) {
|
|
478
|
+
debugLog(`[SellerPayment] Buyer ${buyerPeerId.slice(0, 12)}... disconnected — closing channel immediately`);
|
|
479
|
+
// Fire and forget settlement — clean up maps even if close() fails
|
|
480
|
+
this.settleSession(buyerPeerId, { cleanupOnFailure: true }).catch((err) => {
|
|
481
|
+
debugWarn(`[SellerPayment] Failed to close on disconnect: ${err instanceof Error ? err.message : err}`);
|
|
482
|
+
});
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// Preserve session for reconnect; timeout checker handles ghost scenarios
|
|
258
487
|
this._activeBuyers.delete(buyerPeerId);
|
|
259
|
-
debugLog(`[SellerPayment] Buyer ${buyerPeerId.slice(0, 12)}... disconnected —
|
|
488
|
+
debugLog(`[SellerPayment] Buyer ${buyerPeerId.slice(0, 12)}... disconnected — channel ${session.sessionId.slice(0, 18)}... preserved for reconnect`);
|
|
260
489
|
}
|
|
261
|
-
// ──
|
|
490
|
+
// ── Stale session cleanup ────────────────────────────────────
|
|
262
491
|
/**
|
|
263
|
-
* Check for and
|
|
492
|
+
* Check for stale sessions and attempt to close them.
|
|
493
|
+
* The seller can only close() with a valid SpendingAuth — it cannot
|
|
494
|
+
* requestClose or withdraw (those are buyer-only on-chain).
|
|
495
|
+
* If the seller has no auths, the session remains open until the buyer
|
|
496
|
+
* calls requestClose → withdraw on-chain.
|
|
264
497
|
* Called periodically and on startup for recovery.
|
|
265
498
|
*/
|
|
266
499
|
async checkTimeouts() {
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
for (const
|
|
270
|
-
|
|
271
|
-
continue;
|
|
500
|
+
const nowSecs = Math.floor(Date.now() / 1000);
|
|
501
|
+
const activeChannels = this._channelStore.getActiveChannels('seller');
|
|
502
|
+
for (const channel of activeChannels) {
|
|
503
|
+
const accepted = this._acceptedCumulative.get(channel.sessionId) ?? 0n;
|
|
272
504
|
try {
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
await this._escrowClient.settle(this._signer, session.sessionId, delivered);
|
|
278
|
-
this._sessionStore.updateSessionStatus(session.sessionId, 'settled', delivered.toString());
|
|
505
|
+
// If we have auths and the buyer is disconnected, try to close
|
|
506
|
+
if (accepted > 0n && !this._activeBuyers.has(channel.peerId)) {
|
|
507
|
+
debugLog(`[SellerPayment] Channel ${channel.sessionId.slice(0, 18)}... buyer disconnected — attempting close`);
|
|
508
|
+
await this.settleSession(channel.peerId);
|
|
279
509
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
510
|
+
// If no auths and buyer disconnected, nothing the seller can do on-chain.
|
|
511
|
+
// The buyer must call requestClose → withdraw. We just clean up locally
|
|
512
|
+
// after a reasonable period (e.g. deadline passed).
|
|
513
|
+
if (accepted === 0n && !this._activeBuyers.has(channel.peerId) && nowSecs > channel.deadline) {
|
|
514
|
+
debugLog(`[SellerPayment] Channel ${channel.sessionId.slice(0, 18)}... no auths, past deadline — cleaning up locally`);
|
|
515
|
+
this._channelStore.updateChannelStatus(channel.sessionId, 'timeout');
|
|
516
|
+
this._acceptedCumulative.delete(channel.sessionId);
|
|
517
|
+
this._spent.delete(channel.sessionId);
|
|
518
|
+
this._latestAuth.delete(channel.sessionId);
|
|
519
|
+
this._closeRetryCount.delete(channel.sessionId);
|
|
520
|
+
this._reserveMax.delete(channel.sessionId);
|
|
521
|
+
this._activeBuyers.delete(channel.peerId);
|
|
285
522
|
}
|
|
286
|
-
this._activeBuyers.delete(session.peerId);
|
|
287
|
-
this._topUpRequested.delete(session.sessionId);
|
|
288
523
|
}
|
|
289
524
|
catch (err) {
|
|
290
|
-
debugWarn(`[SellerPayment] Failed to
|
|
525
|
+
debugWarn(`[SellerPayment] Failed to process channel ${channel.sessionId.slice(0, 18)}...: ${err instanceof Error ? err.message : err}`);
|
|
291
526
|
}
|
|
292
527
|
}
|
|
293
528
|
}
|
|
@@ -295,76 +530,124 @@ export class SellerPaymentManager {
|
|
|
295
530
|
hasSession(buyerPeerId) {
|
|
296
531
|
return this._activeBuyers.has(buyerPeerId);
|
|
297
532
|
}
|
|
298
|
-
/**
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Pre-fetch on-chain data (tokenRate, FIRST_SIGN_CAP) so PaymentRequired
|
|
303
|
-
* messages can be sent without blocking on RPC calls.
|
|
304
|
-
*/
|
|
305
|
-
async init() {
|
|
306
|
-
this._lastInitAttemptMs = Date.now();
|
|
307
|
-
try {
|
|
308
|
-
const sellerEvmAddr = identityToEvmAddress(this._identity);
|
|
309
|
-
const [account, firstSignCap] = await Promise.all([
|
|
310
|
-
this._escrowClient.getSellerAccount(sellerEvmAddr),
|
|
311
|
-
this._escrowClient.getFirstSignCap(),
|
|
312
|
-
]);
|
|
313
|
-
this._tokenRate = account.tokenRate;
|
|
314
|
-
this._firstSignCap = firstSignCap;
|
|
315
|
-
debugLog(`[SellerPayment] Cached on-chain data: tokenRate=${this._tokenRate} firstSignCap=${this._firstSignCap}`);
|
|
316
|
-
}
|
|
317
|
-
catch (err) {
|
|
318
|
-
debugWarn(`[SellerPayment] Failed to pre-fetch on-chain data: ${err instanceof Error ? err.message : err}`);
|
|
319
|
-
}
|
|
533
|
+
/** Get the active session for a buyer peer, or null. */
|
|
534
|
+
getChannelByPeer(buyerPeerId) {
|
|
535
|
+
return this._channelStore.getActiveChannelByPeer(buyerPeerId, 'seller');
|
|
320
536
|
}
|
|
321
|
-
/**
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
537
|
+
/** Get total USDC spent for a session (sum of recordSpend calls). */
|
|
538
|
+
getCumulativeSpend(sessionId) {
|
|
539
|
+
return this._spent.get(sessionId) ?? 0n;
|
|
540
|
+
}
|
|
541
|
+
/** Get the highest accepted cumulative amount for a session. */
|
|
542
|
+
getAcceptedCumulative(sessionId) {
|
|
543
|
+
return this._acceptedCumulative.get(sessionId) ?? 0n;
|
|
328
544
|
}
|
|
329
|
-
|
|
545
|
+
/** Get the on-chain reserve budget ceiling for a session. */
|
|
546
|
+
getReserveMax(sessionId) {
|
|
547
|
+
return this._reserveMax.get(sessionId) ?? 0n;
|
|
548
|
+
}
|
|
549
|
+
static DEFAULT_SUGGESTED_AMOUNT = 1000000n; // $1.00 — matches contract FIRST_SIGN_CAP and buyer default
|
|
330
550
|
/**
|
|
331
551
|
* Build the PaymentRequired payload for a buyer that doesn't have a session.
|
|
332
|
-
* Returns null if on-chain data isn't available yet.
|
|
333
|
-
* For returning buyers (proven-sign eligible), uses the configured proven-sign
|
|
334
|
-
* amount instead of the first-sign amount (both default to $0.10).
|
|
335
552
|
*/
|
|
336
553
|
getPaymentRequirements(requestId, buyerPeerId, pricing) {
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
const firstSignCap = this._firstSignCap;
|
|
340
|
-
if (tokenRate === null || firstSignCap === null) {
|
|
341
|
-
return null;
|
|
342
|
-
}
|
|
343
|
-
const defaultAmount = SellerPaymentManager.DEFAULT_SUGGESTED_AMOUNT;
|
|
344
|
-
const firstSignAmount = this._config.firstSignAmountUsdc
|
|
345
|
-
? BigInt(this._config.firstSignAmountUsdc) : defaultAmount;
|
|
346
|
-
const provenSignAmount = this._config.provenSignAmountUsdc
|
|
347
|
-
? BigInt(this._config.provenSignAmountUsdc) : defaultAmount;
|
|
348
|
-
let suggestedAmount = firstSignAmount;
|
|
554
|
+
const minBudgetPerRequest = this._config.minBudgetPerRequest ?? DEFAULT_MIN_BUDGET_PER_REQUEST;
|
|
555
|
+
let suggestedAmount = SellerPaymentManager.DEFAULT_SUGGESTED_AMOUNT;
|
|
349
556
|
if (buyerPeerId) {
|
|
350
|
-
const priorSession = this.
|
|
351
|
-
if (priorSession &&
|
|
352
|
-
|
|
557
|
+
const priorSession = this._channelStore.getLatestChannel(buyerPeerId, 'seller');
|
|
558
|
+
if (priorSession && priorSession.status === 'settled') {
|
|
559
|
+
// Returning buyer with proven history — could use a different amount
|
|
560
|
+
// For now, use the same default; config can override later
|
|
561
|
+
suggestedAmount = SellerPaymentManager.DEFAULT_SUGGESTED_AMOUNT;
|
|
353
562
|
}
|
|
354
563
|
}
|
|
355
564
|
return {
|
|
356
|
-
|
|
357
|
-
tokenRate: tokenRate.toString(),
|
|
358
|
-
firstSignCap: firstSignCap.toString(),
|
|
565
|
+
minBudgetPerRequest,
|
|
359
566
|
suggestedAmount: suggestedAmount.toString(),
|
|
360
567
|
requestId,
|
|
361
568
|
...(pricing?.inputUsdPerMillion != null ? { inputUsdPerMillion: pricing.inputUsdPerMillion } : {}),
|
|
362
569
|
...(pricing?.outputUsdPerMillion != null ? { outputUsdPerMillion: pricing.outputUsdPerMillion } : {}),
|
|
363
570
|
};
|
|
364
571
|
}
|
|
572
|
+
// ── CloseRequested handling ───────────────────────────────────
|
|
573
|
+
/**
|
|
574
|
+
* Handle a CloseRequested event for a channel this seller manages.
|
|
575
|
+
* If the seller has a stored SpendingAuth, immediately close the channel
|
|
576
|
+
* on-chain to claim earnings before the grace period expires.
|
|
577
|
+
*/
|
|
578
|
+
async handleCloseRequested(channelId) {
|
|
579
|
+
const latestAuth = this._latestAuth.get(channelId);
|
|
580
|
+
const accepted = this._acceptedCumulative.get(channelId) ?? 0n;
|
|
581
|
+
const hasSpendingAuth = latestAuth && latestAuth.spendingAuthSig.length > 0;
|
|
582
|
+
if (accepted > 0n && hasSpendingAuth) {
|
|
583
|
+
debugLog(`[SellerPayment] CloseRequested for channel ${channelId.slice(0, 18)}... — closing with cumulative=${latestAuth.cumulativeAmount}`);
|
|
584
|
+
try {
|
|
585
|
+
await this._channelsClient.close(this._signer, channelId, latestAuth.cumulativeAmount, latestAuth.metadata || encodeMetadata(ZERO_METADATA), latestAuth.spendingAuthSig);
|
|
586
|
+
this._channelStore.updateChannelStatus(channelId, 'settled', latestAuth.cumulativeAmount.toString());
|
|
587
|
+
debugLog(`[SellerPayment] Channel ${channelId.slice(0, 18)}... closed successfully after CloseRequested`);
|
|
588
|
+
}
|
|
589
|
+
catch (err) {
|
|
590
|
+
debugWarn(`[SellerPayment] Failed to close channel ${channelId.slice(0, 18)}... after CloseRequested: ${err instanceof Error ? err.message : err}`);
|
|
591
|
+
// Early return: preserve in-memory maps so the next poll cycle can retry close()
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
else if (accepted > 0n) {
|
|
596
|
+
// Had spend but no SpendingAuth (only ReserveAuth) — close with 0 to release deposit
|
|
597
|
+
debugLog(`[SellerPayment] CloseRequested for channel ${channelId.slice(0, 18)}... — no SpendingAuth, closing with finalAmount=0`);
|
|
598
|
+
try {
|
|
599
|
+
await this._channelsClient.close(this._signer, channelId, 0n, encodeMetadata(ZERO_METADATA), '0x');
|
|
600
|
+
this._channelStore.updateChannelStatus(channelId, 'settled', '0');
|
|
601
|
+
}
|
|
602
|
+
catch (err) {
|
|
603
|
+
debugWarn(`[SellerPayment] Failed to close channel ${channelId.slice(0, 18)}... with finalAmount=0: ${err instanceof Error ? err.message : err}`);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
else {
|
|
608
|
+
// No voucher — seller can't claim anything. Clean up locally;
|
|
609
|
+
// buyer will withdraw after grace period.
|
|
610
|
+
debugLog(`[SellerPayment] CloseRequested for channel ${channelId.slice(0, 18)}... — no SpendingAuth, cleaning up locally`);
|
|
611
|
+
this._channelStore.updateChannelStatus(channelId, 'timeout');
|
|
612
|
+
}
|
|
613
|
+
// Clean up in-memory state
|
|
614
|
+
this._acceptedCumulative.delete(channelId);
|
|
615
|
+
this._spent.delete(channelId);
|
|
616
|
+
this._latestAuth.delete(channelId);
|
|
617
|
+
this._closeRetryCount.delete(channelId);
|
|
618
|
+
this._reserveMax.delete(channelId);
|
|
619
|
+
// Find and remove buyer from active set
|
|
620
|
+
const channel = this._channelStore.getChannel(channelId);
|
|
621
|
+
if (channel) {
|
|
622
|
+
this._activeBuyers.delete(channel.peerId);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Poll for CloseRequested events and handle any that match active channels.
|
|
627
|
+
* Returns the block number to use as the next fromBlock cursor.
|
|
628
|
+
*/
|
|
629
|
+
async pollCloseRequested(fromBlock) {
|
|
630
|
+
try {
|
|
631
|
+
// Fetch block number first and pin as toBlock to avoid race:
|
|
632
|
+
// if blocks are mined between the two calls, events in the gap would be missed.
|
|
633
|
+
const latestBlock = await this._channelsClient.getBlockNumber();
|
|
634
|
+
const events = await this._channelsClient.getCloseRequestedEvents(fromBlock, latestBlock);
|
|
635
|
+
for (const event of events) {
|
|
636
|
+
// Only handle channels this seller is actively tracking
|
|
637
|
+
if (this._acceptedCumulative.has(event.channelId) || this._channelStore.getChannel(event.channelId)?.status === 'active') {
|
|
638
|
+
await this.handleCloseRequested(event.channelId);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return latestBlock + 1;
|
|
642
|
+
}
|
|
643
|
+
catch (err) {
|
|
644
|
+
debugWarn(`[SellerPayment] Failed to poll CloseRequested events: ${err instanceof Error ? err.message : err}`);
|
|
645
|
+
return fromBlock; // Retry from same block on next poll
|
|
646
|
+
}
|
|
647
|
+
}
|
|
365
648
|
// ── Lifecycle ─────────────────────────────────────────────────
|
|
366
649
|
close() {
|
|
367
|
-
//
|
|
650
|
+
// ChannelStore is shared with BuyerPaymentManager, closed from node.ts
|
|
368
651
|
}
|
|
369
652
|
}
|
|
370
653
|
//# sourceMappingURL=seller-payment-manager.js.map
|