@antseed/node 0.2.27 → 0.2.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/README.md +14 -13
  2. package/dist/buyer-request-handler.d.ts +41 -0
  3. package/dist/buyer-request-handler.d.ts.map +1 -0
  4. package/dist/buyer-request-handler.js +254 -0
  5. package/dist/buyer-request-handler.js.map +1 -0
  6. package/dist/discovery/announcer.d.ts +5 -4
  7. package/dist/discovery/announcer.d.ts.map +1 -1
  8. package/dist/discovery/announcer.js +11 -18
  9. package/dist/discovery/announcer.js.map +1 -1
  10. package/dist/discovery/index.d.ts +0 -1
  11. package/dist/discovery/index.d.ts.map +1 -1
  12. package/dist/discovery/index.js +0 -1
  13. package/dist/discovery/index.js.map +1 -1
  14. package/dist/discovery/metadata-codec.d.ts +2 -2
  15. package/dist/discovery/metadata-codec.d.ts.map +1 -1
  16. package/dist/discovery/metadata-codec.js +47 -72
  17. package/dist/discovery/metadata-codec.js.map +1 -1
  18. package/dist/discovery/metadata-validator.js +6 -6
  19. package/dist/discovery/metadata-validator.js.map +1 -1
  20. package/dist/discovery/peer-lookup.d.ts.map +1 -1
  21. package/dist/discovery/peer-lookup.js +1 -2
  22. package/dist/discovery/peer-lookup.js.map +1 -1
  23. package/dist/discovery/peer-metadata.d.ts +3 -5
  24. package/dist/discovery/peer-metadata.d.ts.map +1 -1
  25. package/dist/discovery/peer-metadata.js +1 -1
  26. package/dist/discovery/reputation-verifier.d.ts +2 -25
  27. package/dist/discovery/reputation-verifier.d.ts.map +1 -1
  28. package/dist/discovery/reputation-verifier.js +2 -48
  29. package/dist/discovery/reputation-verifier.js.map +1 -1
  30. package/dist/discovery/stats-verifier.d.ts +27 -0
  31. package/dist/discovery/stats-verifier.d.ts.map +1 -0
  32. package/dist/discovery/stats-verifier.js +38 -0
  33. package/dist/discovery/stats-verifier.js.map +1 -0
  34. package/dist/index.d.ts +10 -7
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +8 -5
  37. package/dist/index.js.map +1 -1
  38. package/dist/metering/index.d.ts +1 -0
  39. package/dist/metering/index.d.ts.map +1 -1
  40. package/dist/metering/index.js +1 -0
  41. package/dist/metering/index.js.map +1 -1
  42. package/dist/metering/receipt-generator.d.ts +4 -4
  43. package/dist/metering/receipt-generator.d.ts.map +1 -1
  44. package/dist/metering/receipt-verifier.d.ts +6 -6
  45. package/dist/metering/receipt-verifier.d.ts.map +1 -1
  46. package/dist/metering/receipt-verifier.js +1 -1
  47. package/dist/metering/seller-session-tracker.d.ts +91 -0
  48. package/dist/metering/seller-session-tracker.d.ts.map +1 -0
  49. package/dist/metering/seller-session-tracker.js +261 -0
  50. package/dist/metering/seller-session-tracker.js.map +1 -0
  51. package/dist/metering/storage.d.ts +11 -5
  52. package/dist/metering/storage.d.ts.map +1 -1
  53. package/dist/metering/storage.js +28 -80
  54. package/dist/metering/storage.js.map +1 -1
  55. package/dist/node.d.ts +69 -117
  56. package/dist/node.d.ts.map +1 -1
  57. package/dist/node.js +240 -1269
  58. package/dist/node.js.map +1 -1
  59. package/dist/p2p/connection-auth.d.ts +2 -1
  60. package/dist/p2p/connection-auth.d.ts.map +1 -1
  61. package/dist/p2p/connection-auth.js +6 -6
  62. package/dist/p2p/connection-auth.js.map +1 -1
  63. package/dist/p2p/connection-manager.d.ts +3 -2
  64. package/dist/p2p/connection-manager.d.ts.map +1 -1
  65. package/dist/p2p/connection-manager.js +6 -6
  66. package/dist/p2p/connection-manager.js.map +1 -1
  67. package/dist/p2p/identity.d.ts +22 -15
  68. package/dist/p2p/identity.d.ts.map +1 -1
  69. package/dist/p2p/identity.js +66 -51
  70. package/dist/p2p/identity.js.map +1 -1
  71. package/dist/p2p/index.d.ts +1 -1
  72. package/dist/p2p/index.d.ts.map +1 -1
  73. package/dist/p2p/index.js +1 -1
  74. package/dist/p2p/index.js.map +1 -1
  75. package/dist/p2p/payment-codec.d.ts +4 -8
  76. package/dist/p2p/payment-codec.d.ts.map +1 -1
  77. package/dist/p2p/payment-codec.js +27 -57
  78. package/dist/p2p/payment-codec.js.map +1 -1
  79. package/dist/p2p/payment-mux.d.ts +4 -10
  80. package/dist/p2p/payment-mux.d.ts.map +1 -1
  81. package/dist/p2p/payment-mux.js +11 -33
  82. package/dist/p2p/payment-mux.js.map +1 -1
  83. package/dist/payments/balance-manager.d.ts +2 -2
  84. package/dist/payments/balance-manager.d.ts.map +1 -1
  85. package/dist/payments/balance-manager.js +5 -5
  86. package/dist/payments/balance-manager.js.map +1 -1
  87. package/dist/payments/buyer-payment-manager.d.ts +154 -21
  88. package/dist/payments/buyer-payment-manager.d.ts.map +1 -1
  89. package/dist/payments/buyer-payment-manager.js +540 -166
  90. package/dist/payments/buyer-payment-manager.js.map +1 -1
  91. package/dist/payments/buyer-payment-negotiator.d.ts +84 -0
  92. package/dist/payments/buyer-payment-negotiator.d.ts.map +1 -0
  93. package/dist/payments/buyer-payment-negotiator.js +624 -0
  94. package/dist/payments/buyer-payment-negotiator.js.map +1 -0
  95. package/dist/payments/chain-config.d.ts +10 -4
  96. package/dist/payments/chain-config.d.ts.map +1 -1
  97. package/dist/payments/chain-config.js +19 -9
  98. package/dist/payments/chain-config.js.map +1 -1
  99. package/dist/payments/channel-session-state.d.ts +13 -0
  100. package/dist/payments/channel-session-state.d.ts.map +1 -0
  101. package/dist/payments/channel-session-state.js +25 -0
  102. package/dist/payments/channel-session-state.js.map +1 -0
  103. package/dist/payments/channel-store.d.ts +87 -0
  104. package/dist/payments/channel-store.d.ts.map +1 -0
  105. package/dist/payments/channel-store.js +276 -0
  106. package/dist/payments/channel-store.js.map +1 -0
  107. package/dist/payments/evm/ants-token-client.d.ts +1 -1
  108. package/dist/payments/evm/ants-token-client.d.ts.map +1 -1
  109. package/dist/payments/evm/ants-token-client.js +3 -4
  110. package/dist/payments/evm/ants-token-client.js.map +1 -1
  111. package/dist/payments/evm/base-evm-client.d.ts +10 -1
  112. package/dist/payments/evm/base-evm-client.d.ts.map +1 -1
  113. package/dist/payments/evm/base-evm-client.js +34 -1
  114. package/dist/payments/evm/base-evm-client.js.map +1 -1
  115. package/dist/payments/evm/channels-client.d.ts +51 -0
  116. package/dist/payments/evm/channels-client.d.ts.map +1 -0
  117. package/dist/payments/evm/channels-client.js +101 -0
  118. package/dist/payments/evm/channels-client.js.map +1 -0
  119. package/dist/payments/evm/deposits-client.d.ts +30 -0
  120. package/dist/payments/evm/deposits-client.d.ts.map +1 -0
  121. package/dist/payments/evm/deposits-client.js +78 -0
  122. package/dist/payments/evm/deposits-client.js.map +1 -0
  123. package/dist/payments/evm/emissions-client.d.ts +3 -4
  124. package/dist/payments/evm/emissions-client.d.ts.map +1 -1
  125. package/dist/payments/evm/emissions-client.js +11 -30
  126. package/dist/payments/evm/emissions-client.js.map +1 -1
  127. package/dist/payments/evm/identity-client.d.ts +15 -23
  128. package/dist/payments/evm/identity-client.d.ts.map +1 -1
  129. package/dist/payments/evm/identity-client.js +68 -99
  130. package/dist/payments/evm/identity-client.js.map +1 -1
  131. package/dist/payments/evm/keypair.d.ts +3 -14
  132. package/dist/payments/evm/keypair.d.ts.map +1 -1
  133. package/dist/payments/evm/keypair.js +4 -20
  134. package/dist/payments/evm/keypair.js.map +1 -1
  135. package/dist/payments/evm/sessions-client.d.ts +30 -0
  136. package/dist/payments/evm/sessions-client.d.ts.map +1 -0
  137. package/dist/payments/evm/sessions-client.js +61 -0
  138. package/dist/payments/evm/sessions-client.js.map +1 -0
  139. package/dist/payments/evm/signatures.d.ts +43 -12
  140. package/dist/payments/evm/signatures.d.ts.map +1 -1
  141. package/dist/payments/evm/signatures.js +62 -45
  142. package/dist/payments/evm/signatures.js.map +1 -1
  143. package/dist/payments/evm/staking-client.d.ts +24 -0
  144. package/dist/payments/evm/staking-client.d.ts.map +1 -0
  145. package/dist/payments/evm/staking-client.js +54 -0
  146. package/dist/payments/evm/staking-client.js.map +1 -0
  147. package/dist/payments/evm/stats-client.d.ts +20 -0
  148. package/dist/payments/evm/stats-client.d.ts.map +1 -0
  149. package/dist/payments/evm/stats-client.js +25 -0
  150. package/dist/payments/evm/stats-client.js.map +1 -0
  151. package/dist/payments/index.d.ts +17 -10
  152. package/dist/payments/index.d.ts.map +1 -1
  153. package/dist/payments/index.js +15 -8
  154. package/dist/payments/index.js.map +1 -1
  155. package/dist/payments/pricing.d.ts +25 -0
  156. package/dist/payments/pricing.d.ts.map +1 -0
  157. package/dist/payments/pricing.js +33 -0
  158. package/dist/payments/pricing.js.map +1 -0
  159. package/dist/payments/readiness.d.ts +4 -3
  160. package/dist/payments/readiness.d.ts.map +1 -1
  161. package/dist/payments/readiness.js +11 -18
  162. package/dist/payments/readiness.js.map +1 -1
  163. package/dist/payments/seller-payment-manager.d.ts +72 -47
  164. package/dist/payments/seller-payment-manager.d.ts.map +1 -1
  165. package/dist/payments/seller-payment-manager.js +558 -275
  166. package/dist/payments/seller-payment-manager.js.map +1 -1
  167. package/dist/payments/session-store.d.ts +3 -0
  168. package/dist/payments/session-store.d.ts.map +1 -1
  169. package/dist/payments/session-store.js +31 -2
  170. package/dist/payments/session-store.js.map +1 -1
  171. package/dist/payments/types.d.ts +5 -3
  172. package/dist/payments/types.d.ts.map +1 -1
  173. package/dist/proxy/proxy-mux.d.ts.map +1 -1
  174. package/dist/proxy/proxy-mux.js +3 -2
  175. package/dist/proxy/proxy-mux.js.map +1 -1
  176. package/dist/proxy/request-codec.d.ts.map +1 -1
  177. package/dist/proxy/request-codec.js +3 -0
  178. package/dist/proxy/request-codec.js.map +1 -1
  179. package/dist/reputation/rating-manager.d.ts.map +1 -1
  180. package/dist/reputation/rating-manager.js +2 -4
  181. package/dist/reputation/rating-manager.js.map +1 -1
  182. package/dist/reputation/report-manager.d.ts.map +1 -1
  183. package/dist/reputation/report-manager.js +2 -4
  184. package/dist/reputation/report-manager.js.map +1 -1
  185. package/dist/routing/default-router.d.ts.map +1 -1
  186. package/dist/routing/default-router.js +4 -9
  187. package/dist/routing/default-router.js.map +1 -1
  188. package/dist/seller-request-handler.d.ts +54 -0
  189. package/dist/seller-request-handler.d.ts.map +1 -0
  190. package/dist/seller-request-handler.js +359 -0
  191. package/dist/seller-request-handler.js.map +1 -0
  192. package/dist/storage/migrate.d.ts +13 -0
  193. package/dist/storage/migrate.d.ts.map +1 -0
  194. package/dist/storage/migrate.js +28 -0
  195. package/dist/storage/migrate.js.map +1 -0
  196. package/dist/storage/migrations/channels/001_create_tables.d.ts +3 -0
  197. package/dist/storage/migrations/channels/001_create_tables.d.ts.map +1 -0
  198. package/dist/storage/migrations/channels/001_create_tables.js +45 -0
  199. package/dist/storage/migrations/channels/001_create_tables.js.map +1 -0
  200. package/dist/storage/migrations/channels/002_add_auth_sig_columns.d.ts +3 -0
  201. package/dist/storage/migrations/channels/002_add_auth_sig_columns.d.ts.map +1 -0
  202. package/dist/storage/migrations/channels/002_add_auth_sig_columns.js +19 -0
  203. package/dist/storage/migrations/channels/002_add_auth_sig_columns.js.map +1 -0
  204. package/dist/storage/migrations/channels/index.d.ts +3 -0
  205. package/dist/storage/migrations/channels/index.d.ts.map +1 -0
  206. package/dist/storage/migrations/channels/index.js +4 -0
  207. package/dist/storage/migrations/channels/index.js.map +1 -0
  208. package/dist/storage/migrations/metering/001_create_tables.d.ts +3 -0
  209. package/dist/storage/migrations/metering/001_create_tables.d.ts.map +1 -0
  210. package/dist/storage/migrations/metering/001_create_tables.js +80 -0
  211. package/dist/storage/migrations/metering/001_create_tables.js.map +1 -0
  212. package/dist/storage/migrations/metering/index.d.ts +3 -0
  213. package/dist/storage/migrations/metering/index.d.ts.map +1 -0
  214. package/dist/storage/migrations/metering/index.js +3 -0
  215. package/dist/storage/migrations/metering/index.js.map +1 -0
  216. package/dist/types/capability.d.ts +1 -1
  217. package/dist/types/metering.d.ts +1 -1
  218. package/dist/types/peer.d.ts +10 -11
  219. package/dist/types/peer.d.ts.map +1 -1
  220. package/dist/types/peer.js +7 -3
  221. package/dist/types/peer.js.map +1 -1
  222. package/dist/types/protocol.d.ts +22 -70
  223. package/dist/types/protocol.d.ts.map +1 -1
  224. package/dist/types/protocol.js +1 -3
  225. package/dist/types/protocol.js.map +1 -1
  226. package/dist/types/rating.d.ts +1 -1
  227. package/dist/types/rating.d.ts.map +1 -1
  228. package/dist/types/report.d.ts +1 -1
  229. package/dist/types/report.d.ts.map +1 -1
  230. package/dist/utils/response-usage.d.ts +10 -0
  231. package/dist/utils/response-usage.d.ts.map +1 -0
  232. package/dist/utils/response-usage.js +34 -0
  233. package/dist/utils/response-usage.js.map +1 -0
  234. package/package.json +3 -3
@@ -1,293 +1,528 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { verifyTypedData } from 'ethers';
3
- import { BaseEscrowClient } from './evm/escrow-client.js';
4
- import { identityToEvmWallet, identityToEvmAddress } from './evm/keypair.js';
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
- /** Default settle timeout: 24 hours. */
9
- const DEFAULT_SETTLE_TIMEOUT_SECS = 86400;
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 using the settle-then-reserve
12
- * atomic flow with persistent session storage.
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
- _escrowClient;
18
+ _channelsClient;
18
19
  _config;
19
- _sessionStore;
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
- constructor(identity, config, sessionStore) {
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 = identityToEvmWallet(identity);
34
- this._escrowClient = new BaseEscrowClient({
40
+ this._signer = identity.wallet;
41
+ this._channelsClient = new ChannelsClient({
35
42
  rpcUrl: config.rpcUrl,
36
- contractAddress: config.contractAddress,
37
- usdcAddress: config.usdcAddress,
43
+ contractAddress: config.channelsContractAddress,
38
44
  });
39
- this._sessionStore = sessionStore;
40
- // Hydrate _activeBuyers from persisted sessions so hasSession() works after restart
41
- const activeSessions = this._sessionStore.getActiveSessions('seller');
42
- for (const session of activeSessions) {
43
- this._activeBuyers.add(session.peerId);
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 escrowClient() {
47
- return this._escrowClient;
68
+ get channelsClient() {
69
+ return this._channelsClient;
48
70
  }
49
- // ── SpendingAuth handler (settle-then-reserve) ────────────────
71
+ // ── SpendingAuth handler ─────────────────────────────────────
50
72
  /**
51
73
  * Handle incoming SpendingAuth from a buyer.
52
- * 1. Verify EIP-712 signature
53
- * 2. Settle prior session if one exists
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, buyerEvmAddr, payload, paymentMux) {
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
- const lock = (existing ?? Promise.resolve()).then(() => this._handleSpendingAuthInner(buyerPeerId, buyerEvmAddr, payload, paymentMux));
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
- return lock;
85
+ await lock;
86
+ return result;
63
87
  }
64
- async _handleSpendingAuthInner(buyerPeerId, buyerEvmAddr, payload, paymentMux) {
88
+ async _handleSpendingAuthInner(buyerPeerId, payload, paymentMux) {
89
+ const buyerEvmAddr = peerIdToAddress(buyerPeerId);
65
90
  try {
66
- // 1. Verify EIP-712 signature
67
- const domain = makeEscrowDomain(this._config.chainId, this._config.contractAddress);
68
- const msg = {
69
- seller: identityToEvmAddress(this._identity),
70
- sessionId: payload.sessionId,
71
- maxAmount: BigInt(payload.maxAmountUsdc),
72
- nonce: payload.nonce,
73
- deadline: payload.deadline,
74
- previousConsumption: BigInt(payload.previousConsumption),
75
- previousSessionId: payload.previousSessionId,
76
- };
77
- const recoveredAddr = verifyTypedData(domain, SPENDING_AUTH_TYPES, msg, payload.buyerSig);
78
- if (recoveredAddr.toLowerCase() !== buyerEvmAddr.toLowerCase()) {
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
- catch (err) {
99
- debugWarn(`[SellerPayment] Failed to settle prior session: ${err instanceof Error ? err.message : err}`);
100
- // Do NOT proceed to reserve — proof chain is broken. Buyer will timeout and retry.
101
- return;
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
- // If buyer significantly understated consumption (> 20% gap), reject the
104
- // new SpendingAuth. Seller accepts the loss on this session but refuses to
105
- // continue serving a dishonest buyer. The 20% tolerance accounts for receipt
106
- // delivery timing (buyer may not have received the final receipt).
107
- if (sellerDelivered > 0n && buyerClaimed < sellerDelivered * 80n / 100n) {
108
- debugWarn(`[SellerPayment] Rejecting SpendingAuth — buyer under-reports consumption: ` +
109
- `claimed=${buyerClaimed} delivered=${sellerDelivered}`);
110
- return;
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
- // 3. Fetch tokenRate BEFORE reserve so a failure here doesn't orphan
114
- // an on-chain reservation the seller can't serve.
115
- const sellerEvmAddr = identityToEvmAddress(this._identity);
116
- const account = await this._escrowClient.getSellerAccount(sellerEvmAddr);
117
- this._tokenRate = account.tokenRate;
118
- if (this._tokenRate === 0n) {
119
- throw new Error('Token rate is 0 — set rate with antseed setTokenRate before serving');
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
- // ── Receipt sending ───────────────────────────────────────────
320
+ // ── Per-request validation ──────────────────────────────────
160
321
  /**
161
- * Send a bilateral receipt to the buyer after processing a request.
162
- * Also triggers TopUpRequest if consumption exceeds 80% of authMax.
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 sendReceipt(buyerPeerId, paymentMux, responseBody, tokensDelivered) {
165
- const session = this._sessionStore.getActiveSessionByPeer(buyerPeerId, 'seller');
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] No active session for buyer ${buyerPeerId.slice(0, 12)}... — skipping receipt`);
168
- return;
329
+ debugWarn(`[SellerPayment] validateAndAcceptAuth: no active session for buyer ${buyerPeerId.slice(0, 12)}...`);
330
+ return false;
169
331
  }
170
- // Update tokens cap at the effective token limit so the receipt total
171
- // always matches what settle() will store as settledTokenCount on-chain.
172
- // Without this cap, the buyer's previousConsumption (from tokensDelivered)
173
- // would exceed settledTokenCount (= maxAmount / tokenRate), breaking the
174
- // proof chain with InvalidProofChain on the next reserve().
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
- const effectiveTokenCap = authMax / tokenRate;
181
- let newTotal = BigInt(session.tokensDelivered) + tokensDelivered;
182
- if (newTotal > effectiveTokenCap) {
183
- newTotal = effectiveTokenCap;
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
- const newRequestCount = session.requestCount + 1;
186
- this._sessionStore.updateTokensDelivered(session.sessionId, newTotal.toString(), newRequestCount);
187
- // SHA-256 hash of response body
188
- const responseHash = createHash('sha256').update(responseBody).digest();
189
- // Build receipt message and sign with Ed25519
190
- const sessionIdBytes = hexToBytes(session.sessionId.replace(/^0x/, ''));
191
- const receiptMsg = buildReceiptMessage(sessionIdBytes, newTotal, newRequestCount, new Uint8Array(responseHash));
192
- const sellerSig = await signMessageEd25519(this._identity, receiptMsg);
193
- paymentMux.sendSellerReceipt({
194
- sessionId: session.sessionId,
195
- runningTotal: newTotal.toString(),
196
- requestCount: newRequestCount,
197
- responseHash: bytesToHex(new Uint8Array(responseHash)),
198
- sellerSig: bytesToHex(sellerSig),
199
- });
200
- // Store receipt
201
- this._sessionStore.insertReceipt({
202
- sessionId: session.sessionId,
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
- debugLog(`[SellerPayment] TopUpRequest sent: session=${session.sessionId.slice(0, 18)}... (${newTotal}/${authMax})`);
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
- // ── BuyerAck handler ──────────────────────────────────────────
226
- async handleBuyerAck(buyerPeerId, payload) {
227
- const session = this._sessionStore.getActiveSessionByPeer(buyerPeerId, 'seller');
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] BuyerAck for unknown buyer: ${buyerPeerId.slice(0, 12)}...`);
411
+ debugWarn(`[SellerPayment] settleSession: no active session for buyer ${buyerPeerId.slice(0, 12)}...`);
230
412
  return;
231
413
  }
232
- try {
233
- // Verify buyer's Ed25519 ack signature
234
- const buyerPublicKey = hexToBytes(buyerPeerId);
235
- const sessionIdBytes = hexToBytes(session.sessionId.replace(/^0x/, ''));
236
- const ackMsg = buildAckMessage(sessionIdBytes, BigInt(payload.runningTotal), payload.requestCount);
237
- const sigBytes = hexToBytes(payload.buyerSig);
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
- catch (err) {
248
- debugWarn(`[SellerPayment] Failed to process BuyerAck: ${err instanceof Error ? err.message : err}`);
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._sessionStore.getActiveSessionByPeer(buyerPeerId, 'seller');
471
+ const session = this._channelStore.getActiveChannelByPeer(buyerPeerId, 'seller');
254
472
  if (!session)
255
473
  return;
256
- // Don't settle immediately wait for buyer to return with next auth.
257
- // Session persists in store; timeout checker will handle ghost scenarios.
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 — session ${session.sessionId.slice(0, 18)}... preserved for reconnect`);
488
+ debugLog(`[SellerPayment] Buyer ${buyerPeerId.slice(0, 12)}... disconnected — channel ${session.sessionId.slice(0, 18)}... preserved for reconnect`);
260
489
  }
261
- // ── Timeout management ────────────────────────────────────────
490
+ // ── Stale session cleanup ────────────────────────────────────
262
491
  /**
263
- * Check for and settle timed-out sessions.
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 timeoutSecs = this._config.settleTimeoutSecs ?? DEFAULT_SETTLE_TIMEOUT_SECS;
268
- const timedOut = this._sessionStore.getTimedOutSessions(timeoutSecs);
269
- for (const session of timedOut) {
270
- if (session.status !== 'active')
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
- const delivered = BigInt(session.tokensDelivered);
274
- if (delivered > 0n) {
275
- // Seller delivered tokens settle normally to get paid and avoid ghost penalty
276
- debugLog(`[SellerPayment] Settling timed-out session ${session.sessionId.slice(0, 18)}... with ${delivered} tokens delivered`);
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
- else {
281
- // No delivery use settleTimeout (records ghost, releases buyer funds)
282
- debugLog(`[SellerPayment] Settling timed-out session ${session.sessionId.slice(0, 18)}... (no delivery)`);
283
- await this._escrowClient.settleTimeout(this._signer, session.sessionId);
284
- this._sessionStore.updateSessionStatus(session.sessionId, 'timeout');
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 settle timeout for ${session.sessionId.slice(0, 18)}...: ${err instanceof Error ? err.message : err}`);
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
- /** Minimum interval between init retry attempts. */
299
- _lastInitAttemptMs = 0;
300
- static INIT_RETRY_INTERVAL_MS = 30_000;
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
- /** Retry init if on-chain data is missing and enough time has passed. */
322
- async ensureInitialized() {
323
- if (this._tokenRate !== null && this._firstSignCap !== null)
324
- return;
325
- if (Date.now() - this._lastInitAttemptMs < SellerPaymentManager.INIT_RETRY_INTERVAL_MS)
326
- return;
327
- await this.init();
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
- static DEFAULT_SUGGESTED_AMOUNT = 100000n; // $0.10
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 sellerEvmAddr = identityToEvmAddress(this._identity);
338
- const tokenRate = this._tokenRate;
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._sessionStore.getLatestSession(buyerPeerId, 'seller');
351
- if (priorSession && BigInt(priorSession.tokensDelivered) > 0n) {
352
- suggestedAmount = provenSignAmount;
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
- sellerEvmAddr,
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
- // SessionStore is shared with BuyerPaymentManager, closed from node.ts
650
+ // ChannelStore is shared with BuyerPaymentManager, closed from node.ts
368
651
  }
369
652
  }
370
653
  //# sourceMappingURL=seller-payment-manager.js.map