@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,280 +1,649 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { BaseEscrowClient } from './evm/escrow-client.js';
3
- import { identityToEvmWallet, identityToEvmAddress } from './evm/keypair.js';
4
- import { buildLockMessageHash, buildSettlementMessageHash, buildExtendLockMessageHash, signMessageEcdsa, buildAckMessage, signMessageEd25519, } from './evm/signatures.js';
5
- import { bytesToHex, hexToBytes } from '../utils/hex.js';
2
+ import { DepositsClient } from './evm/deposits-client.js';
3
+ import { signSpendingAuth, signReserveAuth, makeChannelsDomain, computeMetadataHash, encodeMetadata, ZERO_METADATA, ZERO_METADATA_HASH, computeChannelId, } from './evm/signatures.js';
6
4
  import { debugLog, debugWarn } from '../utils/debug.js';
5
+ import { peerIdToAddress } from '../types/peer.js';
6
+ import { estimateCostFromBytes } from './pricing.js';
7
+ // ── Response cost header constants ───────────────────────────────
8
+ const HEADER_COST = 'x-antseed-cost';
9
+ const HEADER_INPUT_TOKENS = 'x-antseed-input-tokens';
10
+ const HEADER_OUTPUT_TOKENS = 'x-antseed-output-tokens';
11
+ /** Default tolerance: accept seller claims up to 1.4x buyer's bytes/4 estimate. */
12
+ const DEFAULT_COST_TOLERANCE = 1.4;
13
+ /** Fraction of reserve ceiling at which to signal a top-up is needed. */
14
+ /** Must match or exceed contract's TOP_UP_SETTLED_THRESHOLD_BPS (85%). */
15
+ const DEFAULT_TOPUP_THRESHOLD = 0.85;
7
16
  /**
8
- * Manages buyer-side bilateral payment sessions across seller connections.
9
- *
10
- * Handles the full lifecycle: lock initiation, receipt acknowledgement,
11
- * top-up approval, and session settlement.
17
+ * Manages buyer-side payment sessions using EIP-712 SpendingAuth
18
+ * with cumulative authorization, bytes/4 cost verification, and overdraft control.
12
19
  */
13
20
  export class BuyerPaymentManager {
14
21
  _identity;
15
22
  _signer;
16
- _escrowClient;
23
+ _depositsClient;
17
24
  _config;
18
- _sessions = new Map();
19
- constructor(identity, config) {
25
+ _channelStore;
26
+ /** In-memory map of active confirmed sessions by seller peerId for fast lookups. */
27
+ _confirmedPeers = new Set();
28
+ /** Peers that explicitly rejected our spending auth. */
29
+ _rejectedPeers = new Set();
30
+ /** sellerPeerId -> cumulative USDC amount in the latest SpendingAuth */
31
+ _cumulativeAmount = new Map();
32
+ /** sellerPeerId -> cumulative metadata for SpendingAuth */
33
+ _metadata = new Map();
34
+ /** sellerPeerId -> buyer-verified cumulative cost from bytes/4 */
35
+ _verifiedCost = new Map();
36
+ /** sellerPeerId -> pricing learned from 402 / peer metadata at session start */
37
+ _sessionPricing = new Map();
38
+ /** Cumulative response token totals per seller, tracked independently of signing metadata. */
39
+ _responseTokenTotals = new Map();
40
+ /** sellerPeerId -> current on-chain reserve ceiling (can grow with top-ups) */
41
+ _currentReserveCeiling = new Map();
42
+ /** sellerPeerId -> salt used in the current reserve */
43
+ _reserveSalt = new Map();
44
+ /** Cached EIP-712 domain — static for the lifetime of this manager. */
45
+ _channelsDomain;
46
+ constructor(identity, config, channelStore) {
20
47
  this._identity = identity;
21
48
  this._config = config;
22
- this._signer = identityToEvmWallet(identity);
23
- this._escrowClient = new BaseEscrowClient({
49
+ this._signer = identity.wallet;
50
+ this._depositsClient = new DepositsClient({
24
51
  rpcUrl: config.rpcUrl,
25
- contractAddress: config.contractAddress,
52
+ contractAddress: config.depositsContractAddress,
26
53
  usdcAddress: config.usdcAddress,
27
54
  });
55
+ this._channelStore = channelStore;
56
+ this._channelsDomain = makeChannelsDomain(config.chainId, config.channelsContractAddress);
57
+ // Hydrate cumulative maps from persisted active sessions
58
+ this._hydrateFromStore();
28
59
  }
29
- get signer() {
30
- return this._signer;
60
+ /** Hydrate cumulative tracking maps from persisted active buyer sessions. */
61
+ _hydrateFromStore() {
62
+ const activeChannels = this._channelStore.getActiveChannelsByBuyer('buyer', this._identity.wallet.address);
63
+ for (const channel of activeChannels) {
64
+ const peerId = channel.peerId;
65
+ this._cumulativeAmount.set(peerId, BigInt(channel.authMax));
66
+ this._metadata.set(peerId, {
67
+ cumulativeInputTokens: BigInt(channel.tokensDelivered),
68
+ cumulativeOutputTokens: BigInt(channel.previousConsumption),
69
+ cumulativeLatencyMs: 0n,
70
+ cumulativeRequestCount: BigInt(channel.requestCount),
71
+ });
72
+ // verifiedCost and pricing are not persisted — start from 0 on hydration.
73
+ // This is conservative: the buyer treats all previously-signed amounts as unverified.
74
+ }
31
75
  }
32
- /** @deprecated Use .signer instead */
33
- get wallet() {
76
+ get signer() {
34
77
  return this._signer;
35
78
  }
36
- /** Replace the signer at runtime (e.g. with a WalletConnect signer). */
37
79
  setSigner(signer) {
38
80
  this._signer = signer;
39
81
  }
40
- get escrowClient() {
41
- return this._escrowClient;
82
+ get depositsClient() {
83
+ return this._depositsClient;
84
+ }
85
+ get _costTolerance() {
86
+ return this._config.costToleranceMultiplier ?? DEFAULT_COST_TOLERANCE;
87
+ }
88
+ _getCeiling(sellerPeerId) {
89
+ return this._currentReserveCeiling.get(sellerPeerId) ?? this._config.maxReserveAmountUsdc;
90
+ }
91
+ /** Clean up all in-memory state for a seller when the session ends. */
92
+ cleanupSession(sellerPeerId) {
93
+ this._cumulativeAmount.delete(sellerPeerId);
94
+ this._metadata.delete(sellerPeerId);
95
+ this._verifiedCost.delete(sellerPeerId);
96
+ this._sessionPricing.delete(sellerPeerId);
97
+ this._currentReserveCeiling.delete(sellerPeerId);
98
+ this._reserveSalt.delete(sellerPeerId);
99
+ this._confirmedPeers.delete(sellerPeerId);
100
+ this._rejectedPeers.delete(sellerPeerId);
101
+ this._responseTokenTotals.delete(sellerPeerId);
102
+ }
103
+ getActiveSession(sellerPeerId) {
104
+ return this._channelStore.getActiveChannelByPeerAndBuyer(sellerPeerId, 'buyer', this._identity.wallet.address);
105
+ }
106
+ retireSession(sellerPeerId, status, settledAmount) {
107
+ const session = this.getActiveSession(sellerPeerId);
108
+ if (session) {
109
+ this._channelStore.updateChannelStatus(session.sessionId, status, settledAmount !== undefined ? settledAmount.toString() : undefined);
110
+ }
111
+ this.cleanupSession(sellerPeerId);
112
+ }
113
+ canReplayReserveAuth(sellerPeerId) {
114
+ return this._reserveSalt.has(sellerPeerId);
42
115
  }
43
- /** Get a snapshot of all active sessions. */
44
- getActiveSessions() {
45
- return [...this._sessions.values()].filter((s) => s.status !== 'ended');
116
+ async resendCurrentSpendingAuth(sellerPeerId, paymentMux) {
117
+ const session = this.getActiveSession(sellerPeerId);
118
+ if (!session) {
119
+ throw new Error(`[BuyerPayment] No active session for seller ${sellerPeerId.slice(0, 12)}...`);
120
+ }
121
+ const cumulativeAmount = this._cumulativeAmount.get(sellerPeerId) ?? BigInt(session.authMax);
122
+ const currentMeta = this._metadata.get(sellerPeerId) ?? ZERO_METADATA;
123
+ const metadataHashHex = computeMetadataHash(currentMeta);
124
+ const encodedMetadata = encodeMetadata(currentMeta);
125
+ const metadataMsg = {
126
+ channelId: session.sessionId,
127
+ cumulativeAmount,
128
+ metadataHash: metadataHashHex,
129
+ };
130
+ const spendingAuthSig = await signSpendingAuth(this._signer, this._channelsDomain, metadataMsg);
131
+ paymentMux.sendSpendingAuth({
132
+ channelId: session.sessionId,
133
+ cumulativeAmount: cumulativeAmount.toString(),
134
+ metadataHash: metadataHashHex,
135
+ metadata: encodedMetadata,
136
+ spendingAuthSig,
137
+ });
138
+ return session.sessionId;
46
139
  }
47
- /** Get the session for a given seller peer, if it exists. */
48
- getSession(sellerPeerId) {
49
- return this._sessions.get(sellerPeerId);
140
+ async resendReserveAuth(sellerPeerId, paymentMux) {
141
+ const session = this.getActiveSession(sellerPeerId);
142
+ const salt = this._reserveSalt.get(sellerPeerId);
143
+ if (!session || !salt) {
144
+ throw new Error(`[BuyerPayment] No replayable reserve for seller ${sellerPeerId.slice(0, 12)}...`);
145
+ }
146
+ // Force a fresh AuthAck after replaying the reserve path.
147
+ this._confirmedPeers.delete(sellerPeerId);
148
+ const maxAmount = this._currentReserveCeiling.get(sellerPeerId) ?? this._config.maxReserveAmountUsdc;
149
+ const deadline = Math.floor(Date.now() / 1000) + this._config.defaultAuthDurationSecs;
150
+ const reserveMsg = {
151
+ channelId: session.sessionId,
152
+ maxAmount,
153
+ deadline: BigInt(deadline),
154
+ };
155
+ const reserveAuthSig = await signReserveAuth(this._signer, this._channelsDomain, reserveMsg);
156
+ paymentMux.sendSpendingAuth({
157
+ channelId: session.sessionId,
158
+ cumulativeAmount: session.authMax,
159
+ metadataHash: ZERO_METADATA_HASH,
160
+ metadata: encodeMetadata(this._metadata.get(sellerPeerId) ?? ZERO_METADATA),
161
+ spendingAuthSig: reserveAuthSig,
162
+ reserveSalt: salt,
163
+ reserveMaxAmount: maxAmount.toString(),
164
+ reserveDeadline: deadline,
165
+ });
166
+ return session.sessionId;
50
167
  }
51
- // ── Lock initiation ─────────────────────────────────────────────
168
+ // ── Spending Authorization ────────────────────────────────────
52
169
  /**
53
- * Generate a session ID, sign a lock authorization, and send it
54
- * to the seller via PaymentMux.
170
+ * Sign and send an initial EIP-712 SpendingAuth to a seller.
171
+ * The initial cumulativeAmount is set to the seller's minBudgetPerRequest.
172
+ *
173
+ * @param pricing Token pricing from the seller's 402 / peer metadata.
55
174
  */
56
- async initiateLock(sellerPeerId, sellerEvmAddress, paymentMux, lockAmount) {
57
- const amount = lockAmount ?? this._config.defaultLockAmountUSDC;
58
- const amountBigInt = BigInt(amount);
59
- // Generate a 32-byte session ID as 0x-prefixed hex (bytes32)
60
- const sessionIdBytes = randomBytes(32);
61
- const sessionId = '0x' + sessionIdBytes.toString('hex');
62
- debugLog(`[BuyerPayment] Initiating lock: session=${sessionId.slice(0, 18)}... seller=${sellerPeerId.slice(0, 12)}... amount=${amount}`);
63
- // Sign the lock message with ECDSA (for on-chain verification)
64
- const messageHash = buildLockMessageHash(sessionId, sellerEvmAddress, amountBigInt);
65
- const buyerSig = await signMessageEcdsa(this._signer, messageHash);
66
- // Store session state
175
+ async authorizeSpending(sellerPeerId, paymentMux, minBudgetPerRequest, reserveAmountOrPricing, pricingArg) {
176
+ const sellerEvmAddr = peerIdToAddress(sellerPeerId);
177
+ const reserveAmount = typeof reserveAmountOrPricing === 'bigint'
178
+ ? reserveAmountOrPricing
179
+ : this._config.maxReserveAmountUsdc;
180
+ const pricing = typeof reserveAmountOrPricing === 'bigint'
181
+ ? pricingArg
182
+ : reserveAmountOrPricing;
183
+ // Budget validation: reject if seller demands more than buyer's overdraft limit
184
+ if (minBudgetPerRequest > this._config.maxPerRequestUsdc) {
185
+ debugWarn(`[BuyerPayment] Seller ${sellerPeerId.slice(0, 12)}... minBudgetPerRequest=${minBudgetPerRequest} exceeds maxPerRequestUsdc=${this._config.maxPerRequestUsdc} — not authorizing`);
186
+ return '';
187
+ }
188
+ // Clear confirmation state so we wait for a fresh AuthAck on the new session
189
+ this._confirmedPeers.delete(sellerPeerId);
190
+ // Store pricing for this session
191
+ if (pricing) {
192
+ this._sessionPricing.set(sellerPeerId, pricing);
193
+ }
194
+ // Generate random salt and compute deterministic channelId
195
+ const salt = '0x' + randomBytes(32).toString('hex');
196
+ const buyerEvmAddr = this._identity.wallet.address;
197
+ const channelId = computeChannelId(buyerEvmAddr, sellerEvmAddr, salt);
198
+ const deadline = Math.floor(Date.now() / 1000) + this._config.defaultAuthDurationSecs;
199
+ debugLog(`[BuyerPayment] authorizeSpending: channel=${channelId.slice(0, 18)}... seller=${sellerPeerId.slice(0, 12)}... amount=${minBudgetPerRequest}`);
200
+ // Sign ReserveAuth — binds channelId, maxAmount, deadline on-chain
201
+ const channelsDomain = this._channelsDomain;
202
+ const maxAmount = reserveAmount;
203
+ const reserveMsg = {
204
+ channelId,
205
+ maxAmount,
206
+ deadline: BigInt(deadline),
207
+ };
208
+ const reserveAuthSig = await signReserveAuth(this._signer, channelsDomain, reserveMsg);
209
+ // Initialize state for this session
210
+ this._cumulativeAmount.set(sellerPeerId, minBudgetPerRequest);
211
+ this._metadata.set(sellerPeerId, { ...ZERO_METADATA });
212
+ this._verifiedCost.set(sellerPeerId, 0n);
213
+ this._currentReserveCeiling.set(sellerPeerId, maxAmount);
214
+ this._reserveSalt.set(sellerPeerId, salt);
215
+ // Store session
67
216
  const now = Date.now();
68
217
  const session = {
69
- sessionId,
70
- sellerPeerId,
71
- sellerEvmAddress,
72
- lockedAmount: amountBigInt,
73
- status: 'pending',
74
- txSignature: null,
75
- lastRunningTotal: 0n,
76
- lastRequestCount: 0,
218
+ sessionId: channelId,
219
+ peerId: sellerPeerId,
220
+ role: 'buyer',
221
+ sellerEvmAddr: peerIdToAddress(sellerPeerId),
222
+ buyerEvmAddr: this._identity.wallet.address,
223
+ nonce: 0,
224
+ authMax: minBudgetPerRequest.toString(),
225
+ deadline,
226
+ previousSessionId: '0x' + '0'.repeat(64),
227
+ previousConsumption: '0',
228
+ tokensDelivered: '0',
229
+ requestCount: 0,
230
+ reservedAt: now,
231
+ settledAt: null,
232
+ settledAmount: null,
233
+ status: 'active',
234
+ latestBuyerSig: null,
235
+ latestSpendingAuthSig: null,
236
+ latestMetadata: null,
77
237
  createdAt: now,
78
238
  updatedAt: now,
79
239
  };
80
- this._sessions.set(sellerPeerId, session);
81
- // Send the lock auth message
82
- paymentMux.sendSessionLockAuth({
83
- sessionId,
84
- lockedAmount: amount,
85
- buyerSig,
240
+ this._channelStore.upsertChannel(session);
241
+ // Send SpendingAuth via PaymentMux — reserve carries ReserveAuth sig
242
+ paymentMux.sendSpendingAuth({
243
+ channelId,
244
+ cumulativeAmount: minBudgetPerRequest.toString(),
245
+ metadataHash: ZERO_METADATA_HASH,
246
+ metadata: encodeMetadata(ZERO_METADATA),
247
+ spendingAuthSig: reserveAuthSig,
248
+ reserveSalt: salt,
249
+ reserveMaxAmount: maxAmount.toString(),
250
+ reserveDeadline: deadline,
86
251
  });
87
- return sessionId;
252
+ return channelId;
88
253
  }
89
- // ── Lock confirmation / rejection handlers ──────────────────────
90
- /**
91
- * Called when the seller confirms the lock was committed on-chain.
92
- */
93
- handleLockConfirm(sellerPeerId, payload) {
94
- const session = this._sessions.get(sellerPeerId);
254
+ // ── AuthAck handler ───────────────────────────────────────────
255
+ handleAuthAck(sellerPeerId, payload) {
256
+ const session = this.getActiveSession(sellerPeerId);
95
257
  if (!session) {
96
- debugWarn(`[BuyerPayment] Lock confirm for unknown seller: ${sellerPeerId.slice(0, 12)}...`);
258
+ debugWarn(`[BuyerPayment] AuthAck for unknown seller: ${sellerPeerId.slice(0, 12)}...`);
97
259
  return;
98
260
  }
99
- if (session.sessionId !== payload.sessionId) {
100
- debugWarn(`[BuyerPayment] Lock confirm session mismatch: expected=${session.sessionId.slice(0, 18)}... got=${payload.sessionId.slice(0, 18)}...`);
261
+ if (session.sessionId !== payload.channelId) {
262
+ debugWarn(`[BuyerPayment] AuthAck channel mismatch: expected=${session.sessionId.slice(0, 18)}... got=${payload.channelId.slice(0, 18)}...`);
101
263
  return;
102
264
  }
103
- session.status = 'confirmed';
104
- session.txSignature = payload.txSignature;
105
- session.updatedAt = Date.now();
106
- debugLog(`[BuyerPayment] Lock confirmed: session=${session.sessionId.slice(0, 18)}... tx=${payload.txSignature.slice(0, 12)}...`);
265
+ this._confirmedPeers.add(sellerPeerId);
266
+ debugLog(`[BuyerPayment] AuthAck confirmed: channel=${session.sessionId.slice(0, 18)}...`);
107
267
  }
268
+ // ── Buyer-side cost verification ──────────────────────────────
108
269
  /**
109
- * Called when the seller rejects the lock.
270
+ * Estimate tokens and cost from response content without updating state.
110
271
  */
111
- handleLockReject(sellerPeerId, payload) {
112
- const session = this._sessions.get(sellerPeerId);
113
- if (!session) {
114
- debugWarn(`[BuyerPayment] Lock reject for unknown seller: ${sellerPeerId.slice(0, 12)}...`);
115
- return;
116
- }
117
- debugWarn(`[BuyerPayment] Lock rejected: session=${session.sessionId.slice(0, 18)}... reason=${payload.reason}`);
118
- this._sessions.delete(sellerPeerId);
272
+ _estimateResponseCost(sellerPeerId, inputBytes, outputBytes) {
273
+ const pricing = this._sessionPricing.get(sellerPeerId);
274
+ if (!pricing)
275
+ return null;
276
+ return estimateCostFromBytes(inputBytes, outputBytes, pricing);
277
+ }
278
+ /**
279
+ * Accumulate a cost estimate into verifiedCost.
280
+ */
281
+ _accumulateVerifiedCost(sellerPeerId, estimate) {
282
+ const prev = this._verifiedCost.get(sellerPeerId) ?? 0n;
283
+ const newVerified = prev + estimate.cost;
284
+ this._verifiedCost.set(sellerPeerId, newVerified);
285
+ return newVerified;
286
+ }
287
+ /**
288
+ * Record response content and update the buyer's verified cost.
289
+ * Call this after receiving each response from the seller.
290
+ *
291
+ * NOTE: Do not call this AND signPerRequestAuth for the same response —
292
+ * signPerRequestAuth already updates verifiedCost internally.
293
+ *
294
+ * @returns The updated verified cost and estimated tokens, or null if no pricing is available.
295
+ */
296
+ recordResponseBytes(sellerPeerId, inputBytes, outputBytes) {
297
+ const estimate = this._estimateResponseCost(sellerPeerId, inputBytes, outputBytes);
298
+ if (!estimate)
299
+ return null;
300
+ const newVerified = this._accumulateVerifiedCost(sellerPeerId, estimate);
301
+ const inSize = inputBytes.length;
302
+ const outSize = outputBytes.length;
303
+ debugLog(`[BuyerPayment] recordResponseBytes: seller=${sellerPeerId.slice(0, 12)}... ` +
304
+ `in=${inSize}B→${estimate.inputTokens}tok out=${outSize}B→${estimate.outputTokens}tok ` +
305
+ `requestCost=${estimate.cost} verifiedCost=${newVerified}`);
306
+ return { verifiedCost: newVerified, inputTokens: estimate.inputTokens, outputTokens: estimate.outputTokens };
307
+ }
308
+ // ── Per-request authorization (overdraft model) ─────────────
309
+ /**
310
+ * Compute the max signable cumulative amount based on the overdraft model:
311
+ * maxSignable = verifiedCost + maxPerRequestUsdc, capped at reserve ceiling.
312
+ */
313
+ _maxSignable(sellerPeerId) {
314
+ const verified = this._verifiedCost.get(sellerPeerId) ?? 0n;
315
+ const ceiling = this._getCeiling(sellerPeerId);
316
+ const maxSignable = verified + this._config.maxPerRequestUsdc;
317
+ return maxSignable < ceiling ? maxSignable : ceiling;
119
318
  }
120
- // ── Receipt handling ────────────────────────────────────────────
121
319
  /**
122
- * Handle a running-total receipt from the seller.
123
- * If autoAck is enabled, automatically counter-sign and send BuyerAck.
320
+ * Check whether the current cumulative amount is approaching the reserve ceiling
321
+ * and a top-up should be triggered.
124
322
  */
125
- async handleSellerReceipt(sellerPeerId, receipt, paymentMux) {
126
- const session = this._sessions.get(sellerPeerId);
323
+ _needsTopUp(sellerPeerId) {
324
+ const ceiling = this._getCeiling(sellerPeerId);
325
+ const current = this._cumulativeAmount.get(sellerPeerId) ?? 0n;
326
+ const threshold = BigInt(Math.floor(Number(ceiling) * DEFAULT_TOPUP_THRESHOLD));
327
+ return current >= threshold;
328
+ }
329
+ /**
330
+ * Sign an updated SpendingAuth after receiving a response.
331
+ *
332
+ * The buyer uses the seller's claimed cost to advance the cumulative amount,
333
+ * but validates it against the buyer's bytes/4 estimate. If the seller's claim
334
+ * exceeds the buyer's estimate by more than the configured tolerance, the buyer
335
+ * caps at tolerance * buyerEstimate. The cumulative is also capped at the
336
+ * overdraft limit (verifiedCost + maxPerRequestUsdc) and the reserve ceiling.
337
+ *
338
+ * @param sellerPeerId Seller peer ID.
339
+ * @param responseStats Byte counts from the last response and seller's claimed cost.
340
+ * @param addedLatencyMs Optional latency for metadata.
341
+ * @returns The signed payload and whether a reserve top-up is needed.
342
+ */
343
+ async signPerRequestAuth(sellerPeerId, responseStats, addedLatencyMs) {
344
+ const session = this.getActiveSession(sellerPeerId);
127
345
  if (!session) {
128
- debugWarn(`[BuyerPayment] Receipt for unknown seller: ${sellerPeerId.slice(0, 12)}...`);
129
- return;
346
+ throw new Error(`[BuyerPayment] No active session for seller ${sellerPeerId.slice(0, 12)}... — call authorizeSpending() first`);
130
347
  }
131
- if (session.status === 'confirmed') {
132
- session.status = 'active';
348
+ // Estimate cost from response bytes (buyer's independent estimate) and accumulate
349
+ const estimate = this._estimateResponseCost(sellerPeerId, responseStats.inputBytes, responseStats.outputBytes);
350
+ const estimatedInputTokens = estimate ? BigInt(estimate.inputTokens) : 0n;
351
+ const estimatedOutputTokens = estimate ? BigInt(estimate.outputTokens) : 0n;
352
+ const buyerEstimatedRequestCost = estimate ? estimate.cost : 0n;
353
+ if (estimate) {
354
+ this._accumulateVerifiedCost(sellerPeerId, estimate);
133
355
  }
134
- // Update running total
135
- session.lastRunningTotal = BigInt(receipt.runningTotal);
136
- session.lastRequestCount = receipt.requestCount;
137
- session.updatedAt = Date.now();
138
- debugLog(`[BuyerPayment] Receipt: session=${session.sessionId.slice(0, 18)}... total=${receipt.runningTotal} count=${receipt.requestCount}`);
139
- const autoAck = this._config.autoAck ?? true;
140
- if (autoAck) {
141
- // Build ack message and sign with Ed25519
142
- const sessionIdBytes = hexToBytes(session.sessionId.startsWith('0x') ? session.sessionId.slice(2) : session.sessionId);
143
- const ackMsg = buildAckMessage(sessionIdBytes, BigInt(receipt.runningTotal), receipt.requestCount);
144
- const sigBytes = await signMessageEd25519(this._identity, ackMsg);
145
- const buyerSig = bytesToHex(sigBytes);
146
- paymentMux.sendBuyerAck({
147
- sessionId: session.sessionId,
148
- runningTotal: receipt.runningTotal,
149
- requestCount: receipt.requestCount,
150
- buyerSig,
151
- });
152
- debugLog(`[BuyerPayment] Auto-ack sent for session=${session.sessionId.slice(0, 18)}...`);
356
+ // Determine the accepted cost for this request:
357
+ // Use seller's claim, but cap at tolerance * buyer estimate if buyer has pricing.
358
+ let acceptedCost = responseStats.sellerClaimedCost ?? buyerEstimatedRequestCost;
359
+ if (responseStats.sellerClaimedCost != null && buyerEstimatedRequestCost > 0n) {
360
+ const maxAcceptable = BigInt(Math.ceil(Number(buyerEstimatedRequestCost) * this._costTolerance));
361
+ if (responseStats.sellerClaimedCost > maxAcceptable) {
362
+ debugWarn(`[BuyerPayment] Seller claimed ${responseStats.sellerClaimedCost} exceeds ${this._costTolerance}x buyer estimate ${buyerEstimatedRequestCost} — capping at ${maxAcceptable}`);
363
+ acceptedCost = maxAcceptable;
364
+ }
153
365
  }
366
+ // Minimum 1 base unit for monotonicity
367
+ if (acceptedCost === 0n)
368
+ acceptedCost = 1n;
369
+ // Update cumulative metadata
370
+ const prev = this._metadata.get(sellerPeerId) ?? ZERO_METADATA;
371
+ const newMeta = {
372
+ cumulativeInputTokens: prev.cumulativeInputTokens + estimatedInputTokens,
373
+ cumulativeOutputTokens: prev.cumulativeOutputTokens + estimatedOutputTokens,
374
+ cumulativeLatencyMs: prev.cumulativeLatencyMs + (addedLatencyMs ?? 0n),
375
+ cumulativeRequestCount: prev.cumulativeRequestCount + 1n,
376
+ };
377
+ this._metadata.set(sellerPeerId, newMeta);
378
+ // Advance cumulative amount by the accepted cost, then add overdraft headroom
379
+ // for the next request (so the seller has budget to serve it).
380
+ // maxSignable already caps at reserve ceiling, so one cap is sufficient
381
+ const prevAmount = this._cumulativeAmount.get(sellerPeerId) ?? 0n;
382
+ const maxSignable = this._maxSignable(sellerPeerId);
383
+ let newAmount = prevAmount + acceptedCost;
384
+ if (newAmount > maxSignable)
385
+ newAmount = maxSignable;
386
+ // Ensure monotonic increase (at least +1 per request)
387
+ if (newAmount <= prevAmount)
388
+ newAmount = prevAmount + 1n;
389
+ if (newAmount > maxSignable)
390
+ newAmount = maxSignable;
391
+ this._cumulativeAmount.set(sellerPeerId, newAmount);
392
+ // Compute metadata hash and encode metadata
393
+ const metadataHashHex = computeMetadataHash(newMeta);
394
+ const encodedMetadata = encodeMetadata(newMeta);
395
+ // Sign EIP-712 SpendingAuth
396
+ const channelsDomain = this._channelsDomain;
397
+ const metadataMsg = {
398
+ channelId: session.sessionId,
399
+ cumulativeAmount: newAmount,
400
+ metadataHash: metadataHashHex,
401
+ };
402
+ const spendingAuthSig = await signSpendingAuth(this._signer, channelsDomain, metadataMsg);
403
+ // Persist updated cumulative values to ChannelStore
404
+ this._channelStore.upsertChannel({
405
+ ...session,
406
+ authMax: newAmount.toString(),
407
+ requestCount: Number(newMeta.cumulativeRequestCount),
408
+ updatedAt: Date.now(),
409
+ });
410
+ const payload = {
411
+ channelId: session.sessionId,
412
+ cumulativeAmount: newAmount.toString(),
413
+ metadataHash: metadataHashHex,
414
+ metadata: encodedMetadata,
415
+ spendingAuthSig,
416
+ };
417
+ const topUpNeeded = this._needsTopUp(sellerPeerId);
418
+ return { payload, topUpNeeded };
154
419
  }
155
- // ── Top-up handling ─────────────────────────────────────────────
420
+ // ── NeedAuth handler ───────────────────────────────────────────
156
421
  /**
157
- * Handle a top-up request from the seller.
158
- * If autoTopUp is enabled and budget allows, sign and send TopUpAuth.
159
- * Otherwise, end the session.
422
+ * Handle seller-initiated NeedAuth messages when the seller's budget runs out mid-session.
423
+ * Caps the signed amount at verifiedCost + maxPerRequestUsdc (overdraft model).
160
424
  */
161
- async handleTopUpRequest(sellerPeerId, request, paymentMux) {
162
- const session = this._sessions.get(sellerPeerId);
425
+ async handleNeedAuth(sellerPeerId, payload, paymentMux) {
426
+ const session = this.getActiveSession(sellerPeerId);
163
427
  if (!session) {
164
- debugWarn(`[BuyerPayment] Top-up for unknown seller: ${sellerPeerId.slice(0, 12)}...`);
428
+ debugWarn(`[BuyerPayment] NeedAuth for unknown seller: ${sellerPeerId.slice(0, 12)}...`);
429
+ return;
430
+ }
431
+ const requiredCumulativeAmount = BigInt(payload.requiredCumulativeAmount);
432
+ const currentCumulative = this._cumulativeAmount.get(sellerPeerId) ?? 0n;
433
+ // Reject stale/lower NeedAuth (monotonicity guard)
434
+ if (requiredCumulativeAmount <= currentCumulative) {
435
+ debugLog(`[BuyerPayment] NeedAuth stale: required=${requiredCumulativeAmount} <= current=${currentCumulative} — ignoring`);
165
436
  return;
166
437
  }
167
- const additionalAmount = BigInt(request.additionalAmount);
168
- const maxBudget = BigInt(this._config.maxSessionBudgetUSDC ?? '10000000');
169
- const newTotal = session.lockedAmount + additionalAmount;
170
- const autoTopUp = this._config.autoTopUp ?? true;
171
- debugLog(`[BuyerPayment] Top-up request: session=${session.sessionId.slice(0, 18)}... additional=${request.additionalAmount} newTotal=${newTotal}`);
172
- if (autoTopUp && newTotal <= maxBudget) {
173
- // Check on-chain balance
174
- const buyerAddr = identityToEvmAddress(this._identity);
175
- const account = await this._escrowClient.getBuyerAccount(buyerAddr);
176
- if (account.available >= additionalAmount) {
177
- // Sign extend-lock authorization
178
- const messageHash = buildExtendLockMessageHash(session.sessionId, session.sellerEvmAddress, additionalAmount);
179
- const buyerSig = await signMessageEcdsa(this._signer, messageHash);
180
- session.lockedAmount = newTotal;
181
- session.updatedAt = Date.now();
182
- paymentMux.sendTopUpAuth({
183
- sessionId: session.sessionId,
184
- additionalAmount: request.additionalAmount,
185
- buyerSig,
186
- });
187
- debugLog(`[BuyerPayment] Top-up authorized: session=${session.sessionId.slice(0, 18)}...`);
188
- return;
438
+ // Cap at overdraft limit: verifiedCost + maxPerRequestUsdc
439
+ let maxSignable = this._maxSignable(sellerPeerId);
440
+ const reserveCeiling = this._getCeiling(sellerPeerId);
441
+ if (requiredCumulativeAmount > maxSignable && maxSignable >= reserveCeiling) {
442
+ try {
443
+ await this.topUpReserve(sellerPeerId, paymentMux);
444
+ }
445
+ catch (err) {
446
+ debugWarn(`[BuyerPayment] NeedAuth: topUpReserve failed: ${err instanceof Error ? err.message : err}`);
189
447
  }
190
- debugWarn(`[BuyerPayment] Insufficient balance for top-up. Available=${account.available}, requested=${additionalAmount}`);
448
+ maxSignable = this._maxSignable(sellerPeerId);
449
+ }
450
+ if (maxSignable <= currentCumulative) {
451
+ debugWarn(`[BuyerPayment] NeedAuth: maxSignable=${maxSignable} <= currentCumulative=${currentCumulative} — cannot authorize more (overdraft limit reached)`);
452
+ return;
453
+ }
454
+ // Sign up to the lesser of what the seller asks and what we allow
455
+ const effectiveAmount = requiredCumulativeAmount < maxSignable ? requiredCumulativeAmount : maxSignable;
456
+ debugLog(`[BuyerPayment] NeedAuth: channel=${session.sessionId.slice(0, 18)}... required=${requiredCumulativeAmount} effective=${effectiveAmount}`);
457
+ // Update cumulative amount
458
+ this._cumulativeAmount.set(sellerPeerId, effectiveAmount);
459
+ // Sign SpendingAuth with the effective amount and current metadata
460
+ const currentMeta = this._metadata.get(sellerPeerId) ?? ZERO_METADATA;
461
+ const metadataHashHex = computeMetadataHash(currentMeta);
462
+ const encodedMetadata = encodeMetadata(currentMeta);
463
+ const channelsDomain = this._channelsDomain;
464
+ const metadataMsg = {
465
+ channelId: session.sessionId,
466
+ cumulativeAmount: effectiveAmount,
467
+ metadataHash: metadataHashHex,
468
+ };
469
+ const spendingAuthSig = await signSpendingAuth(this._signer, channelsDomain, metadataMsg);
470
+ // Persist updated values
471
+ this._channelStore.upsertChannel({
472
+ ...session,
473
+ authMax: effectiveAmount.toString(),
474
+ updatedAt: Date.now(),
475
+ });
476
+ // Send via PaymentMux
477
+ try {
478
+ paymentMux.sendSpendingAuth({
479
+ channelId: session.sessionId,
480
+ cumulativeAmount: effectiveAmount.toString(),
481
+ metadataHash: metadataHashHex,
482
+ metadata: encodedMetadata,
483
+ spendingAuthSig,
484
+ });
485
+ debugLog(`[BuyerPayment] NeedAuth responded: new cumulativeAmount=${effectiveAmount}`);
486
+ }
487
+ catch {
488
+ debugLog(`[BuyerPayment] NeedAuth: connection closed before SpendingAuth could be sent`);
191
489
  }
192
- // Cannot or will not top up — end the session
193
- debugLog(`[BuyerPayment] Declining top-up, ending session=${session.sessionId.slice(0, 18)}...`);
194
- await this.endSession(sellerPeerId, paymentMux, 80);
195
490
  }
196
- // ── Session end ─────────────────────────────────────────────────
491
+ // ── Reserve top-up ─────────────────────────────────────────────
197
492
  /**
198
- * End a session with the given seller. Signs a settlement message
199
- * with ECDSA and sends SessionEnd.
493
+ * Sign a new ReserveAuth with a higher maxAmount to extend the session's reserve ceiling.
494
+ * The seller must call reserve() on-chain again with the new signature.
495
+ * Note: requires contract support for top-up (increaseDeposit on existing channelId).
200
496
  */
201
- async endSession(sellerPeerId, paymentMux, score = 80) {
202
- const session = this._sessions.get(sellerPeerId);
497
+ async topUpReserve(sellerPeerId, paymentMux) {
498
+ const session = this.getActiveSession(sellerPeerId);
203
499
  if (!session) {
204
- debugWarn(`[BuyerPayment] Cannot end session for unknown seller: ${sellerPeerId.slice(0, 12)}...`);
500
+ debugWarn(`[BuyerPayment] topUpReserve: no active session for ${sellerPeerId.slice(0, 12)}...`);
205
501
  return;
206
502
  }
207
- if (session.status === 'ending' || session.status === 'ended') {
208
- return;
503
+ const prevCeiling = this._getCeiling(sellerPeerId);
504
+ const newCeiling = prevCeiling + this._config.maxReserveAmountUsdc;
505
+ const deadline = Math.floor(Date.now() / 1000) + this._config.defaultAuthDurationSecs;
506
+ debugLog(`[BuyerPayment] topUpReserve: channel=${session.sessionId.slice(0, 18)}... ceiling ${prevCeiling} → ${newCeiling}`);
507
+ // Sign ReserveAuth with new maxAmount
508
+ const channelsDomain = this._channelsDomain;
509
+ const reserveMsg = {
510
+ channelId: session.sessionId,
511
+ maxAmount: newCeiling,
512
+ deadline: BigInt(deadline),
513
+ };
514
+ const reserveAuthSig = await signReserveAuth(this._signer, channelsDomain, reserveMsg);
515
+ const currentCumulative = this._cumulativeAmount.get(sellerPeerId) ?? 0n;
516
+ const currentMeta = this._metadata.get(sellerPeerId) ?? ZERO_METADATA;
517
+ const metadataHashHex = computeMetadataHash(currentMeta);
518
+ const encodedMetadata = encodeMetadata(currentMeta);
519
+ const salt = this._reserveSalt.get(sellerPeerId) ?? '0x' + '00'.repeat(32);
520
+ // Send ReserveAuth sig with reserve fields (same pattern as initial authorizeSpending).
521
+ // The seller uses this to call topUp() on-chain with the new maxAmount.
522
+ try {
523
+ paymentMux.sendSpendingAuth({
524
+ channelId: session.sessionId,
525
+ cumulativeAmount: currentCumulative.toString(),
526
+ metadataHash: metadataHashHex,
527
+ metadata: encodedMetadata,
528
+ spendingAuthSig: reserveAuthSig,
529
+ reserveSalt: salt,
530
+ reserveMaxAmount: newCeiling.toString(),
531
+ reserveDeadline: deadline,
532
+ });
533
+ // Only commit the new ceiling after the message is delivered
534
+ this._currentReserveCeiling.set(sellerPeerId, newCeiling);
535
+ debugLog(`[BuyerPayment] topUpReserve sent: newCeiling=${newCeiling}`);
536
+ }
537
+ catch {
538
+ debugLog(`[BuyerPayment] topUpReserve: connection closed before ReserveAuth could be sent`);
209
539
  }
210
- session.status = 'ending';
211
- session.updatedAt = Date.now();
212
- debugLog(`[BuyerPayment] Ending session=${session.sessionId.slice(0, 18)}... total=${session.lastRunningTotal} score=${score}`);
213
- // Sign settlement message with ECDSA
214
- const messageHash = buildSettlementMessageHash(session.sessionId, session.lastRunningTotal, score);
215
- const buyerSig = await signMessageEcdsa(this._signer, messageHash);
216
- paymentMux.sendSessionEnd({
217
- sessionId: session.sessionId,
218
- runningTotal: session.lastRunningTotal.toString(),
219
- requestCount: session.lastRequestCount,
220
- score,
221
- buyerSig,
222
- });
223
- session.status = 'ended';
224
- session.updatedAt = Date.now();
225
- debugLog(`[BuyerPayment] Session ended: ${session.sessionId.slice(0, 18)}...`);
226
540
  }
227
- // ── Escrow operations ───────────────────────────────────────────
228
- /**
229
- * Deposit USDC into the escrow contract.
230
- * @param amount Amount in USDC base units (6 decimals).
231
- */
232
- async deposit(amount) {
233
- debugLog(`[BuyerPayment] Depositing ${amount} to escrow`);
234
- return this._escrowClient.deposit(this._signer, amount);
541
+ // ── Queries ───────────────────────────────────────────────────
542
+ /** Max USDC overdraft (unverified exposure) from buyer config. */
543
+ get maxPerRequestUsdc() {
544
+ return this._config.maxPerRequestUsdc;
235
545
  }
236
- /**
237
- * Withdraw USDC from the escrow contract.
238
- * @param amount Amount in USDC base units (6 decimals).
239
- */
240
- async withdraw(amount) {
241
- debugLog(`[BuyerPayment] Withdrawing ${amount} from escrow`);
242
- return this._escrowClient.withdraw(this._signer, amount);
546
+ /** Max USDC per ReserveAuth signature from buyer config. */
547
+ get maxReserveAmountUsdc() {
548
+ return this._config.maxReserveAmountUsdc;
243
549
  }
244
- /**
245
- * Get the buyer's on-chain escrow balance.
246
- */
247
- async getBalance() {
248
- const buyerAddr = identityToEvmAddress(this._identity);
249
- return this._escrowClient.getBuyerAccount(buyerAddr);
550
+ /** Current buyer-verified cost for a seller. */
551
+ getVerifiedCost(sellerPeerId) {
552
+ return this._verifiedCost.get(sellerPeerId) ?? 0n;
250
553
  }
251
- // ── Dispute helpers ─────────────────────────────────────────────
252
- /**
253
- * Release an expired lock (buyer reclaims funds).
254
- */
255
- async releaseExpiredLock(sessionId) {
256
- debugLog(`[BuyerPayment] Releasing expired lock: session=${sessionId.slice(0, 18)}...`);
257
- return this._escrowClient.releaseExpiredLock(this._signer, sessionId);
554
+ /** Current reserve ceiling for a seller (may be higher than initial after top-ups). */
555
+ getReserveCeiling(sellerPeerId) {
556
+ return this._currentReserveCeiling.get(sellerPeerId) ?? this._config.maxReserveAmountUsdc;
258
557
  }
259
- /**
260
- * Respond to a dispute opened by the seller.
261
- */
262
- async respondToDispute(sessionId) {
263
- debugLog(`[BuyerPayment] Responding to dispute: session=${sessionId.slice(0, 18)}...`);
264
- return this._escrowClient.respondDispute(this._signer, sessionId);
558
+ /** Current cumulative signed amount for a seller. */
559
+ getCumulativeAmount(sellerPeerId) {
560
+ return this._cumulativeAmount.get(sellerPeerId) ?? 0n;
561
+ }
562
+ /** Live cumulative token counts for a seller (in-memory, always up-to-date). */
563
+ getCumulativeTokens(sellerPeerId) {
564
+ const meta = this._metadata.get(sellerPeerId) ?? ZERO_METADATA;
565
+ return { inputTokens: meta.cumulativeInputTokens, outputTokens: meta.cumulativeOutputTokens };
265
566
  }
266
567
  /**
267
- * Check if a session lock has been confirmed (for polling).
568
+ * Accumulate response token counts and persist to the channel store.
569
+ * Tracks its own running totals independently of signPerRequestAuth metadata,
570
+ * so the persisted data is always up-to-date after each response.
268
571
  */
572
+ recordAndPersistTokens(sellerPeerId, inputTokens, outputTokens) {
573
+ const session = this.getActiveSession(sellerPeerId);
574
+ if (!session)
575
+ return;
576
+ const prev = this._responseTokenTotals.get(sellerPeerId) ?? { input: 0, output: 0, requests: 0 };
577
+ const totals = {
578
+ input: prev.input + inputTokens,
579
+ output: prev.output + outputTokens,
580
+ requests: prev.requests + 1,
581
+ };
582
+ this._responseTokenTotals.set(sellerPeerId, totals);
583
+ this._channelStore.upsertChannel({
584
+ ...session,
585
+ tokensDelivered: String(totals.input),
586
+ previousConsumption: String(totals.output),
587
+ requestCount: totals.requests,
588
+ updatedAt: Date.now(),
589
+ });
590
+ }
591
+ /** Get the live response token totals for a seller, or null if none recorded this session. */
592
+ getResponseTokenTotals(sellerPeerId) {
593
+ return this._responseTokenTotals.get(sellerPeerId) ?? null;
594
+ }
595
+ /** Check if a session has been confirmed via AuthAck. */
596
+ isAuthorized(sellerPeerId) {
597
+ return this._confirmedPeers.has(sellerPeerId);
598
+ }
599
+ /** Alias for isAuthorized (used by polling loop). */
269
600
  isLockConfirmed(sellerPeerId) {
270
- const session = this._sessions.get(sellerPeerId);
271
- return session?.status === 'confirmed' || session?.status === 'active';
601
+ return this.isAuthorized(sellerPeerId);
272
602
  }
273
- /**
274
- * Check if a session lock has been rejected (for polling).
275
- */
603
+ /** Check if the lock was explicitly rejected (not just never-contacted). */
276
604
  isLockRejected(sellerPeerId) {
277
- return !this._sessions.has(sellerPeerId);
605
+ return this._rejectedPeers.has(sellerPeerId);
606
+ }
607
+ /** Mark a peer as having rejected our spending auth. */
608
+ markRejected(sellerPeerId) {
609
+ this._rejectedPeers.add(sellerPeerId);
610
+ debugLog(`[BuyerPayment] Peer ${sellerPeerId.slice(0, 12)}... marked as rejected`);
611
+ }
612
+ getSessionHistory(sellerPeerId) {
613
+ const session = this._channelStore.getLatestChannelByPeerAndBuyer(sellerPeerId, 'buyer', this._identity.wallet.address);
614
+ return session ? [session] : [];
615
+ }
616
+ // ── Deposit operations ──────────────────────────────────────────
617
+ async deposit(amount) {
618
+ debugLog(`[BuyerPayment] Depositing ${amount} to deposits`);
619
+ const buyer = this._identity.wallet.address;
620
+ return this._depositsClient.deposit(this._signer, buyer, amount);
621
+ }
622
+ async withdraw(amount) {
623
+ debugLog(`[BuyerPayment] Withdrawing ${amount} from deposits`);
624
+ return this._depositsClient.withdraw(this._signer, this._identity.wallet.address, amount);
625
+ }
626
+ async getBalance() {
627
+ const buyerAddr = this._identity.wallet.address;
628
+ const info = await this._depositsClient.getBuyerBalance(buyerAddr);
629
+ return { available: info.available, reserved: info.reserved };
630
+ }
631
+ // ── Response cost parsing ──────────────────────────────────────
632
+ static parseResponseCost(headers) {
633
+ const costStr = headers[HEADER_COST];
634
+ if (costStr === undefined || costStr === '')
635
+ return null;
636
+ try {
637
+ const cost = BigInt(costStr);
638
+ const inputStr = headers[HEADER_INPUT_TOKENS];
639
+ const inputTokens = inputStr !== undefined && inputStr !== '' ? BigInt(inputStr) : 0n;
640
+ const outputStr = headers[HEADER_OUTPUT_TOKENS];
641
+ const outputTokens = outputStr !== undefined && outputStr !== '' ? BigInt(outputStr) : 0n;
642
+ return { cost, inputTokens, outputTokens };
643
+ }
644
+ catch {
645
+ return null;
646
+ }
278
647
  }
279
648
  }
280
649
  //# sourceMappingURL=buyer-payment-manager.js.map