@antseed/node 0.2.105 → 0.2.107
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.
- package/dist/billing/index.d.ts +2 -0
- package/dist/billing/index.d.ts.map +1 -0
- package/dist/billing/index.js +2 -0
- package/dist/billing/index.js.map +1 -0
- package/dist/billing/unit.d.ts +3 -0
- package/dist/billing/unit.d.ts.map +1 -0
- package/dist/billing/unit.js +3 -0
- package/dist/billing/unit.js.map +1 -0
- package/dist/buyer-request-handler.d.ts +1 -54
- package/dist/buyer-request-handler.d.ts.map +1 -1
- package/dist/buyer-request-handler.js +2 -379
- package/dist/buyer-request-handler.js.map +1 -1
- package/dist/discovery/announcer.d.ts +7 -1
- package/dist/discovery/announcer.d.ts.map +1 -1
- package/dist/discovery/announcer.js +84 -33
- package/dist/discovery/announcer.js.map +1 -1
- package/dist/discovery/http-metadata-resolver.d.ts +4 -0
- package/dist/discovery/http-metadata-resolver.d.ts.map +1 -1
- package/dist/discovery/http-metadata-resolver.js +42 -2
- package/dist/discovery/http-metadata-resolver.js.map +1 -1
- package/dist/discovery/index.d.ts +1 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/index.js +1 -1
- package/dist/discovery/index.js.map +1 -1
- package/dist/discovery/metadata-codec.d.ts +4 -4
- package/dist/discovery/metadata-codec.d.ts.map +1 -1
- package/dist/discovery/metadata-codec.js +339 -23
- package/dist/discovery/metadata-codec.js.map +1 -1
- package/dist/discovery/metadata-validator.d.ts +7 -3
- package/dist/discovery/metadata-validator.d.ts.map +1 -1
- package/dist/discovery/metadata-validator.js +139 -8
- package/dist/discovery/metadata-validator.js.map +1 -1
- package/dist/discovery/peer-lookup.d.ts +1 -0
- package/dist/discovery/peer-lookup.d.ts.map +1 -1
- package/dist/discovery/peer-lookup.js +23 -5
- package/dist/discovery/peer-lookup.js.map +1 -1
- package/dist/discovery/peer-metadata.d.ts +2 -90
- package/dist/discovery/peer-metadata.d.ts.map +1 -1
- package/dist/discovery/peer-metadata.js +3 -11
- package/dist/discovery/peer-metadata.js.map +1 -1
- package/dist/discovery/seller-address-resolver.d.ts +1 -49
- package/dist/discovery/seller-address-resolver.d.ts.map +1 -1
- package/dist/discovery/seller-address-resolver.js +2 -114
- package/dist/discovery/seller-address-resolver.js.map +1 -1
- package/dist/errors.d.ts +43 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +73 -0
- package/dist/errors.js.map +1 -0
- package/dist/health/model-health-checker.d.ts +6 -5
- package/dist/health/model-health-checker.d.ts.map +1 -1
- package/dist/health/model-health-checker.js +22 -10
- package/dist/health/model-health-checker.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/seller-provider.d.ts +6 -0
- package/dist/interfaces/seller-provider.d.ts.map +1 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +28 -0
- package/dist/node.js.map +1 -1
- package/dist/p2p/connection-auth.d.ts +3 -8
- package/dist/p2p/connection-auth.d.ts.map +1 -1
- package/dist/p2p/connection-auth.js +4 -6
- package/dist/p2p/connection-auth.js.map +1 -1
- package/dist/p2p/connection-manager.d.ts +3 -1
- package/dist/p2p/connection-manager.d.ts.map +1 -1
- package/dist/p2p/connection-manager.js.map +1 -1
- package/dist/p2p/identity.d.ts +1 -21
- package/dist/p2p/identity.d.ts.map +1 -1
- package/dist/p2p/identity.js +3 -62
- package/dist/p2p/identity.js.map +1 -1
- package/dist/p2p/message-protocol.d.ts +1 -49
- package/dist/p2p/message-protocol.d.ts.map +1 -1
- package/dist/p2p/message-protocol.js +2 -125
- package/dist/p2p/message-protocol.js.map +1 -1
- package/dist/p2p/payment-codec.d.ts +1 -21
- package/dist/p2p/payment-codec.d.ts.map +1 -1
- package/dist/p2p/payment-codec.js +2 -220
- package/dist/p2p/payment-codec.js.map +1 -1
- package/dist/p2p/payment-mux.d.ts +1 -51
- package/dist/p2p/payment-mux.d.ts.map +1 -1
- package/dist/p2p/payment-mux.js +2 -159
- package/dist/p2p/payment-mux.js.map +1 -1
- package/dist/payments/buyer-free-usage-manager.d.ts +1 -43
- package/dist/payments/buyer-free-usage-manager.d.ts.map +1 -1
- package/dist/payments/buyer-free-usage-manager.js +2 -298
- package/dist/payments/buyer-free-usage-manager.js.map +1 -1
- package/dist/payments/buyer-payment-manager.d.ts +1 -277
- package/dist/payments/buyer-payment-manager.d.ts.map +1 -1
- package/dist/payments/buyer-payment-manager.js +2 -1039
- package/dist/payments/buyer-payment-manager.js.map +1 -1
- package/dist/payments/buyer-payment-negotiator.d.ts +1 -130
- package/dist/payments/buyer-payment-negotiator.d.ts.map +1 -1
- package/dist/payments/buyer-payment-negotiator.js +2 -950
- package/dist/payments/buyer-payment-negotiator.js.map +1 -1
- package/dist/payments/chain-config.d.ts.map +1 -1
- package/dist/payments/chain-config.js +13 -2
- package/dist/payments/chain-config.js.map +1 -1
- package/dist/payments/channel-session-state.d.ts +1 -13
- package/dist/payments/channel-session-state.d.ts.map +1 -1
- package/dist/payments/channel-session-state.js +2 -25
- package/dist/payments/channel-session-state.js.map +1 -1
- package/dist/payments/channel-store.d.ts +2 -51
- package/dist/payments/channel-store.d.ts.map +1 -1
- package/dist/payments/channel-store.js +3 -14
- package/dist/payments/channel-store.js.map +1 -1
- package/dist/payments/channel-usage-accounting.d.ts +1 -17
- package/dist/payments/channel-usage-accounting.d.ts.map +1 -1
- package/dist/payments/channel-usage-accounting.js +2 -65
- package/dist/payments/channel-usage-accounting.js.map +1 -1
- package/dist/payments/evm/base-evm-client.d.ts +1 -26
- package/dist/payments/evm/base-evm-client.d.ts.map +1 -1
- package/dist/payments/evm/base-evm-client.js +2 -115
- package/dist/payments/evm/base-evm-client.js.map +1 -1
- package/dist/payments/evm/channels-client.d.ts +1 -85
- package/dist/payments/evm/channels-client.d.ts.map +1 -1
- package/dist/payments/evm/channels-client.js +2 -193
- package/dist/payments/evm/channels-client.js.map +1 -1
- package/dist/payments/evm/deposits-client.d.ts +1 -29
- package/dist/payments/evm/deposits-client.d.ts.map +1 -1
- package/dist/payments/evm/deposits-client.js +2 -67
- package/dist/payments/evm/deposits-client.js.map +1 -1
- package/dist/payments/evm/signatures.d.ts +1 -203
- package/dist/payments/evm/signatures.d.ts.map +1 -1
- package/dist/payments/evm/signatures.js +2 -236
- package/dist/payments/evm/signatures.js.map +1 -1
- package/dist/payments/evm/subpool-client.d.ts +32 -0
- package/dist/payments/evm/subpool-client.d.ts.map +1 -0
- package/dist/payments/evm/subpool-client.js +158 -0
- package/dist/payments/evm/subpool-client.js.map +1 -0
- package/dist/payments/index.d.ts +1 -1
- package/dist/payments/index.d.ts.map +1 -1
- package/dist/payments/index.js +1 -1
- package/dist/payments/index.js.map +1 -1
- package/dist/payments/pricing.d.ts +1 -26
- package/dist/payments/pricing.d.ts.map +1 -1
- package/dist/payments/pricing.js +2 -36
- package/dist/payments/pricing.js.map +1 -1
- package/dist/payments/seller-payment-manager.d.ts +7 -0
- package/dist/payments/seller-payment-manager.d.ts.map +1 -1
- package/dist/payments/seller-payment-manager.js +27 -2
- package/dist/payments/seller-payment-manager.js.map +1 -1
- package/dist/payments/usdc-utils.d.ts +1 -8
- package/dist/payments/usdc-utils.d.ts.map +1 -1
- package/dist/payments/usdc-utils.js +2 -16
- package/dist/payments/usdc-utils.js.map +1 -1
- package/dist/proxy/proxy-mux.d.ts +1 -69
- package/dist/proxy/proxy-mux.d.ts.map +1 -1
- package/dist/proxy/proxy-mux.js +2 -346
- package/dist/proxy/proxy-mux.js.map +1 -1
- package/dist/proxy/request-codec.d.ts +1 -41
- package/dist/proxy/request-codec.d.ts.map +1 -1
- package/dist/proxy/request-codec.js +2 -280
- package/dist/proxy/request-codec.js.map +1 -1
- package/dist/seller-request-handler.d.ts +10 -3
- package/dist/seller-request-handler.d.ts.map +1 -1
- package/dist/seller-request-handler.js +112 -17
- package/dist/seller-request-handler.js.map +1 -1
- package/dist/types/billing.d.ts +2 -0
- package/dist/types/billing.d.ts.map +1 -0
- package/dist/types/billing.js +2 -0
- package/dist/types/billing.js.map +1 -0
- package/dist/types/capability.d.ts +1 -25
- package/dist/types/capability.d.ts.map +1 -1
- package/dist/types/capability.js +2 -1
- package/dist/types/capability.js.map +1 -1
- package/dist/types/connection.d.ts +1 -26
- package/dist/types/connection.d.ts.map +1 -1
- package/dist/types/connection.js +2 -10
- package/dist/types/connection.js.map +1 -1
- package/dist/types/http.d.ts +1 -39
- package/dist/types/http.d.ts.map +1 -1
- package/dist/types/http.js +2 -16
- package/dist/types/http.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/peer.d.ts +9 -31
- package/dist/types/peer.d.ts.map +1 -1
- package/dist/types/peer.js +2 -14
- package/dist/types/peer.js.map +1 -1
- package/dist/types/protocol.d.ts +1 -306
- package/dist/types/protocol.d.ts.map +1 -1
- package/dist/types/protocol.js +2 -81
- package/dist/types/protocol.js.map +1 -1
- package/dist/types/service-api.d.ts +1 -5
- package/dist/types/service-api.d.ts.map +1 -1
- package/dist/types/service-api.js +2 -12
- package/dist/types/service-api.js.map +1 -1
- package/dist/utils/debug.d.ts +1 -5
- package/dist/utils/debug.d.ts.map +1 -1
- package/dist/utils/debug.js +2 -52
- package/dist/utils/debug.js.map +1 -1
- package/dist/utils/hex.d.ts +1 -2
- package/dist/utils/hex.d.ts.map +1 -1
- package/dist/utils/hex.js +2 -14
- package/dist/utils/hex.js.map +1 -1
- package/dist/utils/json-codec.d.ts +1 -9
- package/dist/utils/json-codec.d.ts.map +1 -1
- package/dist/utils/json-codec.js +2 -51
- package/dist/utils/json-codec.js.map +1 -1
- package/dist/utils/response-usage.d.ts +1 -9
- package/dist/utils/response-usage.d.ts.map +1 -1
- package/dist/utils/response-usage.js +2 -39
- package/dist/utils/response-usage.js.map +1 -1
- package/dist/verification/codec.d.ts +1 -3
- package/dist/verification/codec.d.ts.map +1 -1
- package/dist/verification/codec.js +2 -37
- package/dist/verification/codec.js.map +1 -1
- package/dist/verification/response-auth.d.ts +1 -30
- package/dist/verification/response-auth.d.ts.map +1 -1
- package/dist/verification/response-auth.js +2 -110
- package/dist/verification/response-auth.js.map +1 -1
- package/dist/verification/verification-mux.d.ts +1 -19
- package/dist/verification/verification-mux.d.ts.map +1 -1
- package/dist/verification/verification-mux.js +2 -99
- package/dist/verification/verification-mux.js.map +1 -1
- package/package.json +4 -2
|
@@ -1,951 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|