@antseed/node 0.2.27 → 0.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/README.md +14 -13
  2. package/dist/buyer-request-handler.d.ts +41 -0
  3. package/dist/buyer-request-handler.d.ts.map +1 -0
  4. package/dist/buyer-request-handler.js +254 -0
  5. package/dist/buyer-request-handler.js.map +1 -0
  6. package/dist/discovery/announcer.d.ts +5 -4
  7. package/dist/discovery/announcer.d.ts.map +1 -1
  8. package/dist/discovery/announcer.js +11 -18
  9. package/dist/discovery/announcer.js.map +1 -1
  10. package/dist/discovery/index.d.ts +0 -1
  11. package/dist/discovery/index.d.ts.map +1 -1
  12. package/dist/discovery/index.js +0 -1
  13. package/dist/discovery/index.js.map +1 -1
  14. package/dist/discovery/metadata-codec.d.ts +2 -2
  15. package/dist/discovery/metadata-codec.d.ts.map +1 -1
  16. package/dist/discovery/metadata-codec.js +47 -72
  17. package/dist/discovery/metadata-codec.js.map +1 -1
  18. package/dist/discovery/metadata-validator.js +6 -6
  19. package/dist/discovery/metadata-validator.js.map +1 -1
  20. package/dist/discovery/peer-lookup.d.ts.map +1 -1
  21. package/dist/discovery/peer-lookup.js +1 -2
  22. package/dist/discovery/peer-lookup.js.map +1 -1
  23. package/dist/discovery/peer-metadata.d.ts +3 -5
  24. package/dist/discovery/peer-metadata.d.ts.map +1 -1
  25. package/dist/discovery/peer-metadata.js +1 -1
  26. package/dist/discovery/reputation-verifier.d.ts +2 -25
  27. package/dist/discovery/reputation-verifier.d.ts.map +1 -1
  28. package/dist/discovery/reputation-verifier.js +2 -48
  29. package/dist/discovery/reputation-verifier.js.map +1 -1
  30. package/dist/discovery/stats-verifier.d.ts +27 -0
  31. package/dist/discovery/stats-verifier.d.ts.map +1 -0
  32. package/dist/discovery/stats-verifier.js +38 -0
  33. package/dist/discovery/stats-verifier.js.map +1 -0
  34. package/dist/index.d.ts +10 -7
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +8 -5
  37. package/dist/index.js.map +1 -1
  38. package/dist/metering/index.d.ts +1 -0
  39. package/dist/metering/index.d.ts.map +1 -1
  40. package/dist/metering/index.js +1 -0
  41. package/dist/metering/index.js.map +1 -1
  42. package/dist/metering/receipt-generator.d.ts +4 -4
  43. package/dist/metering/receipt-generator.d.ts.map +1 -1
  44. package/dist/metering/receipt-verifier.d.ts +6 -6
  45. package/dist/metering/receipt-verifier.d.ts.map +1 -1
  46. package/dist/metering/receipt-verifier.js +1 -1
  47. package/dist/metering/seller-session-tracker.d.ts +91 -0
  48. package/dist/metering/seller-session-tracker.d.ts.map +1 -0
  49. package/dist/metering/seller-session-tracker.js +261 -0
  50. package/dist/metering/seller-session-tracker.js.map +1 -0
  51. package/dist/metering/storage.d.ts +11 -5
  52. package/dist/metering/storage.d.ts.map +1 -1
  53. package/dist/metering/storage.js +28 -80
  54. package/dist/metering/storage.js.map +1 -1
  55. package/dist/node.d.ts +69 -117
  56. package/dist/node.d.ts.map +1 -1
  57. package/dist/node.js +240 -1269
  58. package/dist/node.js.map +1 -1
  59. package/dist/p2p/connection-auth.d.ts +2 -1
  60. package/dist/p2p/connection-auth.d.ts.map +1 -1
  61. package/dist/p2p/connection-auth.js +6 -6
  62. package/dist/p2p/connection-auth.js.map +1 -1
  63. package/dist/p2p/connection-manager.d.ts +3 -2
  64. package/dist/p2p/connection-manager.d.ts.map +1 -1
  65. package/dist/p2p/connection-manager.js +6 -6
  66. package/dist/p2p/connection-manager.js.map +1 -1
  67. package/dist/p2p/identity.d.ts +22 -15
  68. package/dist/p2p/identity.d.ts.map +1 -1
  69. package/dist/p2p/identity.js +66 -51
  70. package/dist/p2p/identity.js.map +1 -1
  71. package/dist/p2p/index.d.ts +1 -1
  72. package/dist/p2p/index.d.ts.map +1 -1
  73. package/dist/p2p/index.js +1 -1
  74. package/dist/p2p/index.js.map +1 -1
  75. package/dist/p2p/payment-codec.d.ts +4 -8
  76. package/dist/p2p/payment-codec.d.ts.map +1 -1
  77. package/dist/p2p/payment-codec.js +27 -57
  78. package/dist/p2p/payment-codec.js.map +1 -1
  79. package/dist/p2p/payment-mux.d.ts +4 -10
  80. package/dist/p2p/payment-mux.d.ts.map +1 -1
  81. package/dist/p2p/payment-mux.js +11 -33
  82. package/dist/p2p/payment-mux.js.map +1 -1
  83. package/dist/payments/balance-manager.d.ts +2 -2
  84. package/dist/payments/balance-manager.d.ts.map +1 -1
  85. package/dist/payments/balance-manager.js +5 -5
  86. package/dist/payments/balance-manager.js.map +1 -1
  87. package/dist/payments/buyer-payment-manager.d.ts +154 -21
  88. package/dist/payments/buyer-payment-manager.d.ts.map +1 -1
  89. package/dist/payments/buyer-payment-manager.js +540 -166
  90. package/dist/payments/buyer-payment-manager.js.map +1 -1
  91. package/dist/payments/buyer-payment-negotiator.d.ts +84 -0
  92. package/dist/payments/buyer-payment-negotiator.d.ts.map +1 -0
  93. package/dist/payments/buyer-payment-negotiator.js +624 -0
  94. package/dist/payments/buyer-payment-negotiator.js.map +1 -0
  95. package/dist/payments/chain-config.d.ts +10 -4
  96. package/dist/payments/chain-config.d.ts.map +1 -1
  97. package/dist/payments/chain-config.js +19 -9
  98. package/dist/payments/chain-config.js.map +1 -1
  99. package/dist/payments/channel-session-state.d.ts +13 -0
  100. package/dist/payments/channel-session-state.d.ts.map +1 -0
  101. package/dist/payments/channel-session-state.js +25 -0
  102. package/dist/payments/channel-session-state.js.map +1 -0
  103. package/dist/payments/channel-store.d.ts +87 -0
  104. package/dist/payments/channel-store.d.ts.map +1 -0
  105. package/dist/payments/channel-store.js +276 -0
  106. package/dist/payments/channel-store.js.map +1 -0
  107. package/dist/payments/evm/ants-token-client.d.ts +1 -1
  108. package/dist/payments/evm/ants-token-client.d.ts.map +1 -1
  109. package/dist/payments/evm/ants-token-client.js +3 -4
  110. package/dist/payments/evm/ants-token-client.js.map +1 -1
  111. package/dist/payments/evm/base-evm-client.d.ts +10 -1
  112. package/dist/payments/evm/base-evm-client.d.ts.map +1 -1
  113. package/dist/payments/evm/base-evm-client.js +34 -1
  114. package/dist/payments/evm/base-evm-client.js.map +1 -1
  115. package/dist/payments/evm/channels-client.d.ts +51 -0
  116. package/dist/payments/evm/channels-client.d.ts.map +1 -0
  117. package/dist/payments/evm/channels-client.js +101 -0
  118. package/dist/payments/evm/channels-client.js.map +1 -0
  119. package/dist/payments/evm/deposits-client.d.ts +30 -0
  120. package/dist/payments/evm/deposits-client.d.ts.map +1 -0
  121. package/dist/payments/evm/deposits-client.js +78 -0
  122. package/dist/payments/evm/deposits-client.js.map +1 -0
  123. package/dist/payments/evm/emissions-client.d.ts +3 -4
  124. package/dist/payments/evm/emissions-client.d.ts.map +1 -1
  125. package/dist/payments/evm/emissions-client.js +11 -30
  126. package/dist/payments/evm/emissions-client.js.map +1 -1
  127. package/dist/payments/evm/identity-client.d.ts +10 -23
  128. package/dist/payments/evm/identity-client.d.ts.map +1 -1
  129. package/dist/payments/evm/identity-client.js +43 -100
  130. package/dist/payments/evm/identity-client.js.map +1 -1
  131. package/dist/payments/evm/keypair.d.ts +3 -14
  132. package/dist/payments/evm/keypair.d.ts.map +1 -1
  133. package/dist/payments/evm/keypair.js +4 -20
  134. package/dist/payments/evm/keypair.js.map +1 -1
  135. package/dist/payments/evm/sessions-client.d.ts +30 -0
  136. package/dist/payments/evm/sessions-client.d.ts.map +1 -0
  137. package/dist/payments/evm/sessions-client.js +61 -0
  138. package/dist/payments/evm/sessions-client.js.map +1 -0
  139. package/dist/payments/evm/signatures.d.ts +43 -12
  140. package/dist/payments/evm/signatures.d.ts.map +1 -1
  141. package/dist/payments/evm/signatures.js +62 -45
  142. package/dist/payments/evm/signatures.js.map +1 -1
  143. package/dist/payments/evm/staking-client.d.ts +24 -0
  144. package/dist/payments/evm/staking-client.d.ts.map +1 -0
  145. package/dist/payments/evm/staking-client.js +54 -0
  146. package/dist/payments/evm/staking-client.js.map +1 -0
  147. package/dist/payments/evm/stats-client.d.ts +20 -0
  148. package/dist/payments/evm/stats-client.d.ts.map +1 -0
  149. package/dist/payments/evm/stats-client.js +25 -0
  150. package/dist/payments/evm/stats-client.js.map +1 -0
  151. package/dist/payments/index.d.ts +17 -10
  152. package/dist/payments/index.d.ts.map +1 -1
  153. package/dist/payments/index.js +15 -8
  154. package/dist/payments/index.js.map +1 -1
  155. package/dist/payments/pricing.d.ts +25 -0
  156. package/dist/payments/pricing.d.ts.map +1 -0
  157. package/dist/payments/pricing.js +33 -0
  158. package/dist/payments/pricing.js.map +1 -0
  159. package/dist/payments/readiness.d.ts +4 -3
  160. package/dist/payments/readiness.d.ts.map +1 -1
  161. package/dist/payments/readiness.js +11 -18
  162. package/dist/payments/readiness.js.map +1 -1
  163. package/dist/payments/seller-payment-manager.d.ts +72 -47
  164. package/dist/payments/seller-payment-manager.d.ts.map +1 -1
  165. package/dist/payments/seller-payment-manager.js +558 -275
  166. package/dist/payments/seller-payment-manager.js.map +1 -1
  167. package/dist/payments/session-store.d.ts +3 -0
  168. package/dist/payments/session-store.d.ts.map +1 -1
  169. package/dist/payments/session-store.js +31 -2
  170. package/dist/payments/session-store.js.map +1 -1
  171. package/dist/payments/types.d.ts +5 -3
  172. package/dist/payments/types.d.ts.map +1 -1
  173. package/dist/proxy/proxy-mux.d.ts.map +1 -1
  174. package/dist/proxy/proxy-mux.js +3 -2
  175. package/dist/proxy/proxy-mux.js.map +1 -1
  176. package/dist/proxy/request-codec.d.ts.map +1 -1
  177. package/dist/proxy/request-codec.js +3 -0
  178. package/dist/proxy/request-codec.js.map +1 -1
  179. package/dist/reputation/rating-manager.d.ts.map +1 -1
  180. package/dist/reputation/rating-manager.js +2 -4
  181. package/dist/reputation/rating-manager.js.map +1 -1
  182. package/dist/reputation/report-manager.d.ts.map +1 -1
  183. package/dist/reputation/report-manager.js +2 -4
  184. package/dist/reputation/report-manager.js.map +1 -1
  185. package/dist/routing/default-router.d.ts.map +1 -1
  186. package/dist/routing/default-router.js +4 -9
  187. package/dist/routing/default-router.js.map +1 -1
  188. package/dist/seller-request-handler.d.ts +54 -0
  189. package/dist/seller-request-handler.d.ts.map +1 -0
  190. package/dist/seller-request-handler.js +359 -0
  191. package/dist/seller-request-handler.js.map +1 -0
  192. package/dist/storage/migrate.d.ts +13 -0
  193. package/dist/storage/migrate.d.ts.map +1 -0
  194. package/dist/storage/migrate.js +28 -0
  195. package/dist/storage/migrate.js.map +1 -0
  196. package/dist/storage/migrations/channels/001_create_tables.d.ts +3 -0
  197. package/dist/storage/migrations/channels/001_create_tables.d.ts.map +1 -0
  198. package/dist/storage/migrations/channels/001_create_tables.js +45 -0
  199. package/dist/storage/migrations/channels/001_create_tables.js.map +1 -0
  200. package/dist/storage/migrations/channels/002_add_auth_sig_columns.d.ts +3 -0
  201. package/dist/storage/migrations/channels/002_add_auth_sig_columns.d.ts.map +1 -0
  202. package/dist/storage/migrations/channels/002_add_auth_sig_columns.js +19 -0
  203. package/dist/storage/migrations/channels/002_add_auth_sig_columns.js.map +1 -0
  204. package/dist/storage/migrations/channels/index.d.ts +3 -0
  205. package/dist/storage/migrations/channels/index.d.ts.map +1 -0
  206. package/dist/storage/migrations/channels/index.js +4 -0
  207. package/dist/storage/migrations/channels/index.js.map +1 -0
  208. package/dist/storage/migrations/metering/001_create_tables.d.ts +3 -0
  209. package/dist/storage/migrations/metering/001_create_tables.d.ts.map +1 -0
  210. package/dist/storage/migrations/metering/001_create_tables.js +80 -0
  211. package/dist/storage/migrations/metering/001_create_tables.js.map +1 -0
  212. package/dist/storage/migrations/metering/index.d.ts +3 -0
  213. package/dist/storage/migrations/metering/index.d.ts.map +1 -0
  214. package/dist/storage/migrations/metering/index.js +3 -0
  215. package/dist/storage/migrations/metering/index.js.map +1 -0
  216. package/dist/types/capability.d.ts +1 -1
  217. package/dist/types/metering.d.ts +1 -1
  218. package/dist/types/peer.d.ts +10 -11
  219. package/dist/types/peer.d.ts.map +1 -1
  220. package/dist/types/peer.js +7 -3
  221. package/dist/types/peer.js.map +1 -1
  222. package/dist/types/protocol.d.ts +22 -70
  223. package/dist/types/protocol.d.ts.map +1 -1
  224. package/dist/types/protocol.js +1 -3
  225. package/dist/types/protocol.js.map +1 -1
  226. package/dist/types/rating.d.ts +1 -1
  227. package/dist/types/rating.d.ts.map +1 -1
  228. package/dist/types/report.d.ts +1 -1
  229. package/dist/types/report.d.ts.map +1 -1
  230. package/dist/utils/response-usage.d.ts +10 -0
  231. package/dist/utils/response-usage.d.ts.map +1 -0
  232. package/dist/utils/response-usage.js +34 -0
  233. package/dist/utils/response-usage.js.map +1 -0
  234. package/package.json +3 -3
@@ -1,38 +1,77 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { encodeBytes32String } from 'ethers';
3
- import { BaseEscrowClient } from './evm/escrow-client.js';
4
- import { identityToEvmWallet, identityToEvmAddress } from './evm/keypair.js';
5
- import { signSpendingAuth, makeEscrowDomain, buildAckMessage, signMessageEd25519, verifyMessageEd25519, buildReceiptMessage, } from './evm/signatures.js';
6
- 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';
7
4
  import { debugLog, debugWarn } from '../utils/debug.js';
8
- const ZERO_SESSION_ID = '0x' + '0'.repeat(64);
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;
9
16
  /**
10
17
  * Manages buyer-side payment sessions using EIP-712 SpendingAuth
11
- * with persistent session storage.
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
- _sessionStore;
25
+ _channelStore;
19
26
  /** In-memory map of active confirmed sessions by seller peerId for fast lookups. */
20
27
  _confirmedPeers = new Set();
21
28
  /** Peers that explicitly rejected our spending auth. */
22
29
  _rejectedPeers = new Set();
23
- _nonceCounter;
24
- constructor(identity, config, sessionStore) {
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) {
25
47
  this._identity = identity;
26
48
  this._config = config;
27
- this._signer = identityToEvmWallet(identity);
28
- this._escrowClient = new BaseEscrowClient({
49
+ this._signer = identity.wallet;
50
+ this._depositsClient = new DepositsClient({
29
51
  rpcUrl: config.rpcUrl,
30
- contractAddress: config.contractAddress,
52
+ contractAddress: config.depositsContractAddress,
31
53
  usdcAddress: config.usdcAddress,
32
54
  });
33
- this._sessionStore = sessionStore;
34
- // Restore nonce counter from persisted sessions to avoid duplicates across restarts
35
- this._nonceCounter = sessionStore.getMaxNonce('buyer');
55
+ this._channelStore = channelStore;
56
+ this._channelsDomain = makeChannelsDomain(config.chainId, config.channelsContractAddress);
57
+ // Hydrate cumulative maps from persisted active sessions
58
+ this._hydrateFromStore();
59
+ }
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
+ }
36
75
  }
37
76
  get signer() {
38
77
  return this._signer;
@@ -40,184 +79,519 @@ export class BuyerPaymentManager {
40
79
  setSigner(signer) {
41
80
  this._signer = signer;
42
81
  }
43
- get escrowClient() {
44
- 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);
115
+ }
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;
139
+ }
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;
45
167
  }
46
168
  // ── Spending Authorization ────────────────────────────────────
47
169
  /**
48
- * Sign and send an EIP-712 SpendingAuth to a seller.
49
- * Loads the latest session to build the proof chain.
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.
50
174
  */
51
- async authorizeSpending(sellerPeerId, sellerEvmAddr, paymentMux, maxAmount) {
52
- const amount = maxAmount ?? this._config.defaultMaxAmountUsdc;
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
+ }
53
188
  // Clear confirmation state so we wait for a fresh AuthAck on the new session
54
189
  this._confirmedPeers.delete(sellerPeerId);
55
- // Load latest session to build proof chain.
56
- // Only chain if the session is settled or active-with-delivery (the seller
57
- // will settle it on-chain before reserving the new one).
58
- // Fall back to first-sign for timed-out/ghost sessions.
59
- const latestSession = this._sessionStore.getLatestSession(sellerPeerId, 'buyer');
60
- const canChain = latestSession
61
- && latestSession.status !== 'timeout'
62
- && latestSession.status !== 'ghost'
63
- && BigInt(latestSession.tokensDelivered) > 0n;
64
- const previousConsumption = canChain
65
- ? BigInt(latestSession.tokensDelivered)
66
- : 0n;
67
- const previousSessionId = canChain
68
- ? latestSession.sessionId
69
- : ZERO_SESSION_ID;
70
- // Generate a 32-byte session ID
71
- const sessionIdBytes = randomBytes(32);
72
- const sessionId = '0x' + sessionIdBytes.toString('hex');
73
- const nonce = ++this._nonceCounter;
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);
74
198
  const deadline = Math.floor(Date.now() / 1000) + this._config.defaultAuthDurationSecs;
75
- debugLog(`[BuyerPayment] authorizeSpending: session=${sessionId.slice(0, 18)}... seller=${sellerPeerId.slice(0, 12)}... amount=${amount}`);
76
- // Sign EIP-712 SpendingAuth
77
- const domain = makeEscrowDomain(this._config.chainId, this._config.contractAddress);
78
- const msg = {
79
- seller: sellerEvmAddr,
80
- sessionId,
81
- maxAmount: amount,
82
- nonce,
83
- deadline,
84
- previousConsumption,
85
- previousSessionId,
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),
86
207
  };
87
- const buyerSig = await signSpendingAuth(this._signer, domain, msg);
88
- const buyerEvmAddr = identityToEvmAddress(this._identity);
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);
89
215
  // Store session
90
216
  const now = Date.now();
91
217
  const session = {
92
- sessionId,
218
+ sessionId: channelId,
93
219
  peerId: sellerPeerId,
94
220
  role: 'buyer',
95
- sellerEvmAddr,
96
- buyerEvmAddr,
97
- nonce,
98
- authMax: amount.toString(),
221
+ sellerEvmAddr: peerIdToAddress(sellerPeerId),
222
+ buyerEvmAddr: this._identity.wallet.address,
223
+ nonce: 0,
224
+ authMax: minBudgetPerRequest.toString(),
99
225
  deadline,
100
- previousSessionId,
101
- previousConsumption: previousConsumption.toString(),
226
+ previousSessionId: '0x' + '0'.repeat(64),
227
+ previousConsumption: '0',
102
228
  tokensDelivered: '0',
103
229
  requestCount: 0,
104
230
  reservedAt: now,
105
231
  settledAt: null,
106
232
  settledAmount: null,
107
233
  status: 'active',
234
+ latestBuyerSig: null,
235
+ latestSpendingAuthSig: null,
236
+ latestMetadata: null,
108
237
  createdAt: now,
109
238
  updatedAt: now,
110
239
  };
111
- this._sessionStore.upsertSession(session);
112
- // Send SpendingAuth via PaymentMux
240
+ this._channelStore.upsertChannel(session);
241
+ // Send SpendingAuth via PaymentMux — reserve carries ReserveAuth sig
113
242
  paymentMux.sendSpendingAuth({
114
- sessionId,
115
- maxAmountUsdc: amount.toString(),
116
- nonce,
117
- deadline,
118
- buyerSig,
119
- buyerEvmAddr,
120
- previousConsumption: previousConsumption.toString(),
121
- previousSessionId,
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,
122
251
  });
123
- return sessionId;
252
+ return channelId;
124
253
  }
125
254
  // ── AuthAck handler ───────────────────────────────────────────
126
255
  handleAuthAck(sellerPeerId, payload) {
127
- const session = this._sessionStore.getActiveSessionByPeer(sellerPeerId, 'buyer');
256
+ const session = this.getActiveSession(sellerPeerId);
128
257
  if (!session) {
129
258
  debugWarn(`[BuyerPayment] AuthAck for unknown seller: ${sellerPeerId.slice(0, 12)}...`);
130
259
  return;
131
260
  }
132
- if (session.sessionId !== payload.sessionId) {
133
- debugWarn(`[BuyerPayment] AuthAck 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)}...`);
134
263
  return;
135
264
  }
136
265
  this._confirmedPeers.add(sellerPeerId);
137
- debugLog(`[BuyerPayment] AuthAck confirmed: session=${session.sessionId.slice(0, 18)}...`);
266
+ debugLog(`[BuyerPayment] AuthAck confirmed: channel=${session.sessionId.slice(0, 18)}...`);
267
+ }
268
+ // ── Buyer-side cost verification ──────────────────────────────
269
+ /**
270
+ * Estimate tokens and cost from response content without updating state.
271
+ */
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;
318
+ }
319
+ /**
320
+ * Check whether the current cumulative amount is approaching the reserve ceiling
321
+ * and a top-up should be triggered.
322
+ */
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);
345
+ if (!session) {
346
+ throw new Error(`[BuyerPayment] No active session for seller ${sellerPeerId.slice(0, 12)}... — call authorizeSpending() first`);
347
+ }
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);
355
+ }
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
+ }
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 };
138
419
  }
139
- // ── Seller Receipt handler ────────────────────────────────────
140
- async handleSellerReceipt(sellerPeerId, receipt, paymentMux) {
141
- const session = this._sessionStore.getActiveSessionByPeer(sellerPeerId, 'buyer');
420
+ // ── NeedAuth handler ───────────────────────────────────────────
421
+ /**
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).
424
+ */
425
+ async handleNeedAuth(sellerPeerId, payload, paymentMux) {
426
+ const session = this.getActiveSession(sellerPeerId);
142
427
  if (!session) {
143
- debugWarn(`[BuyerPayment] Receipt for unknown seller: ${sellerPeerId.slice(0, 12)}...`);
428
+ debugWarn(`[BuyerPayment] NeedAuth for unknown seller: ${sellerPeerId.slice(0, 12)}...`);
144
429
  return;
145
430
  }
146
- if (session.sessionId !== receipt.sessionId) {
147
- debugWarn(`[BuyerPayment] Receipt session ID mismatch: active=${session.sessionId.slice(0, 18)}... receipt=${receipt.sessionId.slice(0, 18)}... — discarding stale receipt`);
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`);
148
436
  return;
149
437
  }
150
- // Verify seller's Ed25519 signature
151
- try {
152
- const sellerPublicKey = hexToBytes(sellerPeerId);
153
- const sessionIdBytes = hexToBytes(receipt.sessionId.replace(/^0x/, ''));
154
- const responseHashBytes = hexToBytes(receipt.responseHash);
155
- const receiptMsg = buildReceiptMessage(sessionIdBytes, BigInt(receipt.runningTotal), receipt.requestCount, responseHashBytes);
156
- const sigBytes = hexToBytes(receipt.sellerSig);
157
- const valid = await verifyMessageEd25519(sellerPublicKey, sigBytes, receiptMsg);
158
- if (!valid) {
159
- debugWarn(`[BuyerPayment] Invalid seller receipt signature from ${sellerPeerId.slice(0, 12)}...`);
160
- 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);
161
444
  }
445
+ catch (err) {
446
+ debugWarn(`[BuyerPayment] NeedAuth: topUpReserve failed: ${err instanceof Error ? err.message : err}`);
447
+ }
448
+ maxSignable = this._maxSignable(sellerPeerId);
162
449
  }
163
- catch (err) {
164
- debugWarn(`[BuyerPayment] Failed to verify receipt: ${err instanceof Error ? err.message : err}`);
165
- return;
166
- }
167
- // Validate monotonic increase: runningTotal must exceed previous
168
- const newTotal = BigInt(receipt.runningTotal);
169
- const prevTotal = BigInt(session.tokensDelivered);
170
- if (newTotal <= prevTotal) {
171
- debugWarn(`[BuyerPayment] Receipt runningTotal not monotonic: new=${newTotal} prev=${prevTotal}`);
450
+ if (maxSignable <= currentCumulative) {
451
+ debugWarn(`[BuyerPayment] NeedAuth: maxSignable=${maxSignable} <= currentCumulative=${currentCumulative} cannot authorize more (overdraft limit reached)`);
172
452
  return;
173
453
  }
174
- // Note: we don't compare token count against authMax (USDC) here because
175
- // they're in different units (tokens vs USDC base units). The on-chain
176
- // settle() caps chargeAmount = min(tokenCount * tokenRate, maxAmount),
177
- // so the buyer's EIP-712 signature is the real USDC protection.
178
- debugLog(`[BuyerPayment] Receipt: session=${session.sessionId.slice(0, 18)}... total=${receipt.runningTotal} count=${receipt.requestCount}`);
179
- // Atomically update tokens delivered and store receipt
180
- this._sessionStore.updateDeliveredAndInsertReceipt(session.sessionId, receipt.runningTotal, receipt.requestCount, {
181
- sessionId: session.sessionId,
182
- runningTotal: receipt.runningTotal,
183
- requestCount: receipt.requestCount,
184
- responseHash: receipt.responseHash,
185
- sellerSig: receipt.sellerSig,
186
- buyerAckSig: null,
187
- createdAt: Date.now(),
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(),
188
475
  });
189
- // Auto-ack if configured
190
- if (this._config.autoAck) {
191
- const sessionIdBytes = hexToBytes(session.sessionId.replace(/^0x/, ''));
192
- const ackMsg = buildAckMessage(sessionIdBytes, BigInt(receipt.runningTotal), receipt.requestCount);
193
- const sigBytes = await signMessageEd25519(this._identity, ackMsg);
194
- const buyerSig = bytesToHex(sigBytes);
195
- paymentMux.sendBuyerAck({
196
- sessionId: session.sessionId,
197
- runningTotal: receipt.runningTotal,
198
- requestCount: receipt.requestCount,
199
- buyerSig,
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,
200
484
  });
201
- debugLog(`[BuyerPayment] Auto-ack sent for session=${session.sessionId.slice(0, 18)}...`);
485
+ debugLog(`[BuyerPayment] NeedAuth responded: new cumulativeAmount=${effectiveAmount}`);
486
+ }
487
+ catch {
488
+ debugLog(`[BuyerPayment] NeedAuth: connection closed before SpendingAuth could be sent`);
202
489
  }
203
490
  }
204
- // ── TopUp handler ─────────────────────────────────────────────
205
- async handleTopUpRequest(sellerPeerId, request, paymentMux) {
206
- const session = this._sessionStore.getActiveSessionByPeer(sellerPeerId, 'buyer');
491
+ // ── Reserve top-up ─────────────────────────────────────────────
492
+ /**
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).
496
+ */
497
+ async topUpReserve(sellerPeerId, paymentMux) {
498
+ const session = this.getActiveSession(sellerPeerId);
207
499
  if (!session) {
208
- debugWarn(`[BuyerPayment] Top-up for unknown seller: ${sellerPeerId.slice(0, 12)}...`);
500
+ debugWarn(`[BuyerPayment] topUpReserve: no active session for ${sellerPeerId.slice(0, 12)}...`);
209
501
  return;
210
502
  }
211
- debugLog(`[BuyerPayment] TopUp request: session=${session.sessionId.slice(0, 18)}... currentUsed=${request.currentUsed} currentMax=${request.currentMax}`);
212
- // Sign a new SpendingAuth with increased cap
213
- const currentMax = BigInt(session.authMax);
214
- const additionalAmount = BigInt(request.requestedAdditional);
215
- const newMax = currentMax + additionalAmount;
216
- // The new auth embeds the current consumption as previousConsumption
217
- await this.authorizeSpending(sellerPeerId, session.sellerEvmAddr, paymentMux, newMax);
218
- debugLog(`[BuyerPayment] TopUp authorized: new auth sent with max=${newMax}`);
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`);
539
+ }
219
540
  }
220
541
  // ── Queries ───────────────────────────────────────────────────
542
+ /** Max USDC overdraft (unverified exposure) from buyer config. */
543
+ get maxPerRequestUsdc() {
544
+ return this._config.maxPerRequestUsdc;
545
+ }
546
+ /** Max USDC per ReserveAuth signature from buyer config. */
547
+ get maxReserveAmountUsdc() {
548
+ return this._config.maxReserveAmountUsdc;
549
+ }
550
+ /** Current buyer-verified cost for a seller. */
551
+ getVerifiedCost(sellerPeerId) {
552
+ return this._verifiedCost.get(sellerPeerId) ?? 0n;
553
+ }
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;
557
+ }
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 };
566
+ }
567
+ /**
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.
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
+ }
221
595
  /** Check if a session has been confirmed via AuthAck. */
222
596
  isAuthorized(sellerPeerId) {
223
597
  return this._confirmedPeers.has(sellerPeerId);
@@ -236,40 +610,40 @@ export class BuyerPaymentManager {
236
610
  debugLog(`[BuyerPayment] Peer ${sellerPeerId.slice(0, 12)}... marked as rejected`);
237
611
  }
238
612
  getSessionHistory(sellerPeerId) {
239
- const sessions = [];
240
- const seen = new Set();
241
- let session = this._sessionStore.getLatestSession(sellerPeerId, 'buyer');
242
- while (session && !seen.has(session.sessionId)) {
243
- seen.add(session.sessionId);
244
- sessions.unshift(session);
245
- if (session.previousSessionId === ZERO_SESSION_ID)
246
- break;
247
- session = this._sessionStore.getSession(session.previousSessionId);
248
- }
249
- return sessions;
613
+ const session = this._channelStore.getLatestChannelByPeerAndBuyer(sellerPeerId, 'buyer', this._identity.wallet.address);
614
+ return session ? [session] : [];
250
615
  }
251
- // ── Escrow operations ─────────────────────────────────────────
616
+ // ── Deposit operations ──────────────────────────────────────────
252
617
  async deposit(amount) {
253
- debugLog(`[BuyerPayment] Depositing ${amount} to escrow`);
254
- return this._escrowClient.deposit(this._signer, amount);
618
+ debugLog(`[BuyerPayment] Depositing ${amount} to deposits`);
619
+ const buyer = this._identity.wallet.address;
620
+ return this._depositsClient.deposit(this._signer, buyer, amount);
255
621
  }
256
622
  async withdraw(amount) {
257
- debugLog(`[BuyerPayment] Requesting withdrawal of ${amount} from escrow`);
258
- return this._escrowClient.requestWithdrawal(this._signer, amount);
623
+ debugLog(`[BuyerPayment] Withdrawing ${amount} from deposits`);
624
+ return this._depositsClient.withdraw(this._signer, this._identity.wallet.address, amount);
259
625
  }
260
626
  async getBalance() {
261
- const buyerAddr = identityToEvmAddress(this._identity);
262
- const info = await this._escrowClient.getBuyerBalance(buyerAddr);
627
+ const buyerAddr = this._identity.wallet.address;
628
+ const info = await this._depositsClient.getBuyerBalance(buyerAddr);
263
629
  return { available: info.available, reserved: info.reserved };
264
630
  }
265
- // ── Feedback (Task 6) ─────────────────────────────────────────
266
- async submitFeedback(sellerPeerId, qualityScore, identityClient) {
267
- const session = this._sessionStore.getLatestSession(sellerPeerId, 'buyer');
268
- if (!session || session.status !== 'settled')
631
+ // ── Response cost parsing ──────────────────────────────────────
632
+ static parseResponseCost(headers) {
633
+ const costStr = headers[HEADER_COST];
634
+ if (costStr === undefined || costStr === '')
269
635
  return null;
270
- const tokenId = await identityClient.getTokenId(session.sellerEvmAddr);
271
- const tag = encodeBytes32String('quality');
272
- return identityClient.submitFeedback(this._signer, tokenId, qualityScore, tag);
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
+ }
273
647
  }
274
648
  }
275
649
  //# sourceMappingURL=buyer-payment-manager.js.map