@antseed/node 0.2.26 → 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 +85 -11
- 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 -2
- package/dist/discovery/announcer.d.ts.map +1 -1
- package/dist/discovery/announcer.js +11 -14
- package/dist/discovery/announcer.js.map +1 -1
- package/dist/discovery/index.d.ts +0 -2
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/index.js +0 -2
- 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 -22
- package/dist/discovery/reputation-verifier.d.ts.map +1 -1
- package/dist/discovery/reputation-verifier.js +2 -24
- 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 +17 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -3
- 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 +83 -104
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +313 -1026
- 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 +12 -6
- package/dist/p2p/connection-manager.js.map +1 -1
- package/dist/p2p/identity.d.ts +44 -13
- package/dist/p2p/identity.d.ts.map +1 -1
- package/dist/p2p/identity.js +103 -49
- package/dist/p2p/identity.js.map +1 -1
- package/dist/p2p/index.d.ts +1 -3
- package/dist/p2p/index.d.ts.map +1 -1
- package/dist/p2p/index.js +1 -3
- package/dist/p2p/index.js.map +1 -1
- package/dist/p2p/payment-codec.d.ts +9 -19
- package/dist/p2p/payment-codec.d.ts.map +1 -1
- package/dist/p2p/payment-codec.js +41 -89
- package/dist/p2p/payment-codec.js.map +1 -1
- package/dist/p2p/payment-mux.d.ts +14 -29
- package/dist/p2p/payment-mux.d.ts.map +1 -1
- package/dist/p2p/payment-mux.js +41 -79
- 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 +157 -83
- package/dist/payments/buyer-payment-manager.d.ts.map +1 -1
- package/dist/payments/buyer-payment-manager.js +573 -204
- 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 +44 -0
- package/dist/payments/chain-config.d.ts.map +1 -0
- package/dist/payments/chain-config.js +70 -0
- package/dist/payments/chain-config.js.map +1 -0
- 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 +16 -0
- package/dist/payments/evm/ants-token-client.d.ts.map +1 -0
- package/dist/payments/evm/ants-token-client.js +65 -0
- package/dist/payments/evm/ants-token-client.js.map +1 -0
- package/dist/payments/evm/base-evm-client.d.ts +22 -0
- package/dist/payments/evm/base-evm-client.d.ts.map +1 -0
- package/dist/payments/evm/base-evm-client.js +71 -0
- package/dist/payments/evm/base-evm-client.js.map +1 -0
- 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 +22 -0
- package/dist/payments/evm/emissions-client.d.ts.map +1 -0
- package/dist/payments/evm/emissions-client.js +65 -0
- package/dist/payments/evm/emissions-client.js.map +1 -0
- package/dist/payments/evm/escrow-client.d.ts +57 -36
- package/dist/payments/evm/escrow-client.d.ts.map +1 -1
- package/dist/payments/evm/escrow-client.js +200 -93
- package/dist/payments/evm/escrow-client.js.map +1 -1
- package/dist/payments/evm/identity-client.d.ts +21 -0
- package/dist/payments/evm/identity-client.d.ts.map +1 -0
- package/dist/payments/evm/identity-client.js +68 -0
- package/dist/payments/evm/identity-client.js.map +1 -0
- 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 +54 -10
- package/dist/payments/evm/signatures.d.ts.map +1 -1
- package/dist/payments/evm/signatures.js +80 -54
- 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/evm/subpool-client.d.ts +30 -0
- package/dist/payments/evm/subpool-client.d.ts.map +1 -0
- package/dist/payments/evm/subpool-client.js +158 -0
- package/dist/payments/evm/subpool-client.js.map +1 -0
- package/dist/payments/index.d.ts +29 -9
- package/dist/payments/index.d.ts.map +1 -1
- package/dist/payments/index.js +27 -9
- 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 +13 -0
- package/dist/payments/readiness.d.ts.map +1 -0
- package/dist/payments/readiness.js +57 -0
- package/dist/payments/readiness.js.map +1 -0
- package/dist/payments/seller-payment-manager.d.ts +101 -36
- package/dist/payments/seller-payment-manager.d.ts.map +1 -1
- package/dist/payments/seller-payment-manager.js +612 -120
- package/dist/payments/seller-payment-manager.js.map +1 -1
- package/dist/payments/session-store.d.ts +68 -0
- package/dist/payments/session-store.d.ts.map +1 -0
- package/dist/payments/session-store.js +272 -0
- package/dist/payments/session-store.js.map +1 -0
- package/dist/payments/types.d.ts +5 -3
- package/dist/payments/types.d.ts.map +1 -1
- package/dist/payments/usdc-utils.d.ts +9 -0
- package/dist/payments/usdc-utils.d.ts.map +1 -0
- package/dist/payments/usdc-utils.js +17 -0
- package/dist/payments/usdc-utils.js.map +1 -0
- 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/http.d.ts +2 -0
- package/dist/types/http.d.ts.map +1 -1
- package/dist/types/http.js +2 -0
- package/dist/types/http.js.map +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/metering.d.ts +2 -2
- package/dist/types/metering.d.ts.map +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 +32 -97
- package/dist/types/protocol.d.ts.map +1 -1
- package/dist/types/protocol.js +5 -10
- 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 +4 -3
|
@@ -1,161 +1,653 @@
|
|
|
1
1
|
import { verifyTypedData } from 'ethers';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { makeEscrowDomain, SPENDING_AUTH_V2_TYPES } from './evm/signatures.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';
|
|
5
4
|
import { debugLog, debugWarn } from '../utils/debug.js';
|
|
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';
|
|
9
|
+
/**
|
|
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.
|
|
14
|
+
*/
|
|
6
15
|
export class SellerPaymentManager {
|
|
16
|
+
_identity;
|
|
7
17
|
_signer;
|
|
8
|
-
|
|
18
|
+
_channelsClient;
|
|
9
19
|
_config;
|
|
10
|
-
|
|
11
|
-
|
|
20
|
+
_channelStore;
|
|
21
|
+
/** In-memory cache of active buyer peerIds for fast has-session checks. */
|
|
22
|
+
_activeBuyers = new Set();
|
|
23
|
+
/** Per-buyer mutex to prevent concurrent handleSpendingAuth for the same buyer. */
|
|
24
|
+
_buyerLocks = new Map();
|
|
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) {
|
|
38
|
+
this._identity = identity;
|
|
12
39
|
this._config = config;
|
|
13
|
-
this._signer =
|
|
14
|
-
this.
|
|
40
|
+
this._signer = identity.wallet;
|
|
41
|
+
this._channelsClient = new ChannelsClient({
|
|
15
42
|
rpcUrl: config.rpcUrl,
|
|
16
|
-
contractAddress: config.
|
|
17
|
-
usdcAddress: config.usdcAddress,
|
|
18
|
-
chainId: config.chainId,
|
|
43
|
+
contractAddress: config.channelsContractAddress,
|
|
19
44
|
});
|
|
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
|
+
}
|
|
66
|
+
}
|
|
20
67
|
}
|
|
21
|
-
get
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
68
|
+
get channelsClient() {
|
|
69
|
+
return this._channelsClient;
|
|
70
|
+
}
|
|
71
|
+
// ── SpendingAuth handler ─────────────────────────────────────
|
|
72
|
+
/**
|
|
73
|
+
* Handle incoming SpendingAuth from a buyer.
|
|
74
|
+
* First auth: verify SpendingAuth, reserve on-chain, send AuthAck.
|
|
75
|
+
* Subsequent: verify SpendingAuth signature, validate monotonic increase, persist.
|
|
76
|
+
*/
|
|
77
|
+
async handleSpendingAuth(buyerPeerId, payload, paymentMux) {
|
|
78
|
+
// Per-buyer mutex: serialize concurrent auths for the same buyer
|
|
79
|
+
const existing = this._buyerLocks.get(buyerPeerId);
|
|
80
|
+
let result = 'rejected';
|
|
81
|
+
const lock = (existing ?? Promise.resolve()).then(async () => {
|
|
82
|
+
result = await this._handleSpendingAuthInner(buyerPeerId, payload, paymentMux);
|
|
83
|
+
});
|
|
84
|
+
this._buyerLocks.set(buyerPeerId, lock.catch(() => { }));
|
|
85
|
+
await lock;
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
async _handleSpendingAuthInner(buyerPeerId, payload, paymentMux) {
|
|
89
|
+
const buyerEvmAddr = peerIdToAddress(buyerPeerId);
|
|
90
|
+
try {
|
|
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
|
+
}
|
|
104
|
+
}
|
|
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';
|
|
182
|
+
}
|
|
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);
|
|
194
|
+
}
|
|
195
|
+
debugLog(`[SellerPayment] Top-up completed: channel=${channelId.slice(0, 18)}... new ceiling=${newMaxAmount}`);
|
|
196
|
+
return 'accepted';
|
|
197
|
+
}
|
|
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';
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
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;
|
|
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;
|
|
37
277
|
}
|
|
38
|
-
|
|
39
|
-
|
|
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);
|
|
319
|
+
}
|
|
320
|
+
// ── Per-request validation ──────────────────────────────────
|
|
321
|
+
/**
|
|
322
|
+
* Validate and accept a SpendingAuth attached to an incoming request.
|
|
323
|
+
* Returns true if the buyer has sufficient budget to serve this request.
|
|
324
|
+
*/
|
|
325
|
+
async validateAndAcceptAuth(buyerPeerId, auth) {
|
|
326
|
+
// Look up active session for this buyer
|
|
327
|
+
const session = this._channelStore.getActiveChannelByPeer(buyerPeerId, 'seller');
|
|
328
|
+
if (!session) {
|
|
329
|
+
debugWarn(`[SellerPayment] validateAndAcceptAuth: no active session for buyer ${buyerPeerId.slice(0, 12)}...`);
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
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;
|
|
337
|
+
}
|
|
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);
|
|
40
346
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
}
|
|
43
352
|
}
|
|
44
353
|
catch {
|
|
45
|
-
debugWarn(`[SellerPayment]
|
|
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,
|
|
371
|
+
});
|
|
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)}...`);
|
|
46
396
|
return;
|
|
47
397
|
}
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
debugWarn(`[SellerPayment]
|
|
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);
|
|
402
|
+
}
|
|
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');
|
|
410
|
+
if (!session) {
|
|
411
|
+
debugWarn(`[SellerPayment] settleSession: no active session for buyer ${buyerPeerId.slice(0, 12)}...`);
|
|
62
412
|
return;
|
|
63
413
|
}
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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`);
|
|
420
|
+
}
|
|
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
|
|
71
427
|
}
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
}
|
|
74
460
|
}
|
|
75
461
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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);
|
|
468
|
+
}
|
|
469
|
+
// ── Disconnect handling ───────────────────────────────────────
|
|
470
|
+
onBuyerDisconnect(buyerPeerId) {
|
|
471
|
+
const session = this._channelStore.getActiveChannelByPeer(buyerPeerId, 'seller');
|
|
472
|
+
if (!session)
|
|
81
473
|
return;
|
|
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
|
+
}
|
|
82
485
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
buyerEvmAddr,
|
|
87
|
-
nonce: payload.nonce,
|
|
88
|
-
authMax: maxAmount,
|
|
89
|
-
deadline: payload.deadline,
|
|
90
|
-
buyerSig: payload.buyerSig,
|
|
91
|
-
previousConsumption,
|
|
92
|
-
previousSessionId,
|
|
93
|
-
tokensDelivered: 0n,
|
|
94
|
-
reservedAt: Date.now(),
|
|
95
|
-
settled: false,
|
|
96
|
-
});
|
|
97
|
-
paymentMux.sendAuthAck({ sessionId: payload.sessionId, nonce: payload.nonce });
|
|
98
|
-
debugLog(`[SellerPayment] AuthAck sent for session=${payload.sessionId.slice(0, 18)}...`);
|
|
486
|
+
// Preserve session for reconnect; timeout checker handles ghost scenarios
|
|
487
|
+
this._activeBuyers.delete(buyerPeerId);
|
|
488
|
+
debugLog(`[SellerPayment] Buyer ${buyerPeerId.slice(0, 12)}... disconnected — channel ${session.sessionId.slice(0, 18)}... preserved for reconnect`);
|
|
99
489
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
490
|
+
// ── Stale session cleanup ────────────────────────────────────
|
|
491
|
+
/**
|
|
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.
|
|
497
|
+
* Called periodically and on startup for recovery.
|
|
498
|
+
*/
|
|
499
|
+
async checkTimeouts() {
|
|
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;
|
|
504
|
+
try {
|
|
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);
|
|
509
|
+
}
|
|
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);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
catch (err) {
|
|
525
|
+
debugWarn(`[SellerPayment] Failed to process channel ${channel.sessionId.slice(0, 18)}...: ${err instanceof Error ? err.message : err}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
106
528
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
529
|
+
// ── Queries ───────────────────────────────────────────────────
|
|
530
|
+
hasSession(buyerPeerId) {
|
|
531
|
+
return this._activeBuyers.has(buyerPeerId);
|
|
532
|
+
}
|
|
533
|
+
/** Get the active session for a buyer peer, or null. */
|
|
534
|
+
getChannelByPeer(buyerPeerId) {
|
|
535
|
+
return this._channelStore.getActiveChannelByPeer(buyerPeerId, 'seller');
|
|
536
|
+
}
|
|
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;
|
|
544
|
+
}
|
|
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
|
|
550
|
+
/**
|
|
551
|
+
* Build the PaymentRequired payload for a buyer that doesn't have a session.
|
|
552
|
+
*/
|
|
553
|
+
getPaymentRequirements(requestId, buyerPeerId, pricing) {
|
|
554
|
+
const minBudgetPerRequest = this._config.minBudgetPerRequest ?? DEFAULT_MIN_BUDGET_PER_REQUEST;
|
|
555
|
+
let suggestedAmount = SellerPaymentManager.DEFAULT_SUGGESTED_AMOUNT;
|
|
556
|
+
if (buyerPeerId) {
|
|
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;
|
|
562
|
+
}
|
|
112
563
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
564
|
+
return {
|
|
565
|
+
minBudgetPerRequest,
|
|
566
|
+
suggestedAmount: suggestedAmount.toString(),
|
|
567
|
+
requestId,
|
|
568
|
+
...(pricing?.inputUsdPerMillion != null ? { inputUsdPerMillion: pricing.inputUsdPerMillion } : {}),
|
|
569
|
+
...(pricing?.outputUsdPerMillion != null ? { outputUsdPerMillion: pricing.outputUsdPerMillion } : {}),
|
|
570
|
+
};
|
|
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}`);
|
|
117
584
|
try {
|
|
118
|
-
await this.
|
|
119
|
-
|
|
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`);
|
|
120
588
|
}
|
|
121
589
|
catch (err) {
|
|
122
|
-
debugWarn(`[SellerPayment]
|
|
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;
|
|
123
605
|
}
|
|
124
606
|
}
|
|
125
607
|
else {
|
|
126
|
-
|
|
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);
|
|
127
623
|
}
|
|
128
|
-
this._sessions.delete(buyerPeerId);
|
|
129
624
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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) {
|
|
134
630
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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;
|
|
138
642
|
}
|
|
139
643
|
catch (err) {
|
|
140
|
-
debugWarn(`[SellerPayment]
|
|
141
|
-
|
|
644
|
+
debugWarn(`[SellerPayment] Failed to poll CloseRequested events: ${err instanceof Error ? err.message : err}`);
|
|
645
|
+
return fromBlock; // Retry from same block on next poll
|
|
142
646
|
}
|
|
143
647
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
async stake(amount) {
|
|
148
|
-
return this._escrow.stake(this._signer, amount);
|
|
149
|
-
}
|
|
150
|
-
async unstake(amount) {
|
|
151
|
-
return this._escrow.unstake(this._signer, amount);
|
|
152
|
-
}
|
|
153
|
-
async getPendingEarnings() {
|
|
154
|
-
const addr = await this._signer.getAddress();
|
|
155
|
-
return this._escrow.getSellerPendingEarnings(addr);
|
|
156
|
-
}
|
|
157
|
-
_computeChargeAmount(session) {
|
|
158
|
-
return session.authMax;
|
|
648
|
+
// ── Lifecycle ─────────────────────────────────────────────────
|
|
649
|
+
close() {
|
|
650
|
+
// ChannelStore is shared with BuyerPaymentManager, closed from node.ts
|
|
159
651
|
}
|
|
160
652
|
}
|
|
161
653
|
//# sourceMappingURL=seller-payment-manager.js.map
|