@antseed/node 0.2.106 → 0.2.108

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 (216) hide show
  1. package/dist/billing/index.d.ts +2 -0
  2. package/dist/billing/index.d.ts.map +1 -0
  3. package/dist/billing/index.js +2 -0
  4. package/dist/billing/index.js.map +1 -0
  5. package/dist/billing/unit.d.ts +3 -0
  6. package/dist/billing/unit.d.ts.map +1 -0
  7. package/dist/billing/unit.js +3 -0
  8. package/dist/billing/unit.js.map +1 -0
  9. package/dist/buyer-request-handler.d.ts +1 -54
  10. package/dist/buyer-request-handler.d.ts.map +1 -1
  11. package/dist/buyer-request-handler.js +2 -379
  12. package/dist/buyer-request-handler.js.map +1 -1
  13. package/dist/discovery/announcer.d.ts +7 -1
  14. package/dist/discovery/announcer.d.ts.map +1 -1
  15. package/dist/discovery/announcer.js +84 -33
  16. package/dist/discovery/announcer.js.map +1 -1
  17. package/dist/discovery/http-metadata-resolver.d.ts +4 -0
  18. package/dist/discovery/http-metadata-resolver.d.ts.map +1 -1
  19. package/dist/discovery/http-metadata-resolver.js +42 -2
  20. package/dist/discovery/http-metadata-resolver.js.map +1 -1
  21. package/dist/discovery/index.d.ts +1 -1
  22. package/dist/discovery/index.d.ts.map +1 -1
  23. package/dist/discovery/index.js +1 -1
  24. package/dist/discovery/index.js.map +1 -1
  25. package/dist/discovery/metadata-codec.d.ts +4 -4
  26. package/dist/discovery/metadata-codec.d.ts.map +1 -1
  27. package/dist/discovery/metadata-codec.js +339 -23
  28. package/dist/discovery/metadata-codec.js.map +1 -1
  29. package/dist/discovery/metadata-validator.d.ts +7 -3
  30. package/dist/discovery/metadata-validator.d.ts.map +1 -1
  31. package/dist/discovery/metadata-validator.js +139 -8
  32. package/dist/discovery/metadata-validator.js.map +1 -1
  33. package/dist/discovery/peer-lookup.d.ts +1 -0
  34. package/dist/discovery/peer-lookup.d.ts.map +1 -1
  35. package/dist/discovery/peer-lookup.js +23 -5
  36. package/dist/discovery/peer-lookup.js.map +1 -1
  37. package/dist/discovery/peer-metadata.d.ts +2 -90
  38. package/dist/discovery/peer-metadata.d.ts.map +1 -1
  39. package/dist/discovery/peer-metadata.js +3 -11
  40. package/dist/discovery/peer-metadata.js.map +1 -1
  41. package/dist/discovery/seller-address-resolver.d.ts +1 -49
  42. package/dist/discovery/seller-address-resolver.d.ts.map +1 -1
  43. package/dist/discovery/seller-address-resolver.js +2 -114
  44. package/dist/discovery/seller-address-resolver.js.map +1 -1
  45. package/dist/errors.d.ts +2 -0
  46. package/dist/errors.d.ts.map +1 -0
  47. package/dist/errors.js +2 -0
  48. package/dist/errors.js.map +1 -0
  49. package/dist/health/model-health-checker.d.ts +6 -5
  50. package/dist/health/model-health-checker.d.ts.map +1 -1
  51. package/dist/health/model-health-checker.js +22 -10
  52. package/dist/health/model-health-checker.js.map +1 -1
  53. package/dist/index.d.ts +4 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +5 -1
  56. package/dist/index.js.map +1 -1
  57. package/dist/interfaces/seller-provider.d.ts +6 -0
  58. package/dist/interfaces/seller-provider.d.ts.map +1 -1
  59. package/dist/node.d.ts.map +1 -1
  60. package/dist/node.js +34 -5
  61. package/dist/node.js.map +1 -1
  62. package/dist/p2p/connection-auth.d.ts +3 -8
  63. package/dist/p2p/connection-auth.d.ts.map +1 -1
  64. package/dist/p2p/connection-auth.js +4 -6
  65. package/dist/p2p/connection-auth.js.map +1 -1
  66. package/dist/p2p/connection-manager.d.ts +3 -1
  67. package/dist/p2p/connection-manager.d.ts.map +1 -1
  68. package/dist/p2p/connection-manager.js.map +1 -1
  69. package/dist/p2p/identity.d.ts +1 -21
  70. package/dist/p2p/identity.d.ts.map +1 -1
  71. package/dist/p2p/identity.js +3 -62
  72. package/dist/p2p/identity.js.map +1 -1
  73. package/dist/p2p/message-protocol.d.ts +1 -49
  74. package/dist/p2p/message-protocol.d.ts.map +1 -1
  75. package/dist/p2p/message-protocol.js +2 -125
  76. package/dist/p2p/message-protocol.js.map +1 -1
  77. package/dist/p2p/payment-codec.d.ts +1 -21
  78. package/dist/p2p/payment-codec.d.ts.map +1 -1
  79. package/dist/p2p/payment-codec.js +2 -220
  80. package/dist/p2p/payment-codec.js.map +1 -1
  81. package/dist/p2p/payment-mux.d.ts +1 -51
  82. package/dist/p2p/payment-mux.d.ts.map +1 -1
  83. package/dist/p2p/payment-mux.js +2 -159
  84. package/dist/p2p/payment-mux.js.map +1 -1
  85. package/dist/payments/buyer-free-usage-manager.d.ts +1 -43
  86. package/dist/payments/buyer-free-usage-manager.d.ts.map +1 -1
  87. package/dist/payments/buyer-free-usage-manager.js +2 -298
  88. package/dist/payments/buyer-free-usage-manager.js.map +1 -1
  89. package/dist/payments/buyer-payment-manager.d.ts +1 -277
  90. package/dist/payments/buyer-payment-manager.d.ts.map +1 -1
  91. package/dist/payments/buyer-payment-manager.js +2 -1039
  92. package/dist/payments/buyer-payment-manager.js.map +1 -1
  93. package/dist/payments/buyer-payment-negotiator.d.ts +1 -130
  94. package/dist/payments/buyer-payment-negotiator.d.ts.map +1 -1
  95. package/dist/payments/buyer-payment-negotiator.js +2 -950
  96. package/dist/payments/buyer-payment-negotiator.js.map +1 -1
  97. package/dist/payments/chain-config.d.ts.map +1 -1
  98. package/dist/payments/chain-config.js +13 -2
  99. package/dist/payments/chain-config.js.map +1 -1
  100. package/dist/payments/channel-session-state.d.ts +1 -13
  101. package/dist/payments/channel-session-state.d.ts.map +1 -1
  102. package/dist/payments/channel-session-state.js +2 -25
  103. package/dist/payments/channel-session-state.js.map +1 -1
  104. package/dist/payments/channel-store.d.ts +2 -51
  105. package/dist/payments/channel-store.d.ts.map +1 -1
  106. package/dist/payments/channel-store.js +3 -14
  107. package/dist/payments/channel-store.js.map +1 -1
  108. package/dist/payments/channel-usage-accounting.d.ts +1 -17
  109. package/dist/payments/channel-usage-accounting.d.ts.map +1 -1
  110. package/dist/payments/channel-usage-accounting.js +2 -65
  111. package/dist/payments/channel-usage-accounting.js.map +1 -1
  112. package/dist/payments/evm/base-evm-client.d.ts +1 -26
  113. package/dist/payments/evm/base-evm-client.d.ts.map +1 -1
  114. package/dist/payments/evm/base-evm-client.js +2 -115
  115. package/dist/payments/evm/base-evm-client.js.map +1 -1
  116. package/dist/payments/evm/channels-client.d.ts +1 -85
  117. package/dist/payments/evm/channels-client.d.ts.map +1 -1
  118. package/dist/payments/evm/channels-client.js +2 -193
  119. package/dist/payments/evm/channels-client.js.map +1 -1
  120. package/dist/payments/evm/deposits-client.d.ts +1 -29
  121. package/dist/payments/evm/deposits-client.d.ts.map +1 -1
  122. package/dist/payments/evm/deposits-client.js +2 -67
  123. package/dist/payments/evm/deposits-client.js.map +1 -1
  124. package/dist/payments/evm/signatures.d.ts +1 -203
  125. package/dist/payments/evm/signatures.d.ts.map +1 -1
  126. package/dist/payments/evm/signatures.js +2 -236
  127. package/dist/payments/evm/signatures.js.map +1 -1
  128. package/dist/payments/evm/subpool-client.d.ts +32 -0
  129. package/dist/payments/evm/subpool-client.d.ts.map +1 -0
  130. package/dist/payments/evm/subpool-client.js +158 -0
  131. package/dist/payments/evm/subpool-client.js.map +1 -0
  132. package/dist/payments/index.d.ts +1 -1
  133. package/dist/payments/index.d.ts.map +1 -1
  134. package/dist/payments/index.js +1 -1
  135. package/dist/payments/index.js.map +1 -1
  136. package/dist/payments/pricing.d.ts +1 -26
  137. package/dist/payments/pricing.d.ts.map +1 -1
  138. package/dist/payments/pricing.js +2 -36
  139. package/dist/payments/pricing.js.map +1 -1
  140. package/dist/payments/usdc-utils.d.ts +1 -8
  141. package/dist/payments/usdc-utils.d.ts.map +1 -1
  142. package/dist/payments/usdc-utils.js +2 -16
  143. package/dist/payments/usdc-utils.js.map +1 -1
  144. package/dist/proxy/proxy-mux.d.ts +1 -69
  145. package/dist/proxy/proxy-mux.d.ts.map +1 -1
  146. package/dist/proxy/proxy-mux.js +2 -346
  147. package/dist/proxy/proxy-mux.js.map +1 -1
  148. package/dist/proxy/request-codec.d.ts +1 -41
  149. package/dist/proxy/request-codec.d.ts.map +1 -1
  150. package/dist/proxy/request-codec.js +2 -280
  151. package/dist/proxy/request-codec.js.map +1 -1
  152. package/dist/seller-request-handler.d.ts +10 -3
  153. package/dist/seller-request-handler.d.ts.map +1 -1
  154. package/dist/seller-request-handler.js +112 -17
  155. package/dist/seller-request-handler.js.map +1 -1
  156. package/dist/types/billing.d.ts +2 -0
  157. package/dist/types/billing.d.ts.map +1 -0
  158. package/dist/types/billing.js +2 -0
  159. package/dist/types/billing.js.map +1 -0
  160. package/dist/types/capability.d.ts +1 -25
  161. package/dist/types/capability.d.ts.map +1 -1
  162. package/dist/types/capability.js +2 -1
  163. package/dist/types/capability.js.map +1 -1
  164. package/dist/types/connection.d.ts +1 -26
  165. package/dist/types/connection.d.ts.map +1 -1
  166. package/dist/types/connection.js +2 -10
  167. package/dist/types/connection.js.map +1 -1
  168. package/dist/types/http.d.ts +1 -39
  169. package/dist/types/http.d.ts.map +1 -1
  170. package/dist/types/http.js +2 -16
  171. package/dist/types/http.js.map +1 -1
  172. package/dist/types/index.d.ts +1 -0
  173. package/dist/types/index.d.ts.map +1 -1
  174. package/dist/types/index.js +1 -0
  175. package/dist/types/index.js.map +1 -1
  176. package/dist/types/peer.d.ts +9 -31
  177. package/dist/types/peer.d.ts.map +1 -1
  178. package/dist/types/peer.js +2 -14
  179. package/dist/types/peer.js.map +1 -1
  180. package/dist/types/protocol.d.ts +1 -306
  181. package/dist/types/protocol.d.ts.map +1 -1
  182. package/dist/types/protocol.js +2 -81
  183. package/dist/types/protocol.js.map +1 -1
  184. package/dist/types/service-api.d.ts +1 -5
  185. package/dist/types/service-api.d.ts.map +1 -1
  186. package/dist/types/service-api.js +2 -12
  187. package/dist/types/service-api.js.map +1 -1
  188. package/dist/utils/debug.d.ts +1 -5
  189. package/dist/utils/debug.d.ts.map +1 -1
  190. package/dist/utils/debug.js +2 -52
  191. package/dist/utils/debug.js.map +1 -1
  192. package/dist/utils/hex.d.ts +1 -2
  193. package/dist/utils/hex.d.ts.map +1 -1
  194. package/dist/utils/hex.js +2 -14
  195. package/dist/utils/hex.js.map +1 -1
  196. package/dist/utils/json-codec.d.ts +1 -9
  197. package/dist/utils/json-codec.d.ts.map +1 -1
  198. package/dist/utils/json-codec.js +2 -51
  199. package/dist/utils/json-codec.js.map +1 -1
  200. package/dist/utils/response-usage.d.ts +1 -9
  201. package/dist/utils/response-usage.d.ts.map +1 -1
  202. package/dist/utils/response-usage.js +2 -39
  203. package/dist/utils/response-usage.js.map +1 -1
  204. package/dist/verification/codec.d.ts +1 -3
  205. package/dist/verification/codec.d.ts.map +1 -1
  206. package/dist/verification/codec.js +2 -37
  207. package/dist/verification/codec.js.map +1 -1
  208. package/dist/verification/response-auth.d.ts +1 -30
  209. package/dist/verification/response-auth.d.ts.map +1 -1
  210. package/dist/verification/response-auth.js +2 -110
  211. package/dist/verification/response-auth.js.map +1 -1
  212. package/dist/verification/verification-mux.d.ts +1 -19
  213. package/dist/verification/verification-mux.d.ts.map +1 -1
  214. package/dist/verification/verification-mux.js +2 -99
  215. package/dist/verification/verification-mux.js.map +1 -1
  216. package/package.json +4 -2
@@ -1,951 +1,3 @@
1
- import { PaymentMux } from '../p2p/payment-mux.js';
2
- import { PAYMENT_CODE_CHANNEL_EXHAUSTED, } from '../types/protocol.js';
3
- import { CHANNEL_ROLE, CHANNEL_STATUS } from './channel-store.js';
4
- import { classifyOnChainChannel } from './channel-session-state.js';
5
- import { peerIdToAddress } from '../types/peer.js';
6
- import { debugLog, debugWarn } from '../utils/debug.js';
7
- import { SellerAuthorizationError } from '../discovery/seller-address-resolver.js';
8
- import { parseResponseUsage } from '../utils/response-usage.js';
9
- import { computeCostUsdc } from './pricing.js';
10
- import { formatUsdc } from './usdc-utils.js';
11
- import { parseJsonObject, tryParseJsonObject } from '../utils/json-codec.js';
12
- /** How long to wait for a seller's CloseChannelResult before giving up. */
13
- const CLOSE_REQUEST_TIMEOUT_MS = 60_000;
14
- function parsePaymentRequiredBody(body) {
15
- return tryParseJsonObject(body);
16
- }
17
- function safeBigInt(value) {
18
- try {
19
- return BigInt(value);
20
- }
21
- catch {
22
- return null;
23
- }
24
- }
25
- /**
26
- * Manages all buyer-side payment negotiation state and logic.
27
- *
28
- * Extracted from AntseedNode to separate buyer payment concerns
29
- * (402 handling, SpendingAuth negotiation, per-request auth, cost tracking)
30
- * from core node orchestration.
31
- */
32
- export class BuyerPaymentNegotiator {
33
- _bpm;
34
- _depositsClient;
35
- _channelsClient;
36
- _channelStore;
37
- _freeUsageManager;
38
- _identity;
39
- _emit;
40
- _sellerAddressResolver;
41
- /** Tracks which seller peers the buyer has already negotiated payment for. */
42
- _lockedPeers = new Set();
43
- /** Pending PaymentRequired payloads from sellers, keyed by peerId. */
44
- _pendingPaymentRequired = new Map();
45
- /** Buffered PaymentRequired that arrived before negotiation registered its listener. */
46
- _bufferedPaymentRequired = new Map();
47
- /** Per-peer mutex to prevent concurrent payment negotiations. */
48
- _negotiationLocks = new Map();
49
- /** Peers that have sent their first request after session establishment. */
50
- _firstRequestSent = new Set();
51
- /** Per-peer last response cost, raw content, and latency from the seller. */
52
- _lastResponseCost = new Map();
53
- /** Buyer-side payment muxes keyed by seller peerId. */
54
- _muxes = new Map();
55
- /** In-flight NeedAuth handlers keyed by seller peerId. */
56
- _pendingNeedAuth = new Map();
57
- /** In-flight cooperative-close requests keyed by seller peerId. */
58
- _pendingCloseRequests = new Map();
59
- constructor(identity, bpm, depositsClient, channelsClient, channelStore, _config, emitter, sellerAddressResolver, freeUsageManager) {
60
- this._identity = identity;
61
- this._bpm = bpm;
62
- this._depositsClient = depositsClient;
63
- this._channelsClient = channelsClient;
64
- this._channelStore = channelStore;
65
- this._freeUsageManager = freeUsageManager ?? null;
66
- this._emit = emitter;
67
- this._sellerAddressResolver = sellerAddressResolver;
68
- }
69
- get bpm() {
70
- return this._bpm;
71
- }
72
- async _resolveSellerAddr(peer) {
73
- if (!this._sellerAddressResolver)
74
- return peerIdToAddress(peer.peerId);
75
- try {
76
- return await this._sellerAddressResolver.resolveSellerAddress(peer.peerId, peer.metadata);
77
- }
78
- catch (err) {
79
- if (err instanceof SellerAuthorizationError) {
80
- debugWarn(`[BuyerNegotiator] Dropping peer ${peer.peerId.slice(0, 12)}...: ${err.message}`);
81
- }
82
- throw err;
83
- }
84
- }
85
- getOrCreatePaymentMux(peerId, conn) {
86
- const existing = this._muxes.get(peerId);
87
- if (existing)
88
- return existing;
89
- const pmux = new PaymentMux(conn);
90
- this._muxes.set(peerId, pmux);
91
- pmux.onAuthAck((payload) => {
92
- this._bpm.handleAuthAck(peerId, payload);
93
- });
94
- pmux.onFreeUsageAck((payload) => {
95
- this._freeUsageManager?.handleAck(peerId, payload);
96
- });
97
- pmux.onNeedFreeUsageAuth((payload) => {
98
- const p = this._freeUsageManager?.handleNeedAuth(peerId, payload, pmux);
99
- if (p) {
100
- this._pendingNeedAuth.set(peerId, p);
101
- p.finally(() => {
102
- if (this._pendingNeedAuth.get(peerId) === p)
103
- this._pendingNeedAuth.delete(peerId);
104
- });
105
- }
106
- });
107
- pmux.onNeedAuth((payload) => {
108
- const p = this._bpm.handleNeedAuth(peerId, payload, pmux);
109
- this._pendingNeedAuth.set(peerId, p);
110
- p.finally(() => {
111
- if (this._pendingNeedAuth.get(peerId) === p)
112
- this._pendingNeedAuth.delete(peerId);
113
- });
114
- });
115
- pmux.onCloseChannelResult((payload) => {
116
- const pending = this._pendingCloseRequests.get(peerId);
117
- if (!pending) {
118
- debugWarn(`[BuyerNegotiator] Unsolicited CloseChannelResult from ${peerId.slice(0, 12)}... — ignoring`);
119
- return;
120
- }
121
- if (pending.channelId.toLowerCase() !== payload.channelId.toLowerCase()) {
122
- debugWarn(`[BuyerNegotiator] CloseChannelResult from ${peerId.slice(0, 12)}... is for ` +
123
- `${payload.channelId.slice(0, 18)}..., expected ${pending.channelId.slice(0, 18)}... — ignoring`);
124
- return;
125
- }
126
- clearTimeout(pending.timer);
127
- this._pendingCloseRequests.delete(peerId);
128
- pending.resolve(payload);
129
- });
130
- pmux.onPaymentRequired((payload) => {
131
- const pending = this._pendingPaymentRequired.get(peerId);
132
- if (pending) {
133
- clearTimeout(pending.timer);
134
- this._pendingPaymentRequired.delete(peerId);
135
- pending.resolve(payload);
136
- }
137
- else {
138
- this._bufferedPaymentRequired.set(peerId, payload);
139
- debugLog(`[BuyerNegotiator] PaymentRequired from ${peerId.slice(0, 12)}... buffered`);
140
- }
141
- });
142
- return pmux;
143
- }
144
- getPaymentMux(peerId) {
145
- return this._muxes.get(peerId);
146
- }
147
- trackFreeUsageRequestService(requestId, service) {
148
- this._freeUsageManager?.trackRequestService(requestId, service);
149
- }
150
- async prepareFreeUsageOpen(peer, conn) {
151
- if (!this._freeUsageManager)
152
- return;
153
- const pmux = this.getOrCreatePaymentMux(peer.peerId, conn);
154
- await this._freeUsageManager.prepareOpen(peer, pmux);
155
- void this._freeUsageManager.waitForOpenAck(peer.peerId).catch((err) => {
156
- debugWarn(`[BuyerNegotiator] Free usage open ack unavailable for ${peer.peerId.slice(0, 12)}...: ${err instanceof Error ? err.message : err}`);
157
- });
158
- }
159
- // ── Pre-request auth ────────────────────────────────────────
160
- /**
161
- * If this peer has an active payment session, send a per-request SpendingAuth.
162
- * Handles first-request skip automatically (initial auth already sent during negotiation).
163
- * No-ops if the peer has no established session or if cost data was already
164
- * consumed by sendPostResponseAuth.
165
- */
166
- async preparePreRequestAuth(peer, conn) {
167
- if (!this._lockedPeers.has(peer.peerId))
168
- return;
169
- if (!this._firstRequestSent.has(peer.peerId)) {
170
- this._firstRequestSent.add(peer.peerId);
171
- return;
172
- }
173
- // Skip if cost data was already consumed by post-response auth
174
- if (!this._lastResponseCost.has(peer.peerId))
175
- return;
176
- await this._sendPerRequestAuth(peer.peerId, conn);
177
- }
178
- /**
179
- * Send a SpendingAuth to the seller immediately after receiving a response.
180
- * This ensures the seller always has a valid SpendingAuth for close(),
181
- * even if the buyer disconnects before the next request.
182
- */
183
- async sendPostResponseAuth(peer, conn) {
184
- await this.sendPostResponseAuthTo(peer.peerId, conn);
185
- }
186
- /** peerId-only variant of sendPostResponseAuth. */
187
- async sendPostResponseAuthTo(peerId, conn) {
188
- if (!this._lockedPeers.has(peerId))
189
- return;
190
- if (!this._lastResponseCost.has(peerId))
191
- return;
192
- await this._sendPerRequestAuth(peerId, conn);
193
- }
194
- async _sendPerRequestAuth(peerId, conn) {
195
- const peer = { peerId };
196
- const pmux = this.getOrCreatePaymentMux(peer.peerId, conn);
197
- const lastCost = this._lastResponseCost.get(peer.peerId);
198
- const inputBytes = lastCost?.inputContent ?? new Uint8Array(0);
199
- const outputBytes = lastCost?.outputContent ?? new Uint8Array(0);
200
- const sellerClaimedCost = lastCost?.costUsdc;
201
- const reportedInputTokens = lastCost?.inputTokens;
202
- const reportedOutputTokens = lastCost?.outputTokens;
203
- const reportedCachedInputTokens = lastCost?.cachedInputTokens;
204
- const service = lastCost?.service;
205
- const requestId = lastCost?.requestId;
206
- try {
207
- const { payload, topUpNeeded } = await this._bpm.signPerRequestAuth(peer.peerId, { inputBytes, outputBytes, sellerClaimedCost, reportedInputTokens, reportedOutputTokens, reportedCachedInputTokens, service, requestId });
208
- pmux.sendSpendingAuth(payload);
209
- // Release held content to free memory — no longer needed after signing
210
- this._lastResponseCost.delete(peer.peerId);
211
- debugLog(`[BuyerNegotiator] Per-request SpendingAuth sent to ${peer.peerId.slice(0, 12)}... cumulative=${payload.cumulativeAmount}`);
212
- if (topUpNeeded) {
213
- debugLog(`[BuyerNegotiator] Reserve top-up needed for ${peer.peerId.slice(0, 12)}...`);
214
- await this._bpm.topUpReserve(peer.peerId, pmux);
215
- }
216
- }
217
- catch (err) {
218
- debugWarn(`[BuyerNegotiator] Failed to send per-request SpendingAuth: ${err instanceof Error ? err.message : err}`);
219
- throw err;
220
- }
221
- }
222
- /**
223
- * Ask a seller to close the buyer's payment channel now, bypassing the
224
- * on-chain `requestClose()` → grace period → `withdraw()` flow.
225
- *
226
- * Resolves with the seller's verdict — `status: 'closed'` carries the
227
- * transaction hash. A rejection is a normal outcome (the seller may still be
228
- * serving requests, or be owed for work the buyer hasn't signed for), so it
229
- * resolves rather than throws; only "we never got an answer" throws.
230
- */
231
- async requestChannelClose(peerId, conn, { includeAuth = true, timeoutMs = CLOSE_REQUEST_TIMEOUT_MS } = {}) {
232
- if (this._pendingCloseRequests.has(peerId)) {
233
- throw new Error(`A channel close request is already in flight for ${peerId.slice(0, 12)}...`);
234
- }
235
- const session = this._bpm.getActiveSession(peerId);
236
- if (!session) {
237
- throw new Error(`No active payment channel with seller ${peerId.slice(0, 12)}...`);
238
- }
239
- // Flush any SpendingAuth the buyer still owes for the last response first —
240
- // otherwise the seller rejects with 'pending_auth' on the very first try.
241
- await this.drainPendingNeedAuth();
242
- await this.sendPostResponseAuthTo(peerId, conn).catch((err) => {
243
- debugWarn(`[BuyerNegotiator] Pre-close auth flush failed for ${peerId.slice(0, 12)}...: ${err instanceof Error ? err.message : err}`);
244
- });
245
- const pmux = this.getOrCreatePaymentMux(peerId, conn);
246
- const payload = await this._bpm.buildCloseChannelRequest(peerId, { includeAuth });
247
- const result = await new Promise((resolve, reject) => {
248
- const timer = setTimeout(() => {
249
- this._pendingCloseRequests.delete(peerId);
250
- reject(new Error(`Seller ${peerId.slice(0, 12)}... did not answer the channel close request within ${timeoutMs}ms. ` +
251
- `The channel is unchanged — retry, or fall back to the on-chain request-close flow.`));
252
- }, timeoutMs);
253
- this._pendingCloseRequests.set(peerId, { channelId: payload.channelId, resolve, reject, timer });
254
- try {
255
- pmux.sendCloseChannelRequest(payload);
256
- debugLog(`[BuyerNegotiator] CloseChannelRequest sent to ${peerId.slice(0, 12)}... channel=${payload.channelId.slice(0, 18)}...`);
257
- }
258
- catch (err) {
259
- clearTimeout(timer);
260
- this._pendingCloseRequests.delete(peerId);
261
- reject(err instanceof Error ? err : new Error(String(err)));
262
- }
263
- });
264
- if (result.status === 'closed') {
265
- const finalAmount = result.finalAmount != null ? safeBigInt(result.finalAmount) : null;
266
- this._bpm.retireSession(peerId, CHANNEL_STATUS.SETTLED, finalAmount ?? undefined);
267
- this._lockedPeers.delete(peerId);
268
- this._firstRequestSent.delete(peerId);
269
- this._lastResponseCost.delete(peerId);
270
- debugLog(`[BuyerNegotiator] Seller ${peerId.slice(0, 12)}... closed channel ` +
271
- `${result.channelId.slice(0, 18)}... at ${result.finalAmount ?? '?'} (tx=${result.txHash ?? '?'})`);
272
- this._emit.emit('payment:channel-closed', {
273
- peerId,
274
- channelId: result.channelId,
275
- txHash: result.txHash,
276
- finalAmount: result.finalAmount,
277
- });
278
- }
279
- else {
280
- debugWarn(`[BuyerNegotiator] Seller ${peerId.slice(0, 12)}... declined to close ` +
281
- `${result.channelId.slice(0, 18)}...: ${result.code ?? 'unknown'} — ${result.reason ?? 'no reason given'}`);
282
- }
283
- return result;
284
- }
285
- async handle402(response, peer, conn, req) {
286
- const hadLockedSession = this._lockedPeers.has(peer.peerId);
287
- const directPaymentBody = parsePaymentRequiredBody(response.body);
288
- const responseAlreadyHasRequirements = Boolean(directPaymentBody?.minBudgetPerRequest);
289
- const waitMs = 2_000;
290
- const buffered = responseAlreadyHasRequirements
291
- ? null
292
- : await this._awaitPaymentRequired(peer.peerId, conn, waitMs);
293
- if (buffered)
294
- this._bufferedPaymentRequired.delete(peer.peerId);
295
- const bodyRequirements = responseAlreadyHasRequirements && directPaymentBody != null
296
- ? {
297
- minBudgetPerRequest: String(directPaymentBody.minBudgetPerRequest ?? '10000'),
298
- suggestedAmount: String(directPaymentBody.suggestedAmount ?? '100000'),
299
- requestId: req.requestId,
300
- ...(directPaymentBody.inputUsdPerMillion != null ? { inputUsdPerMillion: Number(directPaymentBody.inputUsdPerMillion) } : {}),
301
- ...(directPaymentBody.outputUsdPerMillion != null ? { outputUsdPerMillion: Number(directPaymentBody.outputUsdPerMillion) } : {}),
302
- ...(directPaymentBody.cachedInputUsdPerMillion != null ? { cachedInputUsdPerMillion: Number(directPaymentBody.cachedInputUsdPerMillion) } : {}),
303
- }
304
- : null;
305
- const paymentRequirements = buffered ?? bodyRequirements;
306
- const requestedReserveAmount = (() => {
307
- if (!paymentRequirements)
308
- return null;
309
- const suggested = safeBigInt(paymentRequirements.suggestedAmount);
310
- if (suggested == null || suggested <= 0n)
311
- return null;
312
- return suggested > this._bpm.maxReserveAmountUsdc ? this._bpm.maxReserveAmountUsdc : suggested;
313
- })();
314
- const returnPaymentRequired = (code, debugReason) => {
315
- debugLog(`[BuyerNegotiator] Got 402 from ${peer.peerId.slice(0, 12)}... — returning to caller (${debugReason})`);
316
- // Always return a normalized buyer-authored body so the desktop UI can
317
- // render a proper "top up your deposits" CTA instead of the raw seller
318
- // 402 JSON (which leaks internal fields like requiredCumulativeAmount,
319
- // currentSpent, and per-service pricing).
320
- const req = buffered
321
- ?? (responseAlreadyHasRequirements && directPaymentBody != null
322
- ? {
323
- minBudgetPerRequest: directPaymentBody.minBudgetPerRequest != null
324
- ? String(directPaymentBody.minBudgetPerRequest)
325
- : undefined,
326
- suggestedAmount: directPaymentBody.suggestedAmount != null
327
- ? String(directPaymentBody.suggestedAmount)
328
- : undefined,
329
- }
330
- : null);
331
- const enrichedBody = JSON.stringify({
332
- error: 'payment_required',
333
- code,
334
- peerId: peer.peerId,
335
- ...(req?.minBudgetPerRequest != null ? { minBudgetPerRequest: req.minBudgetPerRequest } : {}),
336
- ...(req?.suggestedAmount != null ? { suggestedAmount: req.suggestedAmount } : {}),
337
- message: 'You are out of AntSeed credits for this request. Add funds in the AntSeed app, or run "antseed buyer deposit <amount>" from the CLI, then retry.',
338
- });
339
- return {
340
- action: 'return',
341
- response: {
342
- ...response,
343
- headers: { ...response.headers, 'content-type': 'application/json' },
344
- body: new TextEncoder().encode(enrichedBody),
345
- },
346
- };
347
- };
348
- const returnNegotiationFailure = (reason, message, statusCode = 409) => {
349
- debugWarn(`[BuyerNegotiator] Auto-negotiation failed for ${peer.peerId.slice(0, 12)}... — ` +
350
- `${reason}; returning non-payment error to caller`);
351
- return {
352
- action: 'return',
353
- response: {
354
- ...response,
355
- statusCode,
356
- headers: { ...response.headers, 'content-type': 'application/json' },
357
- body: new TextEncoder().encode(JSON.stringify({
358
- error: 'payment_negotiation_failed',
359
- reason,
360
- message,
361
- })),
362
- },
363
- };
364
- };
365
- // Reconcile any active stored session before opening a fresh reserve.
366
- const existingSessionBudgetRequest = buffered
367
- ? BigInt(buffered.minBudgetPerRequest)
368
- : responseAlreadyHasRequirements && directPaymentBody?.minBudgetPerRequest != null
369
- ? BigInt(String(directPaymentBody.minBudgetPerRequest))
370
- : null;
371
- // Absolute cumulative target the seller needs us to sign to unblock the
372
- // channel (sent by the seller in the budget-exhausted branch). When
373
- // present we pass it through to extendCurrentSpendingAuth so the buyer
374
- // catches up in one step instead of creeping by minBudgetPerRequest and
375
- // repeatedly being rejected as underfunded.
376
- const requiredCumulativeTarget = buffered?.requiredCumulativeAmount != null
377
- ? safeBigInt(buffered.requiredCumulativeAmount)
378
- : responseAlreadyHasRequirements && directPaymentBody?.requiredCumulativeAmount != null
379
- ? safeBigInt(String(directPaymentBody.requiredCumulativeAmount))
380
- : null;
381
- // On-chain reserve ceiling the seller is willing to accept. Used to detect
382
- // permanently exhausted channels (where requiredCumulativeAmount > reserveMaxAmount)
383
- // so the buyer doesn't loop signing higher cumulatives the seller will reject.
384
- const sellerReserveMax = buffered?.reserveMaxAmount != null
385
- ? safeBigInt(buffered.reserveMaxAmount)
386
- : responseAlreadyHasRequirements && directPaymentBody?.reserveMaxAmount != null
387
- ? safeBigInt(String(directPaymentBody.reserveMaxAmount))
388
- : null;
389
- const channelExhausted = buffered?.code === PAYMENT_CODE_CHANNEL_EXHAUSTED
390
- || (responseAlreadyHasRequirements && directPaymentBody?.code === PAYMENT_CODE_CHANNEL_EXHAUSTED)
391
- || (requiredCumulativeTarget != null && sellerReserveMax != null && requiredCumulativeTarget > sellerReserveMax);
392
- const hasActiveSession = hadLockedSession || this._bpm.getActiveSession(peer.peerId) != null;
393
- if (channelExhausted && hasActiveSession) {
394
- debugLog(`[BuyerNegotiator] Channel exhausted for ${peer.peerId.slice(0, 12)}... ` +
395
- `(required=${requiredCumulativeTarget ?? 'n/a'} > reserveMax=${sellerReserveMax ?? 'n/a'}) — ` +
396
- `retiring session and renegotiating a fresh reserve`);
397
- // 'ghost' rather than 'settled' — buyer hasn't observed the on-chain
398
- // settle land. Matches the other buyer-side give-up-locally callsites.
399
- this._bpm.retireSession(peer.peerId, CHANNEL_STATUS.GHOST);
400
- this._lockedPeers.delete(peer.peerId);
401
- this._firstRequestSent.delete(peer.peerId);
402
- }
403
- else if (hasActiveSession) {
404
- const recovered = await this._recoverExistingSession(peer, conn, existingSessionBudgetRequest, requiredCumulativeTarget, requiredCumulativeTarget == null);
405
- if (recovered) {
406
- return { action: 'retry' };
407
- }
408
- if (this._bpm.getActiveSession(peer.peerId)) {
409
- if (await this._canRetireStaleSessionWithoutOnChainProof()) {
410
- this._bpm.retireSession(peer.peerId, CHANNEL_STATUS.GHOST);
411
- }
412
- }
413
- if (this._bpm.getActiveSession(peer.peerId)) {
414
- return returnNegotiationFailure('existing_channel_still_active', 'An existing payment channel could not be recovered automatically. Close or recover the channel and retry.');
415
- }
416
- this._lockedPeers.delete(peer.peerId);
417
- this._firstRequestSent.delete(peer.peerId);
418
- }
419
- // Check if we can pay before attempting negotiation
420
- if (!this._depositsClient) {
421
- return returnNegotiationFailure('deposits_not_configured', 'Buyer deposits are not configured, so automatic payment negotiation is unavailable.', 503);
422
- }
423
- // Check on-chain balance before sending ReserveAuth. The seller locks the full
424
- // reserve ceiling on-chain, so a positive-but-too-small available balance would
425
- // otherwise make reserve()/topUp() revert with InsufficientBalance and trigger a
426
- // noisy 402/auth-rejection retry loop.
427
- try {
428
- const buyerAddr = this._identity.wallet.address;
429
- const balance = await this._depositsClient.getBuyerBalance(buyerAddr);
430
- if (balance.available <= 0n) {
431
- return returnPaymentRequired('insufficient_deposits', 'buyer deposits balance is zero');
432
- }
433
- if (requestedReserveAmount != null && balance.available < requestedReserveAmount) {
434
- return returnPaymentRequired('insufficient_deposits', `buyer available deposits ${balance.available} are below requested reserve ${requestedReserveAmount}`);
435
- }
436
- }
437
- catch (err) {
438
- debugWarn(`[BuyerNegotiator] Failed to check buyer balance: ${err instanceof Error ? err.message : err}`);
439
- }
440
- // Re-buffer the PaymentRequired so _doNegotiatePayment can consume it
441
- if (buffered) {
442
- this._bufferedPaymentRequired.set(peer.peerId, buffered);
443
- }
444
- else if (bodyRequirements) {
445
- this._bufferedPaymentRequired.set(peer.peerId, bodyRequirements);
446
- }
447
- debugLog(`[BuyerNegotiator] Got 402 from ${peer.peerId.slice(0, 12)}... — auto-negotiating payment`);
448
- try {
449
- await this._negotiatePayment(peer, conn);
450
- debugLog(`[BuyerNegotiator] Payment negotiated with ${peer.peerId.slice(0, 12)}...`);
451
- return { action: 'retry' };
452
- }
453
- catch (err) {
454
- this._lockedPeers.delete(peer.peerId);
455
- throw err;
456
- }
457
- }
458
- estimateCostFromResponse(peer, response, service, requestId) {
459
- // Prefer session pricing (from PaymentRequired negotiation, includes service-specific rates)
460
- // over peer-level defaults which may be different from the actual service pricing.
461
- const sessionPricing = this._bpm.getSessionPricing(peer.peerId, service);
462
- const inputPricePerM = sessionPricing?.inputUsdPerMillion ?? peer.defaultInputUsdPerMillion;
463
- const outputPricePerM = sessionPricing?.outputUsdPerMillion ?? peer.defaultOutputUsdPerMillion;
464
- if (inputPricePerM == null && outputPricePerM == null)
465
- return;
466
- const usage = parseResponseUsage(response.body);
467
- // Don't estimate from body bytes — seller cost headers are authoritative.
468
- // The old body.length/4 fallback wildly inflated costs for SSE streaming responses.
469
- const pricing = {
470
- inputUsdPerMillion: inputPricePerM ?? 0,
471
- outputUsdPerMillion: outputPricePerM ?? 0,
472
- cachedInputUsdPerMillion: sessionPricing?.cachedInputUsdPerMillion,
473
- };
474
- const costUsdc = computeCostUsdc(usage.freshInputTokens, usage.outputTokens, pricing, usage.cachedInputTokens);
475
- this._lastResponseCost.set(peer.peerId, {
476
- costUsdc,
477
- inputTokens: BigInt(usage.inputTokens),
478
- outputTokens: BigInt(usage.outputTokens),
479
- cachedInputTokens: BigInt(usage.cachedInputTokens),
480
- cumulativeCost: 0n,
481
- inputContent: new Uint8Array(0),
482
- outputContent: response.body,
483
- latencyMs: 0,
484
- service,
485
- requestId,
486
- });
487
- debugLog(`[BuyerNegotiator] Estimated cost for ${peer.peerId.slice(0, 12)}...: ` +
488
- `cost=${costUsdc} (in=${usage.freshInputTokens} cached=${usage.cachedInputTokens} out=${usage.outputTokens})`);
489
- this._bpm.recordAndPersistTokens(peer.peerId, usage.inputTokens, usage.outputTokens);
490
- }
491
- // parseCostHeaders removed — cost data now flows through NeedAuth on PaymentMux.
492
- recordResponseContent(peerId, reqBody, resBody, latencyMs) {
493
- debugLog(`[BuyerNegotiator] recordResponseContent: reqBody=${reqBody.length}B resBody=${resBody.length}B latency=${latencyMs}ms`);
494
- const existing = this._lastResponseCost.get(peerId);
495
- if (existing) {
496
- this._lastResponseCost.set(peerId, {
497
- ...existing,
498
- inputContent: reqBody,
499
- outputContent: resBody,
500
- latencyMs,
501
- });
502
- }
503
- }
504
- async applyExternalSpendingAuth(peer, conn, headerValue) {
505
- const pmux = this.getOrCreatePaymentMux(peer.peerId, conn);
506
- let payload;
507
- try {
508
- const decoded = Buffer.from(headerValue, 'base64').toString('utf-8');
509
- payload = parseJsonObject(decoded);
510
- }
511
- catch {
512
- throw new Error('Invalid x-antseed-spending-auth header: failed to decode');
513
- }
514
- debugLog(`[BuyerNegotiator] External SpendingAuth: channel=${payload.channelId.slice(0, 18)}... amount=${payload.cumulativeAmount}`);
515
- const sellerEvmAddrExternal = await this._resolveSellerAddr(peer);
516
- // Store session so handleAuthAck can find it
517
- if (this._channelStore) {
518
- const reserveDeadline = payload.reserveDeadline ?? (Math.floor(Date.now() / 1000) + 3600);
519
- this._channelStore.upsertChannel({
520
- sessionId: payload.channelId,
521
- peerId: peer.peerId,
522
- role: CHANNEL_ROLE.BUYER,
523
- sellerEvmAddr: sellerEvmAddrExternal,
524
- buyerEvmAddr: this._identity.wallet.address,
525
- nonce: 0,
526
- authMax: payload.cumulativeAmount,
527
- deadline: reserveDeadline,
528
- previousSessionId: '0x' + '0'.repeat(64),
529
- previousConsumption: '0',
530
- tokensDelivered: '0',
531
- requestCount: 0,
532
- reservedAt: Date.now(),
533
- settledAt: null,
534
- settledAmount: null,
535
- status: CHANNEL_STATUS.ACTIVE,
536
- latestBuyerSig: null,
537
- latestSpendingAuthSig: null,
538
- latestMetadata: null,
539
- createdAt: Date.now(),
540
- updatedAt: Date.now(),
541
- });
542
- }
543
- pmux.sendSpendingAuth(payload);
544
- debugLog(`[BuyerNegotiator] External SpendingAuth sent to seller ${peer.peerId.slice(0, 12)}..., waiting for AuthAck...`);
545
- let reserveCtxAmount;
546
- try {
547
- reserveCtxAmount = payload.reserveMaxAmount != null
548
- ? BigInt(payload.reserveMaxAmount)
549
- : BigInt(payload.cumulativeAmount);
550
- }
551
- catch {
552
- reserveCtxAmount = undefined;
553
- }
554
- await this._waitForLockConfirmation(peer.peerId, { requestedReserve: reserveCtxAmount });
555
- debugLog(`[BuyerNegotiator] AuthAck received from seller ${peer.peerId.slice(0, 12)}...`);
556
- this._lockedPeers.add(peer.peerId);
557
- this._emit.emit('payment:signed', {
558
- peerId: peer.peerId,
559
- sellerEvmAddr: sellerEvmAddrExternal,
560
- amount: payload.cumulativeAmount,
561
- });
562
- }
563
- onPeerDisconnect(peerId) {
564
- this._muxes.delete(peerId);
565
- this._bufferedPaymentRequired.delete(peerId);
566
- // Cancel any in-flight PaymentRequired wait
567
- const pendingPR = this._pendingPaymentRequired.get(peerId);
568
- if (pendingPR) {
569
- clearTimeout(pendingPR.timer);
570
- this._pendingPaymentRequired.delete(peerId);
571
- pendingPR.reject(new Error(`Peer ${peerId.slice(0, 12)}... disconnected during payment negotiation`));
572
- }
573
- const pendingClose = this._pendingCloseRequests.get(peerId);
574
- if (pendingClose) {
575
- clearTimeout(pendingClose.timer);
576
- this._pendingCloseRequests.delete(peerId);
577
- pendingClose.reject(new Error(`Peer ${peerId.slice(0, 12)}... disconnected before answering the channel close request`));
578
- }
579
- this._lockedPeers.delete(peerId);
580
- this._firstRequestSent.delete(peerId);
581
- this._lastResponseCost.delete(peerId);
582
- }
583
- /** Wait for in-flight NeedAuth handlers to complete (settlement safety). */
584
- async drainPendingNeedAuth() {
585
- const pending = [...this._pendingNeedAuth.values()];
586
- if (pending.length > 0) {
587
- await Promise.allSettled(pending);
588
- }
589
- }
590
- cleanup() {
591
- this._lockedPeers.clear();
592
- this._firstRequestSent.clear();
593
- this._lastResponseCost.clear();
594
- this._muxes.clear();
595
- for (const [, pending] of this._pendingPaymentRequired) {
596
- clearTimeout(pending.timer);
597
- pending.reject(new Error('Node stopped'));
598
- }
599
- this._pendingPaymentRequired.clear();
600
- this._bufferedPaymentRequired.clear();
601
- this._negotiationLocks.clear();
602
- for (const [, pending] of this._pendingCloseRequests) {
603
- clearTimeout(pending.timer);
604
- pending.reject(new Error('Node stopped'));
605
- }
606
- this._pendingCloseRequests.clear();
607
- }
608
- /**
609
- * Build the full pricing map from peer metadata (defaults + all per-service overrides).
610
- * This ensures the buyer knows pricing for every service the seller offers,
611
- * not just the one that triggered the 402.
612
- */
613
- _buildPricingMap(peer) {
614
- const defaults = {
615
- inputUsdPerMillion: peer.defaultInputUsdPerMillion ?? 0,
616
- outputUsdPerMillion: peer.defaultOutputUsdPerMillion ?? 0,
617
- cachedInputUsdPerMillion: peer.defaultCachedInputUsdPerMillion,
618
- };
619
- if (defaults.inputUsdPerMillion === 0 && defaults.outputUsdPerMillion === 0 && !peer.providerPricing) {
620
- return undefined;
621
- }
622
- const services = {};
623
- if (peer.providerPricing) {
624
- for (const entry of Object.values(peer.providerPricing)) {
625
- if (entry.services) {
626
- for (const [serviceName, sp] of Object.entries(entry.services)) {
627
- services[serviceName] = {
628
- inputUsdPerMillion: sp.inputUsdPerMillion,
629
- outputUsdPerMillion: sp.outputUsdPerMillion,
630
- cachedInputUsdPerMillion: sp.cachedInputUsdPerMillion,
631
- };
632
- }
633
- }
634
- }
635
- }
636
- return { defaults, services };
637
- }
638
- async _negotiatePayment(peer, conn) {
639
- // Per-peer mutex: if another request is already negotiating, wait for it
640
- const existing = this._negotiationLocks.get(peer.peerId);
641
- if (existing) {
642
- await existing;
643
- return;
644
- }
645
- const negotiation = this._doNegotiatePayment(peer, conn);
646
- this._negotiationLocks.set(peer.peerId, negotiation);
647
- try {
648
- await negotiation;
649
- }
650
- finally {
651
- this._negotiationLocks.delete(peer.peerId);
652
- }
653
- }
654
- async _doNegotiatePayment(peer, conn) {
655
- if (this._lockedPeers.has(peer.peerId))
656
- return;
657
- const pmux = this.getOrCreatePaymentMux(peer.peerId, conn);
658
- // Check if PaymentRequired was already buffered
659
- const buffered = this._bufferedPaymentRequired.get(peer.peerId);
660
- if (buffered) {
661
- this._bufferedPaymentRequired.delete(peer.peerId);
662
- debugLog(`[BuyerNegotiator] Using buffered PaymentRequired from ${peer.peerId.slice(0, 12)}...`);
663
- }
664
- const PAYMENT_REQUIRED_TIMEOUT_MS = 10_000;
665
- const requirements = buffered ?? await new Promise((resolve, reject) => {
666
- const timer = setTimeout(() => {
667
- this._pendingPaymentRequired.delete(peer.peerId);
668
- reject(new Error(`PaymentRequired timeout from seller ${peer.peerId.slice(0, 12)}...`));
669
- }, PAYMENT_REQUIRED_TIMEOUT_MS);
670
- this._pendingPaymentRequired.set(peer.peerId, { resolve, reject, timer });
671
- });
672
- debugLog(`[BuyerNegotiator] PaymentRequired from ${peer.peerId.slice(0, 12)}...: minBudgetPerRequest=${requirements.minBudgetPerRequest} suggested=${requirements.suggestedAmount}`);
673
- // Validate seller's per-request minimum
674
- const minBudgetPerRequest = BigInt(requirements.minBudgetPerRequest);
675
- if (minBudgetPerRequest > this._bpm.maxPerRequestUsdc) {
676
- throw new Error(`Seller ${peer.peerId.slice(0, 12)}... minBudgetPerRequest=${minBudgetPerRequest} exceeds buyer maxPerRequestUsdc=${this._bpm.maxPerRequestUsdc}`);
677
- }
678
- // Cap amount at buyer's maxReserveAmountUsdc
679
- let amount;
680
- try {
681
- amount = BigInt(requirements.suggestedAmount);
682
- }
683
- catch {
684
- throw new Error(`Invalid suggestedAmount from seller ${peer.peerId.slice(0, 12)}...: "${requirements.suggestedAmount}"`);
685
- }
686
- if (amount > this._bpm.maxReserveAmountUsdc) {
687
- amount = this._bpm.maxReserveAmountUsdc;
688
- }
689
- if (amount <= 0n) {
690
- throw new Error(`Invalid reserve amount for payment to ${peer.peerId.slice(0, 12)}...`);
691
- }
692
- const sellerEvmAddr = await this._resolveSellerAddr(peer);
693
- this._emit.emit('payment:required', {
694
- peerId: peer.peerId,
695
- sellerEvmAddr,
696
- minBudgetPerRequest: requirements.minBudgetPerRequest,
697
- suggestedAmount: amount.toString(),
698
- });
699
- // Extract pricing from seller's PaymentRequired or peer metadata
700
- const pricing = (requirements.inputUsdPerMillion != null || requirements.outputUsdPerMillion != null)
701
- ? {
702
- inputUsdPerMillion: requirements.inputUsdPerMillion ?? peer.defaultInputUsdPerMillion ?? 0,
703
- outputUsdPerMillion: requirements.outputUsdPerMillion ?? peer.defaultOutputUsdPerMillion ?? 0,
704
- cachedInputUsdPerMillion: requirements.cachedInputUsdPerMillion ?? peer.defaultCachedInputUsdPerMillion,
705
- }
706
- : (peer.defaultInputUsdPerMillion != null || peer.defaultOutputUsdPerMillion != null)
707
- ? {
708
- inputUsdPerMillion: peer.defaultInputUsdPerMillion ?? 0,
709
- outputUsdPerMillion: peer.defaultOutputUsdPerMillion ?? 0,
710
- cachedInputUsdPerMillion: peer.defaultCachedInputUsdPerMillion,
711
- }
712
- : undefined;
713
- // Build full pricing map from peer metadata (defaults + all per-service overrides)
714
- const pricingMap = this._buildPricingMap(peer);
715
- try {
716
- await this._bpm.authorizeSpending(peer.peerId, pmux, minBudgetPerRequest, amount, pricing, pricingMap, peer.metadata);
717
- debugLog(`[BuyerNegotiator] SpendingAuth sent to seller ${peer.peerId.slice(0, 12)}..., waiting for AuthAck...`);
718
- await this._waitForLockConfirmation(peer.peerId, { requestedReserve: amount, minBudgetPerRequest });
719
- debugLog(`[BuyerNegotiator] AuthAck received from seller ${peer.peerId.slice(0, 12)}...`);
720
- this._lockedPeers.add(peer.peerId);
721
- this._emit.emit('payment:signed', {
722
- peerId: peer.peerId,
723
- sellerEvmAddr,
724
- amount: amount.toString(),
725
- });
726
- }
727
- catch (err) {
728
- debugWarn(`[BuyerNegotiator] Payment negotiation failed for ${peer.peerId.slice(0, 12)}...: ${err instanceof Error ? err.message : err}`);
729
- throw err;
730
- }
731
- }
732
- async _awaitPaymentRequired(peerId, conn, timeoutMs) {
733
- const buffered = this._bufferedPaymentRequired.get(peerId);
734
- if (buffered)
735
- return buffered;
736
- // Ensure the buyer-side PaymentMux exists before waiting
737
- this.getOrCreatePaymentMux(peerId, conn);
738
- return await new Promise((resolve) => {
739
- const already = this._bufferedPaymentRequired.get(peerId);
740
- if (already) {
741
- resolve(already);
742
- return;
743
- }
744
- const existing = this._pendingPaymentRequired.get(peerId);
745
- if (existing) {
746
- const wrapper = {
747
- resolve: (payload) => {
748
- clearTimeout(existing.timer);
749
- clearTimeout(wrapper.timer);
750
- if (this._pendingPaymentRequired.get(peerId) === wrapper) {
751
- this._pendingPaymentRequired.delete(peerId);
752
- }
753
- existing.resolve(payload);
754
- resolve(payload);
755
- },
756
- reject: (err) => {
757
- clearTimeout(existing.timer);
758
- clearTimeout(wrapper.timer);
759
- if (this._pendingPaymentRequired.get(peerId) === wrapper) {
760
- this._pendingPaymentRequired.delete(peerId);
761
- }
762
- existing.reject(err);
763
- resolve(null);
764
- },
765
- timer: setTimeout(() => {
766
- clearTimeout(existing.timer);
767
- if (this._pendingPaymentRequired.get(peerId) === wrapper) {
768
- this._pendingPaymentRequired.delete(peerId);
769
- }
770
- resolve(null);
771
- }, timeoutMs),
772
- };
773
- this._pendingPaymentRequired.set(peerId, wrapper);
774
- return;
775
- }
776
- const timer = setTimeout(() => {
777
- if (this._pendingPaymentRequired.get(peerId)?.timer === timer) {
778
- this._pendingPaymentRequired.delete(peerId);
779
- }
780
- resolve(null);
781
- }, timeoutMs);
782
- this._pendingPaymentRequired.set(peerId, {
783
- resolve: (payload) => {
784
- clearTimeout(timer);
785
- if (this._pendingPaymentRequired.get(peerId)?.timer === timer) {
786
- this._pendingPaymentRequired.delete(peerId);
787
- }
788
- resolve(payload);
789
- },
790
- reject: () => {
791
- clearTimeout(timer);
792
- if (this._pendingPaymentRequired.get(peerId)?.timer === timer) {
793
- this._pendingPaymentRequired.delete(peerId);
794
- }
795
- resolve(null);
796
- },
797
- timer,
798
- });
799
- });
800
- }
801
- async _recoverExistingSession(peer, conn, minBudgetPerRequest = null, targetCumulative = null, requireFreshAck = false) {
802
- const session = this._bpm.getActiveSession(peer.peerId);
803
- if (!session) {
804
- return false;
805
- }
806
- if (!this._channelsClient) {
807
- this._bpm.retireSession(peer.peerId, CHANNEL_STATUS.GHOST);
808
- return false;
809
- }
810
- const onChain = await this._getOnChainSessionState(session.sessionId);
811
- if (onChain === null) {
812
- return false;
813
- }
814
- if (!onChain.exists) {
815
- if (this._bpm.canReplayReserveAuth(peer.peerId)) {
816
- const pmux = this.getOrCreatePaymentMux(peer.peerId, conn);
817
- await this._bpm.resendReserveAuth(peer.peerId, pmux);
818
- await this._waitForLockConfirmation(peer.peerId, {
819
- minBudgetPerRequest: minBudgetPerRequest ?? undefined,
820
- });
821
- this._lockedPeers.add(peer.peerId);
822
- return true;
823
- }
824
- this._bpm.retireSession(peer.peerId, CHANNEL_STATUS.GHOST);
825
- return false;
826
- }
827
- if (onChain.status === CHANNEL_STATUS.SETTLED) {
828
- this._bpm.retireSession(peer.peerId, CHANNEL_STATUS.SETTLED, onChain.channel.settled);
829
- return false;
830
- }
831
- if (onChain.status === CHANNEL_STATUS.TIMEOUT) {
832
- this._bpm.retireSession(peer.peerId, CHANNEL_STATUS.TIMEOUT);
833
- return false;
834
- }
835
- if (onChain.status !== 'active') {
836
- this._bpm.retireSession(peer.peerId, CHANNEL_STATUS.GHOST);
837
- return false;
838
- }
839
- const pmux = this.getOrCreatePaymentMux(peer.peerId, conn);
840
- if (requireFreshAck) {
841
- // A 402 with only base PaymentRequired fields while the buyer has an
842
- // active local session means the seller does not currently recognize
843
- // the channel (for example after a seller restart). The previous
844
- // AuthAck is stale in that case; require a fresh one before retrying so
845
- // we do not immediately replay the request into another 402.
846
- this._bpm.clearLockConfirmation(peer.peerId);
847
- }
848
- if (minBudgetPerRequest != null && minBudgetPerRequest > 0n) {
849
- const cumulativeBefore = this._bpm.getCumulativeAmount(peer.peerId);
850
- await this._bpm.extendCurrentSpendingAuth(peer.peerId, minBudgetPerRequest, pmux, targetCumulative != null && targetCumulative > 0n ? targetCumulative : undefined);
851
- const cumulativeAfter = this._bpm.getCumulativeAmount(peer.peerId);
852
- if (cumulativeAfter <= cumulativeBefore) {
853
- // extendCurrentSpendingAuth was a no-op — the overdraft window and reserve ceiling
854
- // are both wedged. Retire the session so the caller negotiates a fresh channel
855
- // instead of spinning in an infinite 402 retry loop.
856
- debugWarn(`[BuyerNegotiator] extendCurrentSpendingAuth made no progress for ${peer.peerId.slice(0, 12)}... ` +
857
- `(cumulative=${cumulativeAfter}); retiring session`);
858
- this._bpm.retireSession(peer.peerId, CHANNEL_STATUS.GHOST);
859
- this._lockedPeers.delete(peer.peerId);
860
- this._firstRequestSent.delete(peer.peerId);
861
- return false;
862
- }
863
- }
864
- else {
865
- await this._bpm.resendCurrentSpendingAuth(peer.peerId, pmux);
866
- }
867
- await this._waitForLockConfirmation(peer.peerId, {
868
- minBudgetPerRequest: minBudgetPerRequest ?? undefined,
869
- });
870
- this._lockedPeers.add(peer.peerId);
871
- return true;
872
- }
873
- async _canRetireStaleSessionWithoutOnChainProof() {
874
- const depositsClient = this._depositsClient;
875
- if (!depositsClient) {
876
- return false;
877
- }
878
- try {
879
- const balance = await depositsClient.getBuyerBalance(this._identity.wallet.address);
880
- return balance.reserved === 0n;
881
- }
882
- catch (err) {
883
- debugWarn(`[BuyerNegotiator] Failed to verify buyer reserved balance while checking stale session: ` +
884
- `${err instanceof Error ? err.message : err}`);
885
- return false;
886
- }
887
- }
888
- async _getOnChainSessionState(channelId) {
889
- try {
890
- const channelsClient = this._channelsClient;
891
- if (!channelsClient) {
892
- return null;
893
- }
894
- return classifyOnChainChannel(await channelsClient.getSession(channelId));
895
- }
896
- catch (err) {
897
- debugWarn(`[BuyerNegotiator] Failed to load on-chain channel ${channelId.slice(0, 18)}...: ${err instanceof Error ? err.message : err}`);
898
- return null;
899
- }
900
- }
901
- async _waitForLockConfirmation(sellerPeerId, ctx) {
902
- const pollIntervalMs = 200;
903
- const timeoutMs = 30_000;
904
- const deadline = Date.now() + timeoutMs;
905
- while (Date.now() < deadline) {
906
- if (this._bpm.isLockConfirmed(sellerPeerId))
907
- return;
908
- if (this._bpm.isLockRejected(sellerPeerId)) {
909
- throw new Error(`Lock rejected by seller ${sellerPeerId.slice(0, 12)}...${this._buildLockFailureHint(ctx)}`);
910
- }
911
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
912
- }
913
- throw new Error(`Lock confirmation timed out for seller ${sellerPeerId.slice(0, 12)}... (${timeoutMs}ms).${this._buildLockFailureHint(ctx)}`);
914
- }
915
- /**
916
- * Build a human-readable hint appended to lock-confirmation failures,
917
- * listing the most likely causes so operators don't have to guess.
918
- *
919
- * Note: this intentionally does NOT call the buyer's RPC. The buyer's RPC
920
- * is typically less reliable than the seller's, and the seller is the one
921
- * making the on-chain call that is failing — it has the exact revert reason
922
- * and a fresh deposits snapshot. A follow-up will plumb a structured
923
- * AuthNack frame from the seller carrying that information; this hint
924
- * exists so the surrounding error shape is already in place when that
925
- * lands.
926
- */
927
- _buildLockFailureHint(ctx) {
928
- const lines = [];
929
- // We don't know the buyer's live balance here — only what we asked for.
930
- // The binding on-chain constraint is lockForChannel(buyer, maxAmount) in
931
- // reserve() / topUp(); settle()/close() cannot throw InsufficientBalance
932
- // because their charges come from already-locked reserve. So the ceiling
933
- // we need the buyer to have covered is requestedReserve.
934
- const reserve = ctx?.requestedReserve;
935
- const min = ctx?.minBudgetPerRequest;
936
- if (reserve != null && reserve > 0n) {
937
- lines.push(`- Deposits may be below the reserve ceiling the seller will lock ` +
938
- `(${formatUsdc(reserve)} USDC). The seller's on-chain reserve() reverts ` +
939
- `with InsufficientBalance before AuthAck. ` +
940
- `Check with "antseed buyer status"; top up with "antseed buyer deposit <amount>".`);
941
- }
942
- else if (min != null && min > 0n) {
943
- lines.push(`- Deposits may be below seller minBudgetPerRequest (${formatUsdc(min)} USDC). ` +
944
- `Check with "antseed buyer status"; top up with "antseed buyer deposit <amount>".`);
945
- }
946
- lines.push('- Seller may be overloaded or gated on subscription (try another peer).');
947
- lines.push('- Seller may be online but the payment process stalled (retry, or pin a different peer).');
948
- return `\nLikely causes:\n ${lines.join('\n ')}`;
949
- }
950
- }
1
+ // Moved to @antseed/buyer-core; re-exported for compatibility.
2
+ export * from '@antseed/buyer-core/buyer-payment-negotiator';
951
3
  //# sourceMappingURL=buyer-payment-negotiator.js.map