@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.
Files changed (255) hide show
  1. package/README.md +85 -11
  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 -2
  7. package/dist/discovery/announcer.d.ts.map +1 -1
  8. package/dist/discovery/announcer.js +11 -14
  9. package/dist/discovery/announcer.js.map +1 -1
  10. package/dist/discovery/index.d.ts +0 -2
  11. package/dist/discovery/index.d.ts.map +1 -1
  12. package/dist/discovery/index.js +0 -2
  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 -22
  27. package/dist/discovery/reputation-verifier.d.ts.map +1 -1
  28. package/dist/discovery/reputation-verifier.js +2 -24
  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 +17 -5
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +11 -3
  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 +83 -104
  56. package/dist/node.d.ts.map +1 -1
  57. package/dist/node.js +313 -1026
  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 +12 -6
  66. package/dist/p2p/connection-manager.js.map +1 -1
  67. package/dist/p2p/identity.d.ts +44 -13
  68. package/dist/p2p/identity.d.ts.map +1 -1
  69. package/dist/p2p/identity.js +103 -49
  70. package/dist/p2p/identity.js.map +1 -1
  71. package/dist/p2p/index.d.ts +1 -3
  72. package/dist/p2p/index.d.ts.map +1 -1
  73. package/dist/p2p/index.js +1 -3
  74. package/dist/p2p/index.js.map +1 -1
  75. package/dist/p2p/payment-codec.d.ts +9 -19
  76. package/dist/p2p/payment-codec.d.ts.map +1 -1
  77. package/dist/p2p/payment-codec.js +41 -89
  78. package/dist/p2p/payment-codec.js.map +1 -1
  79. package/dist/p2p/payment-mux.d.ts +14 -29
  80. package/dist/p2p/payment-mux.d.ts.map +1 -1
  81. package/dist/p2p/payment-mux.js +41 -79
  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 +157 -83
  88. package/dist/payments/buyer-payment-manager.d.ts.map +1 -1
  89. package/dist/payments/buyer-payment-manager.js +573 -204
  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 +44 -0
  96. package/dist/payments/chain-config.d.ts.map +1 -0
  97. package/dist/payments/chain-config.js +70 -0
  98. package/dist/payments/chain-config.js.map +1 -0
  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 +16 -0
  108. package/dist/payments/evm/ants-token-client.d.ts.map +1 -0
  109. package/dist/payments/evm/ants-token-client.js +65 -0
  110. package/dist/payments/evm/ants-token-client.js.map +1 -0
  111. package/dist/payments/evm/base-evm-client.d.ts +22 -0
  112. package/dist/payments/evm/base-evm-client.d.ts.map +1 -0
  113. package/dist/payments/evm/base-evm-client.js +71 -0
  114. package/dist/payments/evm/base-evm-client.js.map +1 -0
  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 +22 -0
  124. package/dist/payments/evm/emissions-client.d.ts.map +1 -0
  125. package/dist/payments/evm/emissions-client.js +65 -0
  126. package/dist/payments/evm/emissions-client.js.map +1 -0
  127. package/dist/payments/evm/escrow-client.d.ts +57 -36
  128. package/dist/payments/evm/escrow-client.d.ts.map +1 -1
  129. package/dist/payments/evm/escrow-client.js +200 -93
  130. package/dist/payments/evm/escrow-client.js.map +1 -1
  131. package/dist/payments/evm/identity-client.d.ts +21 -0
  132. package/dist/payments/evm/identity-client.d.ts.map +1 -0
  133. package/dist/payments/evm/identity-client.js +68 -0
  134. package/dist/payments/evm/identity-client.js.map +1 -0
  135. package/dist/payments/evm/keypair.d.ts +3 -14
  136. package/dist/payments/evm/keypair.d.ts.map +1 -1
  137. package/dist/payments/evm/keypair.js +4 -20
  138. package/dist/payments/evm/keypair.js.map +1 -1
  139. package/dist/payments/evm/sessions-client.d.ts +30 -0
  140. package/dist/payments/evm/sessions-client.d.ts.map +1 -0
  141. package/dist/payments/evm/sessions-client.js +61 -0
  142. package/dist/payments/evm/sessions-client.js.map +1 -0
  143. package/dist/payments/evm/signatures.d.ts +54 -10
  144. package/dist/payments/evm/signatures.d.ts.map +1 -1
  145. package/dist/payments/evm/signatures.js +80 -54
  146. package/dist/payments/evm/signatures.js.map +1 -1
  147. package/dist/payments/evm/staking-client.d.ts +24 -0
  148. package/dist/payments/evm/staking-client.d.ts.map +1 -0
  149. package/dist/payments/evm/staking-client.js +54 -0
  150. package/dist/payments/evm/staking-client.js.map +1 -0
  151. package/dist/payments/evm/stats-client.d.ts +20 -0
  152. package/dist/payments/evm/stats-client.d.ts.map +1 -0
  153. package/dist/payments/evm/stats-client.js +25 -0
  154. package/dist/payments/evm/stats-client.js.map +1 -0
  155. package/dist/payments/evm/subpool-client.d.ts +30 -0
  156. package/dist/payments/evm/subpool-client.d.ts.map +1 -0
  157. package/dist/payments/evm/subpool-client.js +158 -0
  158. package/dist/payments/evm/subpool-client.js.map +1 -0
  159. package/dist/payments/index.d.ts +29 -9
  160. package/dist/payments/index.d.ts.map +1 -1
  161. package/dist/payments/index.js +27 -9
  162. package/dist/payments/index.js.map +1 -1
  163. package/dist/payments/pricing.d.ts +25 -0
  164. package/dist/payments/pricing.d.ts.map +1 -0
  165. package/dist/payments/pricing.js +33 -0
  166. package/dist/payments/pricing.js.map +1 -0
  167. package/dist/payments/readiness.d.ts +13 -0
  168. package/dist/payments/readiness.d.ts.map +1 -0
  169. package/dist/payments/readiness.js +57 -0
  170. package/dist/payments/readiness.js.map +1 -0
  171. package/dist/payments/seller-payment-manager.d.ts +101 -36
  172. package/dist/payments/seller-payment-manager.d.ts.map +1 -1
  173. package/dist/payments/seller-payment-manager.js +612 -120
  174. package/dist/payments/seller-payment-manager.js.map +1 -1
  175. package/dist/payments/session-store.d.ts +68 -0
  176. package/dist/payments/session-store.d.ts.map +1 -0
  177. package/dist/payments/session-store.js +272 -0
  178. package/dist/payments/session-store.js.map +1 -0
  179. package/dist/payments/types.d.ts +5 -3
  180. package/dist/payments/types.d.ts.map +1 -1
  181. package/dist/payments/usdc-utils.d.ts +9 -0
  182. package/dist/payments/usdc-utils.d.ts.map +1 -0
  183. package/dist/payments/usdc-utils.js +17 -0
  184. package/dist/payments/usdc-utils.js.map +1 -0
  185. package/dist/proxy/proxy-mux.d.ts.map +1 -1
  186. package/dist/proxy/proxy-mux.js +3 -2
  187. package/dist/proxy/proxy-mux.js.map +1 -1
  188. package/dist/proxy/request-codec.d.ts.map +1 -1
  189. package/dist/proxy/request-codec.js +3 -0
  190. package/dist/proxy/request-codec.js.map +1 -1
  191. package/dist/reputation/rating-manager.d.ts.map +1 -1
  192. package/dist/reputation/rating-manager.js +2 -4
  193. package/dist/reputation/rating-manager.js.map +1 -1
  194. package/dist/reputation/report-manager.d.ts.map +1 -1
  195. package/dist/reputation/report-manager.js +2 -4
  196. package/dist/reputation/report-manager.js.map +1 -1
  197. package/dist/routing/default-router.d.ts.map +1 -1
  198. package/dist/routing/default-router.js +4 -9
  199. package/dist/routing/default-router.js.map +1 -1
  200. package/dist/seller-request-handler.d.ts +54 -0
  201. package/dist/seller-request-handler.d.ts.map +1 -0
  202. package/dist/seller-request-handler.js +359 -0
  203. package/dist/seller-request-handler.js.map +1 -0
  204. package/dist/storage/migrate.d.ts +13 -0
  205. package/dist/storage/migrate.d.ts.map +1 -0
  206. package/dist/storage/migrate.js +28 -0
  207. package/dist/storage/migrate.js.map +1 -0
  208. package/dist/storage/migrations/channels/001_create_tables.d.ts +3 -0
  209. package/dist/storage/migrations/channels/001_create_tables.d.ts.map +1 -0
  210. package/dist/storage/migrations/channels/001_create_tables.js +45 -0
  211. package/dist/storage/migrations/channels/001_create_tables.js.map +1 -0
  212. package/dist/storage/migrations/channels/002_add_auth_sig_columns.d.ts +3 -0
  213. package/dist/storage/migrations/channels/002_add_auth_sig_columns.d.ts.map +1 -0
  214. package/dist/storage/migrations/channels/002_add_auth_sig_columns.js +19 -0
  215. package/dist/storage/migrations/channels/002_add_auth_sig_columns.js.map +1 -0
  216. package/dist/storage/migrations/channels/index.d.ts +3 -0
  217. package/dist/storage/migrations/channels/index.d.ts.map +1 -0
  218. package/dist/storage/migrations/channels/index.js +4 -0
  219. package/dist/storage/migrations/channels/index.js.map +1 -0
  220. package/dist/storage/migrations/metering/001_create_tables.d.ts +3 -0
  221. package/dist/storage/migrations/metering/001_create_tables.d.ts.map +1 -0
  222. package/dist/storage/migrations/metering/001_create_tables.js +80 -0
  223. package/dist/storage/migrations/metering/001_create_tables.js.map +1 -0
  224. package/dist/storage/migrations/metering/index.d.ts +3 -0
  225. package/dist/storage/migrations/metering/index.d.ts.map +1 -0
  226. package/dist/storage/migrations/metering/index.js +3 -0
  227. package/dist/storage/migrations/metering/index.js.map +1 -0
  228. package/dist/types/capability.d.ts +1 -1
  229. package/dist/types/http.d.ts +2 -0
  230. package/dist/types/http.d.ts.map +1 -1
  231. package/dist/types/http.js +2 -0
  232. package/dist/types/http.js.map +1 -1
  233. package/dist/types/index.d.ts +0 -1
  234. package/dist/types/index.d.ts.map +1 -1
  235. package/dist/types/index.js +0 -1
  236. package/dist/types/index.js.map +1 -1
  237. package/dist/types/metering.d.ts +2 -2
  238. package/dist/types/metering.d.ts.map +1 -1
  239. package/dist/types/peer.d.ts +10 -11
  240. package/dist/types/peer.d.ts.map +1 -1
  241. package/dist/types/peer.js +7 -3
  242. package/dist/types/peer.js.map +1 -1
  243. package/dist/types/protocol.d.ts +32 -97
  244. package/dist/types/protocol.d.ts.map +1 -1
  245. package/dist/types/protocol.js +5 -10
  246. package/dist/types/protocol.js.map +1 -1
  247. package/dist/types/rating.d.ts +1 -1
  248. package/dist/types/rating.d.ts.map +1 -1
  249. package/dist/types/report.d.ts +1 -1
  250. package/dist/types/report.d.ts.map +1 -1
  251. package/dist/utils/response-usage.d.ts +10 -0
  252. package/dist/utils/response-usage.d.ts.map +1 -0
  253. package/dist/utils/response-usage.js +34 -0
  254. package/dist/utils/response-usage.js.map +1 -0
  255. package/package.json +4 -3
@@ -1,161 +1,653 @@
1
1
  import { verifyTypedData } from 'ethers';
2
- import { EscrowClient } from './evm/escrow-client.js';
3
- import { identityToEvmWallet } from './evm/keypair.js';
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
- _escrow;
18
+ _channelsClient;
9
19
  _config;
10
- _sessions = new Map();
11
- constructor(identity, config) {
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 = identityToEvmWallet(identity);
14
- this._escrow = new EscrowClient({
40
+ this._signer = identity.wallet;
41
+ this._channelsClient = new ChannelsClient({
15
42
  rpcUrl: config.rpcUrl,
16
- contractAddress: config.contractAddress,
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 signer() { return this._signer; }
22
- get escrowClient() { return this._escrow; }
23
- hasSession(buyerPeerId) {
24
- const s = this._sessions.get(buyerPeerId);
25
- return !!s && !s.settled;
26
- }
27
- setBuyerEvmAddress(buyerPeerId, evmAddress) {
28
- const session = this._sessions.get(buyerPeerId);
29
- if (session)
30
- session.buyerEvmAddr = evmAddress;
31
- }
32
- async handleSpendingAuthV2(buyerPeerId, buyerEvmAddr, payload, paymentMux) {
33
- const now = Math.floor(Date.now() / 1000);
34
- if (!payload.deadline || payload.deadline <= now) {
35
- debugWarn(`[SellerPayment] Expired SpendingAuthV2 from ${buyerPeerId.slice(0, 12)}... deadline=${payload.deadline}`);
36
- return;
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
- let maxAmount;
39
- let previousConsumption;
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
- maxAmount = BigInt(payload.maxAmountUsdc);
42
- previousConsumption = BigInt(payload.previousConsumption ?? '0');
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] Invalid amounts in SpendingAuthV2 from ${buyerPeerId.slice(0, 12)}...`);
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 previousSessionId = payload.previousSessionId ?? ('0x' + '00'.repeat(32));
49
- const sellerAddr = await this._signer.getAddress();
50
- const domain = makeEscrowDomain(this._config.chainId, this._config.contractAddress);
51
- const recovered = verifyTypedData(domain, SPENDING_AUTH_V2_TYPES, {
52
- seller: sellerAddr,
53
- sessionId: payload.sessionId,
54
- maxAmount,
55
- nonce: payload.nonce,
56
- deadline: payload.deadline,
57
- previousConsumption,
58
- previousSessionId,
59
- }, payload.buyerSig);
60
- if (recovered.toLowerCase() !== buyerEvmAddr.toLowerCase()) {
61
- debugWarn(`[SellerPayment] Invalid V2 sig from ${buyerPeerId.slice(0, 12)}... recovered=${recovered.slice(0, 10)}... expected=${buyerEvmAddr.slice(0, 10)}...`);
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 prevSession = this._sessions.get(buyerPeerId);
65
- if (prevSession && !prevSession.settled) {
66
- debugLog(`[SellerPayment] Settling previous session ${prevSession.sessionId.slice(0, 18)}... before reserve()`);
67
- try {
68
- const chargeAmount = this._computeChargeAmount(prevSession);
69
- await this._escrow.settle(this._signer, prevSession.sessionId, chargeAmount);
70
- prevSession.settled = true;
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
- catch (err) {
73
- debugWarn(`[SellerPayment] Failed to settle previous session: ${err}`);
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
- try {
77
- await this._escrow.reserve(this._signer, payload.sessionId, buyerEvmAddr, maxAmount, payload.nonce, payload.deadline, previousConsumption, previousSessionId, payload.buyerSig);
78
- }
79
- catch (err) {
80
- debugWarn(`[SellerPayment] reserve() failed for ${buyerPeerId.slice(0, 12)}...: ${err}`);
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
- this._sessions.set(buyerPeerId, {
84
- sessionId: payload.sessionId,
85
- buyerPeerId,
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
- addTokensDelivered(buyerPeerId, tokens) {
101
- const session = this._sessions.get(buyerPeerId);
102
- if (!session || session.settled)
103
- return;
104
- session.tokensDelivered += tokens;
105
- debugLog(`[SellerPayment] Tokens accrued for ${buyerPeerId.slice(0, 12)}...: ${session.tokensDelivered}`);
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
- async onBuyerDisconnect(buyerPeerId) {
108
- const session = this._sessions.get(buyerPeerId);
109
- if (!session || session.settled) {
110
- this._sessions.delete(buyerPeerId);
111
- return;
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
- const settleTimeoutMs = 24 * 60 * 60 * 1000;
114
- const elapsed = Date.now() - session.reservedAt;
115
- if (elapsed >= settleTimeoutMs) {
116
- debugLog(`[SellerPayment] settleTimeout() for ghost session ${session.sessionId.slice(0, 18)}...`);
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._escrow.settleTimeout(this._signer, session.sessionId);
119
- session.settled = true;
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] settleTimeout() failed: ${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;
123
605
  }
124
606
  }
125
607
  else {
126
- debugLog('[SellerPayment] Buyer disconnected, session not yet timeout-eligible. Will settle on next SpendingAuthV2.');
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
- async settleSession(buyerPeerId, chargeAmount) {
131
- const session = this._sessions.get(buyerPeerId);
132
- if (!session || session.settled)
133
- return;
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
- await this._escrow.settle(this._signer, session.sessionId, chargeAmount);
136
- session.settled = true;
137
- debugLog(`[SellerPayment] Settled session ${session.sessionId.slice(0, 18)}... charged=${chargeAmount}`);
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] settle() failed: ${err}`);
141
- throw 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
142
646
  }
143
647
  }
144
- async claimEarnings() {
145
- return this._escrow.claimEarnings(this._signer);
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