@antseed/node 0.2.106 → 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/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,1040 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { signSpendingAuth, signReserveAuth, makeChannelsDomain, computeMetadataHash, encodeMetadata, ZERO_METADATA, ZERO_METADATA_HASH, computeChannelId, } from './evm/signatures.js';
|
|
4
|
-
import { debugLog, debugWarn } from '../utils/debug.js';
|
|
5
|
-
import { peerIdToAddress } from '../types/peer.js';
|
|
6
|
-
import { CHANNEL_ROLE, CHANNEL_STATUS } from './channel-store.js';
|
|
7
|
-
import { advanceUsageMetadata, CountedRequestTracker, RequestServiceTracker } from './channel-usage-accounting.js';
|
|
8
|
-
import { estimateCostFromBytes, computeCostUsdc } from './pricing.js';
|
|
9
|
-
/** Default tolerance: accept seller claims up to 1.4x buyer's estimate. */
|
|
10
|
-
const DEFAULT_COST_TOLERANCE = 1.4;
|
|
11
|
-
/** Fraction of reserve ceiling at which to signal a top-up is needed.
|
|
12
|
-
* Trigger well before the contract's TOP_UP_SETTLED_THRESHOLD_BPS (85%)
|
|
13
|
-
* so that by the time the seller calls topUp() on-chain, enough has been
|
|
14
|
-
* settled to pass the threshold check. */
|
|
15
|
-
const DEFAULT_TOPUP_THRESHOLD = 0.65;
|
|
16
|
-
/**
|
|
17
|
-
* Manages buyer-side payment sessions using EIP-712 SpendingAuth
|
|
18
|
-
* with cumulative authorization, bytes/4 cost verification, and overdraft control.
|
|
19
|
-
*/
|
|
20
|
-
export class BuyerPaymentManager {
|
|
21
|
-
_identity;
|
|
22
|
-
_signer;
|
|
23
|
-
_depositsClient;
|
|
24
|
-
_config;
|
|
25
|
-
_channelStore;
|
|
26
|
-
/** In-memory map of active confirmed sessions by seller peerId for fast lookups. */
|
|
27
|
-
_confirmedPeers = new Set();
|
|
28
|
-
/** Peers that explicitly rejected our spending auth. */
|
|
29
|
-
_rejectedPeers = new Set();
|
|
30
|
-
_sellerAddressResolver;
|
|
31
|
-
/** sellerPeerId -> cumulative USDC amount in the latest SpendingAuth */
|
|
32
|
-
_cumulativeAmount = new Map();
|
|
33
|
-
/** sellerPeerId -> cumulative metadata for SpendingAuth */
|
|
34
|
-
_metadata = new Map();
|
|
35
|
-
/** sellerPeerId -> buyer-verified cumulative cost from bytes/4 */
|
|
36
|
-
_verifiedCost = new Map();
|
|
37
|
-
/** requestId -> service/model the buyer requested (from its own request body).
|
|
38
|
-
* Used in handleNeedAuth to validate cost with the correct pricing tier
|
|
39
|
-
* without trusting the seller's claim of which service was used. */
|
|
40
|
-
_requestService = new RequestServiceTracker();
|
|
41
|
-
/** sellerPeerId -> full pricing map (defaults + per-service overrides from peer metadata / 402) */
|
|
42
|
-
_sessionPricing = new Map();
|
|
43
|
-
/** Cumulative response token totals per seller, tracked independently of signing metadata. */
|
|
44
|
-
_responseTokenTotals = new Map();
|
|
45
|
-
/** sellerPeerId -> current on-chain reserve ceiling (can grow with top-ups) */
|
|
46
|
-
_currentReserveCeiling = new Map();
|
|
47
|
-
/** sellerPeerId -> original first-reserve amount for replaying reserve() safely. */
|
|
48
|
-
_initialReserveAmount = new Map();
|
|
49
|
-
/** sellerPeerId -> salt used in the current reserve */
|
|
50
|
-
_reserveSalt = new Map();
|
|
51
|
-
/** Cached EIP-712 domain — static for the lifetime of this manager. */
|
|
52
|
-
_channelsDomain;
|
|
53
|
-
constructor(identity, config, channelStore, sellerAddressResolver) {
|
|
54
|
-
this._identity = identity;
|
|
55
|
-
this._config = config;
|
|
56
|
-
this._sellerAddressResolver = sellerAddressResolver;
|
|
57
|
-
this._signer = identity.wallet;
|
|
58
|
-
this._depositsClient = new DepositsClient({
|
|
59
|
-
rpcUrl: config.rpcUrl,
|
|
60
|
-
...(config.fallbackRpcUrls ? { fallbackRpcUrls: config.fallbackRpcUrls } : {}),
|
|
61
|
-
contractAddress: config.depositsContractAddress,
|
|
62
|
-
usdcAddress: config.usdcAddress,
|
|
63
|
-
evmChainId: config.chainId,
|
|
64
|
-
});
|
|
65
|
-
this._channelStore = channelStore;
|
|
66
|
-
this._channelsDomain = makeChannelsDomain(config.chainId, config.channelsContractAddress);
|
|
67
|
-
// Hydrate cumulative maps from persisted active sessions
|
|
68
|
-
this._hydrateFromStore();
|
|
69
|
-
}
|
|
70
|
-
/** Hydrate cumulative tracking maps from persisted active buyer sessions. */
|
|
71
|
-
_hydrateFromStore() {
|
|
72
|
-
const activeChannels = this._channelStore.getActiveChannelsByBuyer(CHANNEL_ROLE.BUYER, this._identity.wallet.address);
|
|
73
|
-
for (const channel of activeChannels) {
|
|
74
|
-
const peerId = channel.peerId;
|
|
75
|
-
const persistedCumulative = BigInt(channel.authMax);
|
|
76
|
-
this._cumulativeAmount.set(peerId, persistedCumulative);
|
|
77
|
-
const metadata = this._sanitizeMetadata(this._channelStore.getChannelMetadata(channel));
|
|
78
|
-
this._metadata.set(peerId, metadata);
|
|
79
|
-
if (!this._disableMetadataV2Services) {
|
|
80
|
-
this._persistServiceMetadata(channel.sessionId, metadata);
|
|
81
|
-
}
|
|
82
|
-
// Hydrate verifiedCost to authMax so _maxSignable can grow beyond maxPerRequestUsdc.
|
|
83
|
-
// Without this, maxSignable = 0 + maxPerRequestUsdc after restart, permanently capping
|
|
84
|
-
// the cumulative and causing non-monotonic SpendingAuth rejections on the seller.
|
|
85
|
-
this._verifiedCost.set(peerId, persistedCumulative);
|
|
86
|
-
this._responseTokenTotals.set(peerId, {
|
|
87
|
-
input: Number(channel.tokensDelivered),
|
|
88
|
-
output: Number(channel.previousConsumption),
|
|
89
|
-
requests: channel.requestCount,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
get signer() {
|
|
94
|
-
return this._signer;
|
|
95
|
-
}
|
|
96
|
-
setSigner(signer) {
|
|
97
|
-
this._signer = signer;
|
|
98
|
-
}
|
|
99
|
-
get depositsClient() {
|
|
100
|
-
return this._depositsClient;
|
|
101
|
-
}
|
|
102
|
-
get _costTolerance() {
|
|
103
|
-
return this._config.costToleranceMultiplier ?? DEFAULT_COST_TOLERANCE;
|
|
104
|
-
}
|
|
105
|
-
get _disableMetadataV2Services() {
|
|
106
|
-
return this._config.disableMetadataV2Services === true;
|
|
107
|
-
}
|
|
108
|
-
_sanitizeMetadata(metadata) {
|
|
109
|
-
const current = metadata ?? ZERO_METADATA;
|
|
110
|
-
if (!this._disableMetadataV2Services)
|
|
111
|
-
return current;
|
|
112
|
-
return {
|
|
113
|
-
cumulativeInputTokens: current.cumulativeInputTokens,
|
|
114
|
-
cumulativeOutputTokens: current.cumulativeOutputTokens,
|
|
115
|
-
cumulativeRequestCount: current.cumulativeRequestCount,
|
|
116
|
-
services: [],
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
_advanceUsageMetadata(previous, service, delta) {
|
|
120
|
-
return advanceUsageMetadata(this._sanitizeMetadata(previous), this._disableMetadataV2Services ? undefined : service, delta);
|
|
121
|
-
}
|
|
122
|
-
_getCeiling(sellerPeerId) {
|
|
123
|
-
return this._currentReserveCeiling.get(sellerPeerId) ?? this._config.maxReserveAmountUsdc;
|
|
124
|
-
}
|
|
125
|
-
/** Clean up all in-memory state for a seller when the session ends. */
|
|
126
|
-
cleanupSession(sellerPeerId) {
|
|
127
|
-
this._cumulativeAmount.delete(sellerPeerId);
|
|
128
|
-
this._metadata.delete(sellerPeerId);
|
|
129
|
-
this._verifiedCost.delete(sellerPeerId);
|
|
130
|
-
this._sessionPricing.delete(sellerPeerId);
|
|
131
|
-
this._currentReserveCeiling.delete(sellerPeerId);
|
|
132
|
-
this._initialReserveAmount.delete(sellerPeerId);
|
|
133
|
-
this._reserveSalt.delete(sellerPeerId);
|
|
134
|
-
this._confirmedPeers.delete(sellerPeerId);
|
|
135
|
-
this._rejectedPeers.delete(sellerPeerId);
|
|
136
|
-
this._responseTokenTotals.delete(sellerPeerId);
|
|
137
|
-
}
|
|
138
|
-
getActiveSession(sellerPeerId) {
|
|
139
|
-
return this._channelStore.getActiveChannelByPeerAndBuyer(sellerPeerId, CHANNEL_ROLE.BUYER, this._identity.wallet.address);
|
|
140
|
-
}
|
|
141
|
-
retireSession(sellerPeerId, status, settledAmount) {
|
|
142
|
-
const session = this.getActiveSession(sellerPeerId);
|
|
143
|
-
if (session) {
|
|
144
|
-
this._channelStore.updateChannelStatus(session.sessionId, status, settledAmount !== undefined ? settledAmount.toString() : undefined);
|
|
145
|
-
}
|
|
146
|
-
this.cleanupSession(sellerPeerId);
|
|
147
|
-
}
|
|
148
|
-
canReplayReserveAuth(sellerPeerId) {
|
|
149
|
-
return this._reserveSalt.has(sellerPeerId);
|
|
150
|
-
}
|
|
151
|
-
clearLockConfirmation(sellerPeerId) {
|
|
152
|
-
this._confirmedPeers.delete(sellerPeerId);
|
|
153
|
-
this._rejectedPeers.delete(sellerPeerId);
|
|
154
|
-
}
|
|
155
|
-
async resendCurrentSpendingAuth(sellerPeerId, paymentMux) {
|
|
156
|
-
const session = this.getActiveSession(sellerPeerId);
|
|
157
|
-
if (!session) {
|
|
158
|
-
throw new Error(`[BuyerPayment] No active session for seller ${sellerPeerId.slice(0, 12)}...`);
|
|
159
|
-
}
|
|
160
|
-
const cumulativeAmount = this._cumulativeAmount.get(sellerPeerId) ?? BigInt(session.authMax);
|
|
161
|
-
const currentMeta = this._sanitizeMetadata(this._metadata.get(sellerPeerId));
|
|
162
|
-
const metadataHashHex = computeMetadataHash(currentMeta);
|
|
163
|
-
const encodedMetadata = encodeMetadata(currentMeta);
|
|
164
|
-
const metadataMsg = {
|
|
165
|
-
channelId: session.sessionId,
|
|
166
|
-
cumulativeAmount,
|
|
167
|
-
metadataHash: metadataHashHex,
|
|
168
|
-
};
|
|
169
|
-
const spendingAuthSig = await signSpendingAuth(this._signer, this._channelsDomain, metadataMsg);
|
|
170
|
-
paymentMux.sendSpendingAuth({
|
|
171
|
-
channelId: session.sessionId,
|
|
172
|
-
cumulativeAmount: cumulativeAmount.toString(),
|
|
173
|
-
metadataHash: metadataHashHex,
|
|
174
|
-
metadata: encodedMetadata,
|
|
175
|
-
spendingAuthSig,
|
|
176
|
-
});
|
|
177
|
-
return session.sessionId;
|
|
178
|
-
}
|
|
179
|
-
async extendCurrentSpendingAuth(sellerPeerId, minBudgetPerRequest, paymentMux, targetCumulative) {
|
|
180
|
-
const session = this.getActiveSession(sellerPeerId);
|
|
181
|
-
if (!session) {
|
|
182
|
-
throw new Error(`[BuyerPayment] No active session for seller ${sellerPeerId.slice(0, 12)}...`);
|
|
183
|
-
}
|
|
184
|
-
const currentCumulative = this._cumulativeAmount.get(sellerPeerId) ?? BigInt(session.authMax);
|
|
185
|
-
let maxSignable = this._maxSignable(sellerPeerId);
|
|
186
|
-
maxSignable = this._reopenOverdraftWindowIfCollapsed(sellerPeerId, currentCumulative, maxSignable, 'extendCurrentSpendingAuth');
|
|
187
|
-
const ceiling = this._getCeiling(sellerPeerId);
|
|
188
|
-
// Prefer the seller-supplied target when present — a raw
|
|
189
|
-
// `currentCumulative + minBudgetPerRequest` advance may still fall short
|
|
190
|
-
// of what the seller has already spent, producing an infinite 402 loop.
|
|
191
|
-
const minAdvance = currentCumulative + minBudgetPerRequest;
|
|
192
|
-
const requestedAmount = targetCumulative != null && targetCumulative > minAdvance
|
|
193
|
-
? targetCumulative
|
|
194
|
-
: minAdvance;
|
|
195
|
-
const nextCumulative = requestedAmount < maxSignable ? requestedAmount : maxSignable;
|
|
196
|
-
const extendNeedsTopUp = this._needsCeilingAdvance(requestedAmount, maxSignable, ceiling);
|
|
197
|
-
if (nextCumulative <= currentCumulative) {
|
|
198
|
-
// Nothing to sign at current ceiling — try topUp anyway for next round
|
|
199
|
-
if (extendNeedsTopUp) {
|
|
200
|
-
await this._topUpAfterSpendAuthBestEffort(sellerPeerId, paymentMux, 'extendCurrentSpendingAuth');
|
|
201
|
-
}
|
|
202
|
-
debugWarn(`[BuyerPayment] Cannot extend active session for ${sellerPeerId.slice(0, 12)}... ` +
|
|
203
|
-
`(current=${currentCumulative} maxSignable=${maxSignable} target=${targetCumulative ?? 'n/a'})`);
|
|
204
|
-
return session.sessionId;
|
|
205
|
-
}
|
|
206
|
-
const currentMeta = this._sanitizeMetadata(this._metadata.get(sellerPeerId));
|
|
207
|
-
await this._sendUpdatedSpendingAuth(session, sellerPeerId, nextCumulative, currentMeta, paymentMux);
|
|
208
|
-
// Send topUp AFTER the SpendingAuth so the seller processes the higher
|
|
209
|
-
// cumulative first, meeting the on-chain settle threshold for topUp.
|
|
210
|
-
if (extendNeedsTopUp) {
|
|
211
|
-
await this._topUpAfterSpendAuthBestEffort(sellerPeerId, paymentMux, 'extendCurrentSpendingAuth');
|
|
212
|
-
}
|
|
213
|
-
return session.sessionId;
|
|
214
|
-
}
|
|
215
|
-
/**
|
|
216
|
-
* Build a CloseChannelRequest for an active session.
|
|
217
|
-
*
|
|
218
|
-
* With `includeAuth` (the default) the buyer signs its current cumulative so
|
|
219
|
-
* a seller that lost the last SpendingAuth can still close at the full
|
|
220
|
-
* amount owed. Signing costs nothing extra: the cumulative is unchanged, so
|
|
221
|
-
* this authorizes no more than the seller could already claim. Pass
|
|
222
|
-
* `includeAuth: false` to send a bare request and let the seller close using
|
|
223
|
-
* whatever auth it already holds.
|
|
224
|
-
*/
|
|
225
|
-
async buildCloseChannelRequest(sellerPeerId, { includeAuth = true } = {}) {
|
|
226
|
-
const session = this.getActiveSession(sellerPeerId);
|
|
227
|
-
if (!session) {
|
|
228
|
-
throw new Error(`[BuyerPayment] No active session for seller ${sellerPeerId.slice(0, 12)}...`);
|
|
229
|
-
}
|
|
230
|
-
const cumulativeAmount = this._cumulativeAmount.get(sellerPeerId) ?? BigInt(session.authMax);
|
|
231
|
-
if (!includeAuth || cumulativeAmount <= 0n) {
|
|
232
|
-
return { version: 1, channelId: session.sessionId };
|
|
233
|
-
}
|
|
234
|
-
const currentMeta = this._sanitizeMetadata(this._metadata.get(sellerPeerId));
|
|
235
|
-
const metadataHash = computeMetadataHash(currentMeta);
|
|
236
|
-
const metadataMsg = {
|
|
237
|
-
channelId: session.sessionId,
|
|
238
|
-
cumulativeAmount,
|
|
239
|
-
metadataHash,
|
|
240
|
-
};
|
|
241
|
-
return {
|
|
242
|
-
version: 1,
|
|
243
|
-
channelId: session.sessionId,
|
|
244
|
-
cumulativeAmount: cumulativeAmount.toString(),
|
|
245
|
-
metadataHash,
|
|
246
|
-
metadata: encodeMetadata(currentMeta),
|
|
247
|
-
spendingAuthSig: await signSpendingAuth(this._signer, this._channelsDomain, metadataMsg),
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
async resendReserveAuth(sellerPeerId, paymentMux) {
|
|
251
|
-
const session = this.getActiveSession(sellerPeerId);
|
|
252
|
-
const salt = this._reserveSalt.get(sellerPeerId);
|
|
253
|
-
if (!session || !salt) {
|
|
254
|
-
throw new Error(`[BuyerPayment] No replayable reserve for seller ${sellerPeerId.slice(0, 12)}...`);
|
|
255
|
-
}
|
|
256
|
-
// Force a fresh AuthAck after replaying the reserve path.
|
|
257
|
-
this._confirmedPeers.delete(sellerPeerId);
|
|
258
|
-
const replayAmount = this._initialReserveAmount.get(sellerPeerId)
|
|
259
|
-
?? this._currentReserveCeiling.get(sellerPeerId)
|
|
260
|
-
?? this._config.maxReserveAmountUsdc;
|
|
261
|
-
const maxAmount = replayAmount > this._config.maxReserveAmountUsdc
|
|
262
|
-
? this._config.maxReserveAmountUsdc
|
|
263
|
-
: replayAmount;
|
|
264
|
-
const deadline = Math.floor(Date.now() / 1000) + this._config.defaultAuthDurationSecs;
|
|
265
|
-
const reserveMsg = {
|
|
266
|
-
channelId: session.sessionId,
|
|
267
|
-
maxAmount,
|
|
268
|
-
deadline: BigInt(deadline),
|
|
269
|
-
};
|
|
270
|
-
const reserveAuthSig = await signReserveAuth(this._signer, this._channelsDomain, reserveMsg);
|
|
271
|
-
paymentMux.sendSpendingAuth({
|
|
272
|
-
channelId: session.sessionId,
|
|
273
|
-
cumulativeAmount: session.authMax,
|
|
274
|
-
metadataHash: ZERO_METADATA_HASH,
|
|
275
|
-
metadata: encodeMetadata(this._sanitizeMetadata(this._metadata.get(sellerPeerId))),
|
|
276
|
-
spendingAuthSig: reserveAuthSig,
|
|
277
|
-
reserveSalt: salt,
|
|
278
|
-
reserveMaxAmount: maxAmount.toString(),
|
|
279
|
-
reserveDeadline: deadline,
|
|
280
|
-
});
|
|
281
|
-
return session.sessionId;
|
|
282
|
-
}
|
|
283
|
-
_reopenOverdraftWindowIfCollapsed(sellerPeerId, currentCumulative, maxSignable, context) {
|
|
284
|
-
// Overdraft-window unblock: when the buyer has already signed up to
|
|
285
|
-
// `verified + maxPerRequest` and the seller returned 402 because `spent`
|
|
286
|
-
// caught up, advance `verifiedCost` to the current signed cumulative so
|
|
287
|
-
// the window reopens. The buyer has already committed to pay
|
|
288
|
-
// `currentCumulative` via the signed SpendingAuth, so the seller can
|
|
289
|
-
// already claim that much on-chain — advancing verified to match doesn't
|
|
290
|
-
// expose new funds, it just reclaims overdraft headroom.
|
|
291
|
-
//
|
|
292
|
-
// SECURITY: the trust anchor here is `currentCumulative`, NOT any
|
|
293
|
-
// seller-supplied target. A malicious seller could overstate required
|
|
294
|
-
// spend in a 402 / NeedAuth path in an attempt to drain the reserve. We
|
|
295
|
-
// defend by only advancing verifiedCost to the amount the buyer has
|
|
296
|
-
// already signed. Seller-provided values remain destination hints bounded
|
|
297
|
-
// by `verifiedCost + maxPerRequestUsdc` and are never used to mint new
|
|
298
|
-
// trust directly.
|
|
299
|
-
if (maxSignable > currentCumulative)
|
|
300
|
-
return maxSignable;
|
|
301
|
-
const previousVerified = this._verifiedCost.get(sellerPeerId) ?? 0n;
|
|
302
|
-
if (currentCumulative <= previousVerified)
|
|
303
|
-
return maxSignable;
|
|
304
|
-
this._verifiedCost.set(sellerPeerId, currentCumulative);
|
|
305
|
-
const reopened = this._maxSignable(sellerPeerId);
|
|
306
|
-
debugLog(`[BuyerPayment] ${context}: advanced verifiedCost ${previousVerified} → ${currentCumulative} ` +
|
|
307
|
-
`to unblock overdraft window for ${sellerPeerId.slice(0, 12)}...`);
|
|
308
|
-
return reopened;
|
|
309
|
-
}
|
|
310
|
-
_needsCeilingAdvance(requestedAmount, maxSignable, ceiling) {
|
|
311
|
-
return requestedAmount > maxSignable && maxSignable >= ceiling;
|
|
312
|
-
}
|
|
313
|
-
/**
|
|
314
|
-
* Both signPerRequestAuth (buyer-initiated) and handleNeedAuth (seller-initiated)
|
|
315
|
-
* can fire for the same request when the proactive auth doesn't fully cover the
|
|
316
|
-
* seller's required cumulative. Whichever path counts a request's tokens first
|
|
317
|
-
* records its requestId here so the other path attributes only the amount delta.
|
|
318
|
-
*/
|
|
319
|
-
_serviceTokensCounted = new CountedRequestTracker();
|
|
320
|
-
_spendListener = null;
|
|
321
|
-
/**
|
|
322
|
-
* Observe per-request spend as it is signed. Used to attribute cost to the
|
|
323
|
-
* caller's own unit of work (a tool conversation, say), which only the layer
|
|
324
|
-
* that issued the request can know.
|
|
325
|
-
*/
|
|
326
|
-
setSpendListener(listener) {
|
|
327
|
-
this._spendListener = listener;
|
|
328
|
-
}
|
|
329
|
-
_reportSpend(event) {
|
|
330
|
-
if (!this._spendListener)
|
|
331
|
-
return;
|
|
332
|
-
try {
|
|
333
|
-
this._spendListener(event);
|
|
334
|
-
}
|
|
335
|
-
catch (err) {
|
|
336
|
-
// Accounting is a bystander here — never let it break the payment path.
|
|
337
|
-
debugWarn(`[BuyerPayment] spend listener threw: ${err instanceof Error ? err.message : err}`);
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
_persistServiceMetadata(sessionId, metadata) {
|
|
341
|
-
this._channelStore.replaceMetadataServiceTotals(sessionId, this._sanitizeMetadata(metadata).services);
|
|
342
|
-
}
|
|
343
|
-
async _sendUpdatedSpendingAuth(session, sellerPeerId, cumulativeAmount, metadata, paymentMux) {
|
|
344
|
-
const sanitizedMetadata = this._sanitizeMetadata(metadata);
|
|
345
|
-
const metadataHashHex = computeMetadataHash(sanitizedMetadata);
|
|
346
|
-
const encodedMetadata = encodeMetadata(sanitizedMetadata);
|
|
347
|
-
const metadataMsg = {
|
|
348
|
-
channelId: session.sessionId,
|
|
349
|
-
cumulativeAmount,
|
|
350
|
-
metadataHash: metadataHashHex,
|
|
351
|
-
};
|
|
352
|
-
const spendingAuthSig = await signSpendingAuth(this._signer, this._channelsDomain, metadataMsg);
|
|
353
|
-
this._cumulativeAmount.set(sellerPeerId, cumulativeAmount);
|
|
354
|
-
this._persistServiceMetadata(session.sessionId, sanitizedMetadata);
|
|
355
|
-
this._channelStore.upsertChannel({
|
|
356
|
-
...session,
|
|
357
|
-
authMax: cumulativeAmount.toString(),
|
|
358
|
-
updatedAt: Date.now(),
|
|
359
|
-
});
|
|
360
|
-
paymentMux.sendSpendingAuth({
|
|
361
|
-
channelId: session.sessionId,
|
|
362
|
-
cumulativeAmount: cumulativeAmount.toString(),
|
|
363
|
-
metadataHash: metadataHashHex,
|
|
364
|
-
metadata: encodedMetadata,
|
|
365
|
-
spendingAuthSig,
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
async _topUpAfterSpendAuthBestEffort(sellerPeerId, paymentMux, context) {
|
|
369
|
-
try {
|
|
370
|
-
await this.topUpReserve(sellerPeerId, paymentMux);
|
|
371
|
-
}
|
|
372
|
-
catch (err) {
|
|
373
|
-
debugWarn(`[BuyerPayment] ${context}: topUpReserve failed: ${err instanceof Error ? err.message : err}`);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
// ── Spending Authorization ────────────────────────────────────
|
|
377
|
-
/**
|
|
378
|
-
* Sign and send an initial EIP-712 SpendingAuth to a seller.
|
|
379
|
-
* The initial cumulativeAmount is set to the seller's minBudgetPerRequest.
|
|
380
|
-
*
|
|
381
|
-
* @param pricing Token pricing from the seller's 402 / peer metadata.
|
|
382
|
-
* @param pricingMap Full pricing map (defaults + per-service) from peer metadata. If provided,
|
|
383
|
-
* merges with existing session pricing. The `pricing` arg is used as the service-specific
|
|
384
|
-
* override for the service that triggered the 402.
|
|
385
|
-
*/
|
|
386
|
-
async authorizeSpending(sellerPeerId, paymentMux, minBudgetPerRequest, reserveAmountOrPricing, pricingArg, pricingMap, metadataArg) {
|
|
387
|
-
const sellerEvmAddr = this._sellerAddressResolver
|
|
388
|
-
? await this._sellerAddressResolver.resolveSellerAddress(sellerPeerId, metadataArg)
|
|
389
|
-
: peerIdToAddress(sellerPeerId);
|
|
390
|
-
const reserveAmount = typeof reserveAmountOrPricing === 'bigint'
|
|
391
|
-
? reserveAmountOrPricing
|
|
392
|
-
: this._config.maxReserveAmountUsdc;
|
|
393
|
-
const pricing = typeof reserveAmountOrPricing === 'bigint'
|
|
394
|
-
? pricingArg
|
|
395
|
-
: reserveAmountOrPricing;
|
|
396
|
-
// Budget validation: reject if seller demands more than buyer's overdraft limit
|
|
397
|
-
if (minBudgetPerRequest > this._config.maxPerRequestUsdc) {
|
|
398
|
-
debugWarn(`[BuyerPayment] Seller ${sellerPeerId.slice(0, 12)}... minBudgetPerRequest=${minBudgetPerRequest} exceeds maxPerRequestUsdc=${this._config.maxPerRequestUsdc} — not authorizing`);
|
|
399
|
-
return '';
|
|
400
|
-
}
|
|
401
|
-
// Clear confirmation state so we wait for a fresh AuthAck on the new session
|
|
402
|
-
this._confirmedPeers.delete(sellerPeerId);
|
|
403
|
-
// Store full pricing map (defaults + all per-service overrides).
|
|
404
|
-
// Merge 402-negotiated pricing on top of peer-metadata defaults so
|
|
405
|
-
// the seller's live rate takes precedence over cached peer metadata.
|
|
406
|
-
if (pricingMap) {
|
|
407
|
-
const mergedDefaults = pricing
|
|
408
|
-
? { ...pricingMap.defaults, ...pricing }
|
|
409
|
-
: pricingMap.defaults;
|
|
410
|
-
this._sessionPricing.set(sellerPeerId, { defaults: mergedDefaults, services: pricingMap.services });
|
|
411
|
-
}
|
|
412
|
-
else if (pricing) {
|
|
413
|
-
// Legacy: single pricing, store as defaults
|
|
414
|
-
const existing = this._sessionPricing.get(sellerPeerId);
|
|
415
|
-
this._sessionPricing.set(sellerPeerId, {
|
|
416
|
-
defaults: pricing,
|
|
417
|
-
services: existing?.services ?? {},
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
// Generate random salt and compute deterministic channelId
|
|
421
|
-
const salt = '0x' + randomBytes(32).toString('hex');
|
|
422
|
-
const buyerEvmAddr = this._identity.wallet.address;
|
|
423
|
-
const channelId = computeChannelId(buyerEvmAddr, sellerEvmAddr, salt);
|
|
424
|
-
const deadline = Math.floor(Date.now() / 1000) + this._config.defaultAuthDurationSecs;
|
|
425
|
-
debugLog(`[BuyerPayment] authorizeSpending: channel=${channelId.slice(0, 18)}... seller=${sellerPeerId.slice(0, 12)}... amount=${minBudgetPerRequest}`);
|
|
426
|
-
// Sign ReserveAuth — binds channelId, maxAmount, deadline on-chain
|
|
427
|
-
const channelsDomain = this._channelsDomain;
|
|
428
|
-
const maxAmount = reserveAmount;
|
|
429
|
-
const reserveMsg = {
|
|
430
|
-
channelId,
|
|
431
|
-
maxAmount,
|
|
432
|
-
deadline: BigInt(deadline),
|
|
433
|
-
};
|
|
434
|
-
const reserveAuthSig = await signReserveAuth(this._signer, channelsDomain, reserveMsg);
|
|
435
|
-
// Initialize state for this session.
|
|
436
|
-
// Start cumulative at 0 — the initial message is a ReserveAuth (not a SpendingAuth).
|
|
437
|
-
// The first real SpendingAuth will be sent by handleNeedAuth after the seller serves
|
|
438
|
-
// the first request and reports its cost.
|
|
439
|
-
this._cumulativeAmount.set(sellerPeerId, 0n);
|
|
440
|
-
this._metadata.set(sellerPeerId, this._sanitizeMetadata({ ...ZERO_METADATA }));
|
|
441
|
-
this._verifiedCost.set(sellerPeerId, 0n);
|
|
442
|
-
this._currentReserveCeiling.set(sellerPeerId, maxAmount);
|
|
443
|
-
this._initialReserveAmount.set(sellerPeerId, maxAmount);
|
|
444
|
-
this._reserveSalt.set(sellerPeerId, salt);
|
|
445
|
-
// Store session
|
|
446
|
-
const now = Date.now();
|
|
447
|
-
const session = {
|
|
448
|
-
sessionId: channelId,
|
|
449
|
-
peerId: sellerPeerId,
|
|
450
|
-
role: CHANNEL_ROLE.BUYER,
|
|
451
|
-
sellerEvmAddr,
|
|
452
|
-
buyerEvmAddr: this._identity.wallet.address,
|
|
453
|
-
nonce: 0,
|
|
454
|
-
authMax: '0',
|
|
455
|
-
deadline,
|
|
456
|
-
previousSessionId: '0x' + '0'.repeat(64),
|
|
457
|
-
previousConsumption: '0',
|
|
458
|
-
tokensDelivered: '0',
|
|
459
|
-
requestCount: 0,
|
|
460
|
-
reservedAt: now,
|
|
461
|
-
settledAt: null,
|
|
462
|
-
settledAmount: null,
|
|
463
|
-
status: CHANNEL_STATUS.ACTIVE,
|
|
464
|
-
latestBuyerSig: null,
|
|
465
|
-
latestSpendingAuthSig: null,
|
|
466
|
-
latestMetadata: null,
|
|
467
|
-
createdAt: now,
|
|
468
|
-
updatedAt: now,
|
|
469
|
-
};
|
|
470
|
-
this._channelStore.upsertChannel(session);
|
|
471
|
-
// Send SpendingAuth via PaymentMux — reserve carries ReserveAuth sig
|
|
472
|
-
paymentMux.sendSpendingAuth({
|
|
473
|
-
channelId,
|
|
474
|
-
cumulativeAmount: '0',
|
|
475
|
-
metadataHash: ZERO_METADATA_HASH,
|
|
476
|
-
metadata: encodeMetadata(ZERO_METADATA),
|
|
477
|
-
spendingAuthSig: reserveAuthSig,
|
|
478
|
-
reserveSalt: salt,
|
|
479
|
-
reserveMaxAmount: maxAmount.toString(),
|
|
480
|
-
reserveDeadline: deadline,
|
|
481
|
-
});
|
|
482
|
-
return channelId;
|
|
483
|
-
}
|
|
484
|
-
// ── AuthAck handler ───────────────────────────────────────────
|
|
485
|
-
handleAuthAck(sellerPeerId, payload) {
|
|
486
|
-
const session = this.getActiveSession(sellerPeerId);
|
|
487
|
-
if (!session) {
|
|
488
|
-
debugWarn(`[BuyerPayment] AuthAck for unknown seller: ${sellerPeerId.slice(0, 12)}...`);
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
if (session.sessionId !== payload.channelId) {
|
|
492
|
-
debugWarn(`[BuyerPayment] AuthAck channel mismatch: expected=${session.sessionId.slice(0, 18)}... got=${payload.channelId.slice(0, 18)}...`);
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
this._confirmedPeers.add(sellerPeerId);
|
|
496
|
-
debugLog(`[BuyerPayment] AuthAck confirmed: channel=${session.sessionId.slice(0, 18)}...`);
|
|
497
|
-
}
|
|
498
|
-
// ── Buyer-side cost verification ──────────────────────────────
|
|
499
|
-
/**
|
|
500
|
-
* Estimate tokens and cost from response content without updating state.
|
|
501
|
-
*/
|
|
502
|
-
_estimateResponseCost(sellerPeerId, inputBytes, outputBytes, service) {
|
|
503
|
-
const pricing = this.getSessionPricing(sellerPeerId, service);
|
|
504
|
-
if (!pricing)
|
|
505
|
-
return null;
|
|
506
|
-
return estimateCostFromBytes(inputBytes, outputBytes, pricing);
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* Accumulate a cost estimate into verifiedCost.
|
|
510
|
-
*/
|
|
511
|
-
_accumulateVerifiedCost(sellerPeerId, estimate) {
|
|
512
|
-
const prev = this._verifiedCost.get(sellerPeerId) ?? 0n;
|
|
513
|
-
const newVerified = prev + estimate.cost;
|
|
514
|
-
this._verifiedCost.set(sellerPeerId, newVerified);
|
|
515
|
-
return newVerified;
|
|
516
|
-
}
|
|
517
|
-
/**
|
|
518
|
-
* Record response content and update the buyer's verified cost.
|
|
519
|
-
* Call this after receiving each response from the seller.
|
|
520
|
-
*
|
|
521
|
-
* NOTE: Do not call this AND signPerRequestAuth for the same response —
|
|
522
|
-
* signPerRequestAuth already updates verifiedCost internally.
|
|
523
|
-
*
|
|
524
|
-
* @returns The updated verified cost and estimated tokens, or null if no pricing is available.
|
|
525
|
-
*/
|
|
526
|
-
recordResponseBytes(sellerPeerId, inputBytes, outputBytes) {
|
|
527
|
-
const estimate = this._estimateResponseCost(sellerPeerId, inputBytes, outputBytes);
|
|
528
|
-
if (!estimate)
|
|
529
|
-
return null;
|
|
530
|
-
const newVerified = this._accumulateVerifiedCost(sellerPeerId, estimate);
|
|
531
|
-
const inSize = inputBytes.length;
|
|
532
|
-
const outSize = outputBytes.length;
|
|
533
|
-
debugLog(`[BuyerPayment] recordResponseBytes: seller=${sellerPeerId.slice(0, 12)}... ` +
|
|
534
|
-
`in=${inSize}B→${estimate.inputTokens}tok out=${outSize}B→${estimate.outputTokens}tok ` +
|
|
535
|
-
`requestCost=${estimate.cost} verifiedCost=${newVerified}`);
|
|
536
|
-
return { verifiedCost: newVerified, inputTokens: estimate.inputTokens, outputTokens: estimate.outputTokens };
|
|
537
|
-
}
|
|
538
|
-
// ── Per-request authorization (overdraft model) ─────────────
|
|
539
|
-
/**
|
|
540
|
-
* Compute the max signable cumulative amount based on the overdraft model:
|
|
541
|
-
* maxSignable = verifiedCost + maxPerRequestUsdc, capped at reserve ceiling.
|
|
542
|
-
*/
|
|
543
|
-
_maxSignable(sellerPeerId) {
|
|
544
|
-
const verified = this._verifiedCost.get(sellerPeerId) ?? 0n;
|
|
545
|
-
const ceiling = this._getCeiling(sellerPeerId);
|
|
546
|
-
const maxSignable = verified + this._config.maxPerRequestUsdc;
|
|
547
|
-
return maxSignable < ceiling ? maxSignable : ceiling;
|
|
548
|
-
}
|
|
549
|
-
/**
|
|
550
|
-
* Check whether the current cumulative amount is approaching the reserve ceiling
|
|
551
|
-
* and a top-up should be triggered.
|
|
552
|
-
*/
|
|
553
|
-
_needsTopUp(sellerPeerId) {
|
|
554
|
-
const ceiling = this._getCeiling(sellerPeerId);
|
|
555
|
-
const current = this._cumulativeAmount.get(sellerPeerId) ?? 0n;
|
|
556
|
-
const threshold = BigInt(Math.floor(Number(ceiling) * DEFAULT_TOPUP_THRESHOLD));
|
|
557
|
-
return current >= threshold;
|
|
558
|
-
}
|
|
559
|
-
/**
|
|
560
|
-
* Sign an updated SpendingAuth after receiving a response.
|
|
561
|
-
*
|
|
562
|
-
* The buyer uses the seller's claimed cost to advance the cumulative amount,
|
|
563
|
-
* but validates it against the buyer's bytes/4 estimate. If the seller's claim
|
|
564
|
-
* exceeds the buyer's estimate by more than the configured tolerance, the buyer
|
|
565
|
-
* caps at tolerance * buyerEstimate. The cumulative is also capped at the
|
|
566
|
-
* overdraft limit (verifiedCost + maxPerRequestUsdc) and the reserve ceiling.
|
|
567
|
-
*
|
|
568
|
-
* @param sellerPeerId Seller peer ID.
|
|
569
|
-
* @param responseStats Byte counts from the last response and seller's claimed cost.
|
|
570
|
-
* @returns The signed payload and whether a reserve top-up is needed.
|
|
571
|
-
*/
|
|
572
|
-
async signPerRequestAuth(sellerPeerId, responseStats) {
|
|
573
|
-
const session = this.getActiveSession(sellerPeerId);
|
|
574
|
-
if (!session) {
|
|
575
|
-
throw new Error(`[BuyerPayment] No active session for seller ${sellerPeerId.slice(0, 12)}... — call authorizeSpending() first`);
|
|
576
|
-
}
|
|
577
|
-
// Prefer reported token counts (from seller headers or buyer's parsed response usage)
|
|
578
|
-
// over byte-based estimation. Byte estimation inflates tokens due to JSON/SSE overhead.
|
|
579
|
-
// Also treat seller-claimed cost of 0 as authoritative — don't fall back to byte estimation.
|
|
580
|
-
const hasReportedTokens = (responseStats.reportedInputTokens != null && responseStats.reportedInputTokens > 0n) ||
|
|
581
|
-
(responseStats.reportedOutputTokens != null && responseStats.reportedOutputTokens > 0n) ||
|
|
582
|
-
(responseStats.sellerClaimedCost != null && responseStats.sellerClaimedCost === 0n);
|
|
583
|
-
let estimatedInputTokens;
|
|
584
|
-
let estimatedOutputTokens;
|
|
585
|
-
let estimatedCachedInputTokens = 0n;
|
|
586
|
-
let buyerEstimatedRequestCost;
|
|
587
|
-
if (hasReportedTokens) {
|
|
588
|
-
estimatedInputTokens = responseStats.reportedInputTokens ?? 0n;
|
|
589
|
-
estimatedOutputTokens = responseStats.reportedOutputTokens ?? 0n;
|
|
590
|
-
const cachedInputTokens = responseStats.reportedCachedInputTokens ?? 0n;
|
|
591
|
-
estimatedCachedInputTokens = cachedInputTokens;
|
|
592
|
-
// For cost estimation, reportedInputTokens is normalized to total logical
|
|
593
|
-
// input tokens (fresh + cached), so split fresh by subtracting cached.
|
|
594
|
-
const freshInputTokens = cachedInputTokens > 0n
|
|
595
|
-
? BigInt(Math.max(0, Number(estimatedInputTokens) - Number(cachedInputTokens)))
|
|
596
|
-
: estimatedInputTokens;
|
|
597
|
-
// Compute cost from reported tokens using service-specific pricing
|
|
598
|
-
const pricing = this.getSessionPricing(sellerPeerId, responseStats.service);
|
|
599
|
-
if (pricing) {
|
|
600
|
-
const cost = computeCostUsdc(Number(freshInputTokens), Number(estimatedOutputTokens), pricing, Number(cachedInputTokens));
|
|
601
|
-
buyerEstimatedRequestCost = cost;
|
|
602
|
-
this._accumulateVerifiedCost(sellerPeerId, { cost, inputTokens: Number(estimatedInputTokens), outputTokens: Number(estimatedOutputTokens) });
|
|
603
|
-
}
|
|
604
|
-
else if (responseStats.sellerClaimedCost != null && responseStats.sellerClaimedCost > 0n) {
|
|
605
|
-
// No local pricing available (e.g. after buyer restart before session pricing is restored).
|
|
606
|
-
// Use seller's claimed cost for verifiedCost accumulation so _maxSignable can grow.
|
|
607
|
-
// The seller's claim is still validated against tolerance below.
|
|
608
|
-
buyerEstimatedRequestCost = responseStats.sellerClaimedCost;
|
|
609
|
-
this._accumulateVerifiedCost(sellerPeerId, { cost: responseStats.sellerClaimedCost, inputTokens: Number(estimatedInputTokens), outputTokens: Number(estimatedOutputTokens) });
|
|
610
|
-
}
|
|
611
|
-
else {
|
|
612
|
-
buyerEstimatedRequestCost = 0n;
|
|
613
|
-
}
|
|
614
|
-
debugLog(`[BuyerPayment] Using reported tokens: in=${estimatedInputTokens} cached=${cachedInputTokens} out=${estimatedOutputTokens} cost=${buyerEstimatedRequestCost}`);
|
|
615
|
-
}
|
|
616
|
-
else {
|
|
617
|
-
// Fall back to byte-based estimation
|
|
618
|
-
const estimate = this._estimateResponseCost(sellerPeerId, responseStats.inputBytes, responseStats.outputBytes, responseStats.service);
|
|
619
|
-
estimatedInputTokens = estimate ? BigInt(estimate.inputTokens) : 0n;
|
|
620
|
-
estimatedOutputTokens = estimate ? BigInt(estimate.outputTokens) : 0n;
|
|
621
|
-
buyerEstimatedRequestCost = estimate ? estimate.cost : 0n;
|
|
622
|
-
if (estimate) {
|
|
623
|
-
this._accumulateVerifiedCost(sellerPeerId, estimate);
|
|
624
|
-
}
|
|
625
|
-
debugLog(`[BuyerPayment] Byte-estimated tokens (no reported): in=${estimatedInputTokens} out=${estimatedOutputTokens} cost=${buyerEstimatedRequestCost}`);
|
|
626
|
-
}
|
|
627
|
-
// Determine the accepted cost for this request:
|
|
628
|
-
// When the seller reports a cost within tolerance, accept the seller's claim so the
|
|
629
|
-
// buyer doesn't underpay due to minor parsing differences. Only cap when the seller's
|
|
630
|
-
// claim exceeds the tolerance threshold.
|
|
631
|
-
let acceptedCost;
|
|
632
|
-
if (responseStats.sellerClaimedCost != null && responseStats.sellerClaimedCost > 0n) {
|
|
633
|
-
if (buyerEstimatedRequestCost > 0n) {
|
|
634
|
-
const maxAcceptable = BigInt(Math.ceil(Number(buyerEstimatedRequestCost) * this._costTolerance));
|
|
635
|
-
if (responseStats.sellerClaimedCost > maxAcceptable) {
|
|
636
|
-
debugWarn(`[BuyerPayment] Seller claimed ${responseStats.sellerClaimedCost} exceeds ${this._costTolerance}x buyer estimate ${buyerEstimatedRequestCost} — capping at ${maxAcceptable}`);
|
|
637
|
-
acceptedCost = maxAcceptable;
|
|
638
|
-
}
|
|
639
|
-
else {
|
|
640
|
-
// Seller's claim is within tolerance — accept it as-is
|
|
641
|
-
acceptedCost = responseStats.sellerClaimedCost;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
else {
|
|
645
|
-
// No buyer estimate available — accept seller's claim
|
|
646
|
-
acceptedCost = responseStats.sellerClaimedCost;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
else {
|
|
650
|
-
acceptedCost = buyerEstimatedRequestCost;
|
|
651
|
-
}
|
|
652
|
-
// If cost is 0, the cumulative amount stays the same — no spending auth needed
|
|
653
|
-
// but we still sign one to keep the seller's session alive.
|
|
654
|
-
// Advance cumulative amount by the accepted cost, then add overdraft headroom
|
|
655
|
-
// for the next request (so the seller has budget to serve it).
|
|
656
|
-
// maxSignable already caps at reserve ceiling, so one cap is sufficient
|
|
657
|
-
const prevAmount = this._cumulativeAmount.get(sellerPeerId) ?? 0n;
|
|
658
|
-
const maxSignable = this._maxSignable(sellerPeerId);
|
|
659
|
-
let newAmount = prevAmount + acceptedCost;
|
|
660
|
-
if (newAmount > maxSignable)
|
|
661
|
-
newAmount = maxSignable;
|
|
662
|
-
this._cumulativeAmount.set(sellerPeerId, newAmount);
|
|
663
|
-
const signedDelta = newAmount - prevAmount;
|
|
664
|
-
// Update cumulative metadata
|
|
665
|
-
// If handleNeedAuth already counted this request (seller-initiated NeedAuth
|
|
666
|
-
// raced ahead), skip service attribution entirely: service totals are
|
|
667
|
-
// documented as lower bounds, so undercounting beats double-counting.
|
|
668
|
-
const alreadyCounted = this._serviceTokensCounted.has(responseStats.requestId);
|
|
669
|
-
const newMeta = this._advanceUsageMetadata(this._metadata.get(sellerPeerId), alreadyCounted ? undefined : responseStats.service, {
|
|
670
|
-
amount: signedDelta,
|
|
671
|
-
inputTokens: estimatedInputTokens,
|
|
672
|
-
cachedInputTokens: estimatedCachedInputTokens,
|
|
673
|
-
outputTokens: estimatedOutputTokens,
|
|
674
|
-
requests: 1n,
|
|
675
|
-
});
|
|
676
|
-
if (!alreadyCounted)
|
|
677
|
-
this._serviceTokensCounted.mark(responseStats.requestId);
|
|
678
|
-
this._metadata.set(sellerPeerId, newMeta);
|
|
679
|
-
this._persistServiceMetadata(session.sessionId, newMeta);
|
|
680
|
-
this._reportSpend({
|
|
681
|
-
sellerPeerId,
|
|
682
|
-
requestId: responseStats.requestId ?? null,
|
|
683
|
-
amountUsdc: signedDelta.toString(),
|
|
684
|
-
inputTokens: (alreadyCounted ? 0n : estimatedInputTokens).toString(),
|
|
685
|
-
cachedInputTokens: (alreadyCounted ? 0n : estimatedCachedInputTokens).toString(),
|
|
686
|
-
outputTokens: (alreadyCounted ? 0n : estimatedOutputTokens).toString(),
|
|
687
|
-
});
|
|
688
|
-
debugLog(`[BuyerPayment] signPerRequestAuth #${newMeta.cumulativeRequestCount}: ` +
|
|
689
|
-
`thisReq in=${estimatedInputTokens} out=${estimatedOutputTokens} | ` +
|
|
690
|
-
`cumulative in=${newMeta.cumulativeInputTokens} out=${newMeta.cumulativeOutputTokens} | ` +
|
|
691
|
-
`acceptedCost=${acceptedCost} cumulativeAmount=${newAmount}`);
|
|
692
|
-
// Compute metadata hash and encode metadata
|
|
693
|
-
const metadataHashHex = computeMetadataHash(newMeta);
|
|
694
|
-
const encodedMetadata = encodeMetadata(newMeta);
|
|
695
|
-
// Sign EIP-712 SpendingAuth
|
|
696
|
-
const channelsDomain = this._channelsDomain;
|
|
697
|
-
const metadataMsg = {
|
|
698
|
-
channelId: session.sessionId,
|
|
699
|
-
cumulativeAmount: newAmount,
|
|
700
|
-
metadataHash: metadataHashHex,
|
|
701
|
-
};
|
|
702
|
-
const spendingAuthSig = await signSpendingAuth(this._signer, channelsDomain, metadataMsg);
|
|
703
|
-
// Persist updated cumulative values to ChannelStore
|
|
704
|
-
this._channelStore.upsertChannel({
|
|
705
|
-
...session,
|
|
706
|
-
authMax: newAmount.toString(),
|
|
707
|
-
requestCount: Number(newMeta.cumulativeRequestCount),
|
|
708
|
-
updatedAt: Date.now(),
|
|
709
|
-
});
|
|
710
|
-
const payload = {
|
|
711
|
-
channelId: session.sessionId,
|
|
712
|
-
cumulativeAmount: newAmount.toString(),
|
|
713
|
-
metadataHash: metadataHashHex,
|
|
714
|
-
metadata: encodedMetadata,
|
|
715
|
-
spendingAuthSig,
|
|
716
|
-
};
|
|
717
|
-
const topUpNeeded = this._needsTopUp(sellerPeerId);
|
|
718
|
-
return { payload, topUpNeeded };
|
|
719
|
-
}
|
|
720
|
-
// ── NeedAuth handler ───────────────────────────────────────────
|
|
721
|
-
/**
|
|
722
|
-
* Handle seller-initiated NeedAuth messages sent after every served request.
|
|
723
|
-
* The seller includes the cost of the last request; the buyer validates it
|
|
724
|
-
* against its own token count and signs a new SpendingAuth for the required
|
|
725
|
-
* cumulative amount, capped at the reserve ceiling.
|
|
726
|
-
*/
|
|
727
|
-
async handleNeedAuth(sellerPeerId, payload, paymentMux) {
|
|
728
|
-
const session = this.getActiveSession(sellerPeerId);
|
|
729
|
-
if (!session) {
|
|
730
|
-
debugWarn(`[BuyerPayment] NeedAuth for unknown seller: ${sellerPeerId.slice(0, 12)}...`);
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
const buyerService = this._requestService.get(payload.requestId);
|
|
734
|
-
const requiredCumulativeAmount = BigInt(payload.requiredCumulativeAmount);
|
|
735
|
-
const currentCumulative = this._cumulativeAmount.get(sellerPeerId) ?? 0n;
|
|
736
|
-
// Reject stale/lower NeedAuth (monotonicity guard)
|
|
737
|
-
if (requiredCumulativeAmount <= currentCumulative) {
|
|
738
|
-
debugLog(`[BuyerPayment] NeedAuth stale: required=${requiredCumulativeAmount} <= current=${currentCumulative} — ignoring`);
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
this._requestService.take(payload.requestId);
|
|
742
|
-
let acceptedServiceCost = 0n;
|
|
743
|
-
const reportedInputTokens = BigInt(payload.inputTokens ?? '0');
|
|
744
|
-
const reportedCachedInputTokens = BigInt(payload.cachedInputTokens ?? '0');
|
|
745
|
-
const reportedOutputTokens = BigInt(payload.outputTokens ?? '0');
|
|
746
|
-
// Validate the seller's claimed cost if reported
|
|
747
|
-
if (payload.lastRequestCost) {
|
|
748
|
-
const sellerCost = BigInt(payload.lastRequestCost);
|
|
749
|
-
const sellerIn = reportedInputTokens;
|
|
750
|
-
const sellerOut = reportedOutputTokens;
|
|
751
|
-
const sellerCached = reportedCachedInputTokens;
|
|
752
|
-
acceptedServiceCost = sellerCost;
|
|
753
|
-
// Prefer seller-supplied freshInputTokens to avoid OpenAI-vs-Anthropic
|
|
754
|
-
// cached-semantics ambiguity (OpenAI: prompt_tokens includes cached;
|
|
755
|
-
// Anthropic: input_tokens excludes cached). Fall back to OpenAI-style
|
|
756
|
-
// subtraction for older sellers that don't emit the field.
|
|
757
|
-
const sellerFreshExplicit = payload.freshInputTokens != null
|
|
758
|
-
? BigInt(payload.freshInputTokens)
|
|
759
|
-
: null;
|
|
760
|
-
// Use the buyer's own knowledge of which service it requested, not the seller's claim.
|
|
761
|
-
// A malicious seller could set service to a more expensive model to inflate the ceiling.
|
|
762
|
-
const pricing = this.getSessionPricing(sellerPeerId, buyerService);
|
|
763
|
-
if (pricing && sellerCost > 0n) {
|
|
764
|
-
const freshIn = sellerFreshExplicit ?? (sellerCached > 0n
|
|
765
|
-
? BigInt(Math.max(0, Number(sellerIn) - Number(sellerCached)))
|
|
766
|
-
: sellerIn);
|
|
767
|
-
const buyerEstimate = computeCostUsdc(Number(freshIn), Number(sellerOut), pricing, Number(sellerCached));
|
|
768
|
-
const maxAcceptable = BigInt(Math.ceil(Number(buyerEstimate) * this._costTolerance));
|
|
769
|
-
if (buyerEstimate > 0n && sellerCost > maxAcceptable) {
|
|
770
|
-
debugWarn(`[BuyerPayment] NeedAuth: seller claimed cost ${sellerCost} exceeds ${this._costTolerance}x buyer estimate ${buyerEstimate} — rejecting`);
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
debugLog(`[BuyerPayment] NeedAuth: seller cost=${sellerCost} buyer estimate=${buyerEstimate} (in=${sellerIn} cached=${sellerCached} out=${sellerOut}) — validated`);
|
|
774
|
-
}
|
|
775
|
-
else {
|
|
776
|
-
debugLog(`[BuyerPayment] NeedAuth: seller cost=${sellerCost} (no local pricing, accepting)`);
|
|
777
|
-
}
|
|
778
|
-
// Accumulate verified cost from the seller's report
|
|
779
|
-
this._accumulateVerifiedCost(sellerPeerId, {
|
|
780
|
-
cost: sellerCost,
|
|
781
|
-
inputTokens: Number(sellerIn),
|
|
782
|
-
outputTokens: Number(sellerOut),
|
|
783
|
-
});
|
|
784
|
-
}
|
|
785
|
-
// Cap at overdraft limit: verifiedCost + maxPerRequestUsdc, then at reserve ceiling.
|
|
786
|
-
// This prevents a malicious seller from claiming a small cost but requesting the full reserve.
|
|
787
|
-
let maxSignable = this._maxSignable(sellerPeerId);
|
|
788
|
-
const ceiling = this._getCeiling(sellerPeerId);
|
|
789
|
-
let needsTopUp = this._needsCeilingAdvance(requiredCumulativeAmount, maxSignable, ceiling);
|
|
790
|
-
if (maxSignable <= currentCumulative && !needsTopUp) {
|
|
791
|
-
maxSignable = this._reopenOverdraftWindowIfCollapsed(sellerPeerId, currentCumulative, maxSignable, 'handleNeedAuth');
|
|
792
|
-
needsTopUp = this._needsCeilingAdvance(requiredCumulativeAmount, maxSignable, ceiling);
|
|
793
|
-
if (maxSignable <= currentCumulative && !needsTopUp) {
|
|
794
|
-
debugWarn(`[BuyerPayment] NeedAuth: maxSignable=${maxSignable} <= currentCumulative=${currentCumulative} — overdraft limit reached`);
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
// When a topUp is needed, first sign at the current ceiling so the seller
|
|
799
|
-
// has a high-enough settled amount to pass the on-chain TopUpThresholdNotMet
|
|
800
|
-
// check (contract requires 85% of deposit to be settleable before topUp).
|
|
801
|
-
// We cap at the old ceiling here; the topUp is sent AFTER so the seller
|
|
802
|
-
// processes the SpendingAuth first, then the topUp with adequate settle amount.
|
|
803
|
-
const effectiveAmount = needsTopUp
|
|
804
|
-
? (maxSignable > currentCumulative ? maxSignable : currentCumulative)
|
|
805
|
-
: (requiredCumulativeAmount < maxSignable ? requiredCumulativeAmount : maxSignable);
|
|
806
|
-
if (effectiveAmount <= currentCumulative) {
|
|
807
|
-
// Nothing to sign — trigger topUp anyway to extend ceiling for next round
|
|
808
|
-
if (needsTopUp) {
|
|
809
|
-
await this._topUpAfterSpendAuthBestEffort(sellerPeerId, paymentMux, 'handleNeedAuth');
|
|
810
|
-
}
|
|
811
|
-
debugWarn(`[BuyerPayment] NeedAuth: effectiveAmount=${effectiveAmount} <= currentCumulative=${currentCumulative} — cannot advance`);
|
|
812
|
-
return;
|
|
813
|
-
}
|
|
814
|
-
debugLog(`[BuyerPayment] NeedAuth: channel=${session.sessionId.slice(0, 18)}... required=${requiredCumulativeAmount} effective=${effectiveAmount}`);
|
|
815
|
-
// Advance cumulative metadata. requestCount always increments so the
|
|
816
|
-
// on-chain metadata stays consistent even when older sellers omit token
|
|
817
|
-
// fields; absent token fields contribute 0 to the running totals.
|
|
818
|
-
const signedDelta = effectiveAmount - currentCumulative;
|
|
819
|
-
const serviceAmountDelta = acceptedServiceCost > 0n
|
|
820
|
-
? (acceptedServiceCost < signedDelta ? acceptedServiceCost : signedDelta)
|
|
821
|
-
: 0n;
|
|
822
|
-
// If signPerRequestAuth already counted this request (buyer-initiated auth
|
|
823
|
-
// raced ahead but didn't fully cover the seller's required cumulative), skip
|
|
824
|
-
// service attribution entirely: service totals are documented as lower
|
|
825
|
-
// bounds, so undercounting beats double-counting.
|
|
826
|
-
const alreadyCounted = this._serviceTokensCounted.has(payload.requestId);
|
|
827
|
-
const newMeta = this._advanceUsageMetadata(this._metadata.get(sellerPeerId), alreadyCounted ? undefined : buyerService, {
|
|
828
|
-
amount: serviceAmountDelta,
|
|
829
|
-
inputTokens: reportedInputTokens,
|
|
830
|
-
cachedInputTokens: reportedCachedInputTokens,
|
|
831
|
-
outputTokens: reportedOutputTokens,
|
|
832
|
-
requests: 1n,
|
|
833
|
-
});
|
|
834
|
-
if (!alreadyCounted)
|
|
835
|
-
this._serviceTokensCounted.mark(payload.requestId);
|
|
836
|
-
this._metadata.set(sellerPeerId, newMeta);
|
|
837
|
-
// Send via PaymentMux
|
|
838
|
-
try {
|
|
839
|
-
await this._sendUpdatedSpendingAuth(session, sellerPeerId, effectiveAmount, newMeta, paymentMux);
|
|
840
|
-
this._reportSpend({
|
|
841
|
-
sellerPeerId,
|
|
842
|
-
requestId: payload.requestId ?? null,
|
|
843
|
-
amountUsdc: signedDelta.toString(),
|
|
844
|
-
inputTokens: (alreadyCounted ? 0n : reportedInputTokens).toString(),
|
|
845
|
-
cachedInputTokens: (alreadyCounted ? 0n : reportedCachedInputTokens).toString(),
|
|
846
|
-
outputTokens: (alreadyCounted ? 0n : reportedOutputTokens).toString(),
|
|
847
|
-
});
|
|
848
|
-
debugLog(`[BuyerPayment] NeedAuth responded: new cumulativeAmount=${effectiveAmount}`);
|
|
849
|
-
}
|
|
850
|
-
catch {
|
|
851
|
-
debugLog(`[BuyerPayment] NeedAuth: connection closed before SpendingAuth could be sent`);
|
|
852
|
-
}
|
|
853
|
-
// Send topUp AFTER the SpendingAuth so the seller processes the higher
|
|
854
|
-
// cumulative first — this ensures the on-chain settle amount meets the
|
|
855
|
-
// contract's TopUpThresholdNotMet requirement (85% of deposit must be
|
|
856
|
-
// settleable before topUp is allowed). Also proactively send the top-up
|
|
857
|
-
// once the signed cumulative reaches the buyer's 65% threshold; the seller
|
|
858
|
-
// may defer the on-chain topUp until the contract's 85% gate is satisfied.
|
|
859
|
-
if (needsTopUp || this._needsTopUp(sellerPeerId)) {
|
|
860
|
-
await this._topUpAfterSpendAuthBestEffort(sellerPeerId, paymentMux, 'handleNeedAuth');
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
// ── Reserve top-up ─────────────────────────────────────────────
|
|
864
|
-
/**
|
|
865
|
-
* Sign a new ReserveAuth with a higher maxAmount to extend the session's reserve ceiling.
|
|
866
|
-
* The seller must call reserve() on-chain again with the new signature.
|
|
867
|
-
* Note: requires contract support for top-up (increaseDeposit on existing channelId).
|
|
868
|
-
*/
|
|
869
|
-
async topUpReserve(sellerPeerId, paymentMux) {
|
|
870
|
-
const session = this.getActiveSession(sellerPeerId);
|
|
871
|
-
if (!session) {
|
|
872
|
-
debugWarn(`[BuyerPayment] topUpReserve: no active session for ${sellerPeerId.slice(0, 12)}...`);
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
-
const prevCeiling = this._getCeiling(sellerPeerId);
|
|
876
|
-
const newCeiling = prevCeiling + this._config.maxReserveAmountUsdc;
|
|
877
|
-
const additionalReserve = newCeiling - prevCeiling;
|
|
878
|
-
const deadline = Math.floor(Date.now() / 1000) + this._config.defaultAuthDurationSecs;
|
|
879
|
-
try {
|
|
880
|
-
const balance = await this.getBalance();
|
|
881
|
-
if (balance.available < additionalReserve) {
|
|
882
|
-
throw new Error(`Insufficient buyer deposits for reserve top-up: available=${balance.available} required=${additionalReserve}`);
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
catch (err) {
|
|
886
|
-
if (err instanceof Error && err.message.startsWith('Insufficient buyer deposits')) {
|
|
887
|
-
throw err;
|
|
888
|
-
}
|
|
889
|
-
debugWarn(`[BuyerPayment] topUpReserve: unable to verify buyer deposits before signing top-up: ` +
|
|
890
|
-
`${err instanceof Error ? err.message : err}`);
|
|
891
|
-
}
|
|
892
|
-
debugLog(`[BuyerPayment] topUpReserve: channel=${session.sessionId.slice(0, 18)}... ceiling ${prevCeiling} → ${newCeiling}`);
|
|
893
|
-
// Sign ReserveAuth with new maxAmount
|
|
894
|
-
const channelsDomain = this._channelsDomain;
|
|
895
|
-
const reserveMsg = {
|
|
896
|
-
channelId: session.sessionId,
|
|
897
|
-
maxAmount: newCeiling,
|
|
898
|
-
deadline: BigInt(deadline),
|
|
899
|
-
};
|
|
900
|
-
const reserveAuthSig = await signReserveAuth(this._signer, channelsDomain, reserveMsg);
|
|
901
|
-
const currentCumulative = this._cumulativeAmount.get(sellerPeerId) ?? 0n;
|
|
902
|
-
const currentMeta = this._sanitizeMetadata(this._metadata.get(sellerPeerId));
|
|
903
|
-
const metadataHashHex = computeMetadataHash(currentMeta);
|
|
904
|
-
const encodedMetadata = encodeMetadata(currentMeta);
|
|
905
|
-
const salt = this._reserveSalt.get(sellerPeerId) ?? '0x' + '00'.repeat(32);
|
|
906
|
-
// Send ReserveAuth sig with reserve fields (same pattern as initial authorizeSpending).
|
|
907
|
-
// The seller uses this to call topUp() on-chain with the new maxAmount.
|
|
908
|
-
try {
|
|
909
|
-
paymentMux.sendSpendingAuth({
|
|
910
|
-
channelId: session.sessionId,
|
|
911
|
-
cumulativeAmount: currentCumulative.toString(),
|
|
912
|
-
metadataHash: metadataHashHex,
|
|
913
|
-
metadata: encodedMetadata,
|
|
914
|
-
spendingAuthSig: reserveAuthSig,
|
|
915
|
-
reserveSalt: salt,
|
|
916
|
-
reserveMaxAmount: newCeiling.toString(),
|
|
917
|
-
reserveDeadline: deadline,
|
|
918
|
-
});
|
|
919
|
-
// Only commit the new ceiling after the message is delivered
|
|
920
|
-
this._currentReserveCeiling.set(sellerPeerId, newCeiling);
|
|
921
|
-
debugLog(`[BuyerPayment] topUpReserve sent: newCeiling=${newCeiling}`);
|
|
922
|
-
}
|
|
923
|
-
catch {
|
|
924
|
-
debugLog(`[BuyerPayment] topUpReserve: connection closed before ReserveAuth could be sent`);
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
// ── Queries ───────────────────────────────────────────────────
|
|
928
|
-
/** Max USDC overdraft (unverified exposure) from buyer config. */
|
|
929
|
-
get maxPerRequestUsdc() {
|
|
930
|
-
return this._config.maxPerRequestUsdc;
|
|
931
|
-
}
|
|
932
|
-
/** Max USDC per ReserveAuth signature from buyer config. */
|
|
933
|
-
get maxReserveAmountUsdc() {
|
|
934
|
-
return this._config.maxReserveAmountUsdc;
|
|
935
|
-
}
|
|
936
|
-
/** Current buyer-verified cost for a seller. */
|
|
937
|
-
getVerifiedCost(sellerPeerId) {
|
|
938
|
-
return this._verifiedCost.get(sellerPeerId) ?? 0n;
|
|
939
|
-
}
|
|
940
|
-
/** Current reserve ceiling for a seller (may be higher than initial after top-ups). */
|
|
941
|
-
getReserveCeiling(sellerPeerId) {
|
|
942
|
-
return this._currentReserveCeiling.get(sellerPeerId) ?? this._config.maxReserveAmountUsdc;
|
|
943
|
-
}
|
|
944
|
-
/** Current cumulative signed amount for a seller. */
|
|
945
|
-
getCumulativeAmount(sellerPeerId) {
|
|
946
|
-
return this._cumulativeAmount.get(sellerPeerId) ?? 0n;
|
|
947
|
-
}
|
|
948
|
-
/** Live cumulative token counts for a seller (in-memory, always up-to-date). */
|
|
949
|
-
getCumulativeTokens(sellerPeerId) {
|
|
950
|
-
const meta = this._sanitizeMetadata(this._metadata.get(sellerPeerId));
|
|
951
|
-
return { inputTokens: meta.cumulativeInputTokens, outputTokens: meta.cumulativeOutputTokens };
|
|
952
|
-
}
|
|
953
|
-
/**
|
|
954
|
-
* Accumulate response token counts and persist to the channel store.
|
|
955
|
-
* Tracks its own running totals independently of signPerRequestAuth metadata,
|
|
956
|
-
* so the persisted data is always up-to-date after each response.
|
|
957
|
-
*/
|
|
958
|
-
recordAndPersistTokens(sellerPeerId, inputTokens, outputTokens) {
|
|
959
|
-
const session = this.getActiveSession(sellerPeerId);
|
|
960
|
-
if (!session)
|
|
961
|
-
return;
|
|
962
|
-
const prev = this._responseTokenTotals.get(sellerPeerId) ?? {
|
|
963
|
-
input: Number(session.tokensDelivered),
|
|
964
|
-
output: Number(session.previousConsumption),
|
|
965
|
-
requests: session.requestCount,
|
|
966
|
-
};
|
|
967
|
-
debugLog(`[BuyerPayment] recordTokens: thisReq in=${inputTokens} out=${outputTokens} | ` +
|
|
968
|
-
`prevTotal in=${prev.input} out=${prev.output} reqs=${prev.requests}`);
|
|
969
|
-
const totals = {
|
|
970
|
-
input: prev.input + inputTokens,
|
|
971
|
-
output: prev.output + outputTokens,
|
|
972
|
-
requests: prev.requests + 1,
|
|
973
|
-
};
|
|
974
|
-
this._responseTokenTotals.set(sellerPeerId, totals);
|
|
975
|
-
this._channelStore.upsertChannel({
|
|
976
|
-
...session,
|
|
977
|
-
tokensDelivered: String(totals.input),
|
|
978
|
-
previousConsumption: String(totals.output),
|
|
979
|
-
requestCount: totals.requests,
|
|
980
|
-
updatedAt: Date.now(),
|
|
981
|
-
});
|
|
982
|
-
}
|
|
983
|
-
/** Register which service the buyer requested for a given requestId. */
|
|
984
|
-
trackRequestService(requestId, service) {
|
|
985
|
-
this._requestService.track(requestId, service);
|
|
986
|
-
}
|
|
987
|
-
/** Get the live response token totals for a seller, or null if none recorded this session. */
|
|
988
|
-
getResponseTokenTotals(sellerPeerId) {
|
|
989
|
-
return this._responseTokenTotals.get(sellerPeerId) ?? null;
|
|
990
|
-
}
|
|
991
|
-
/** Get the session pricing for a seller+service. Resolves service-specific pricing first, then defaults. */
|
|
992
|
-
getSessionPricing(sellerPeerId, service) {
|
|
993
|
-
const map = this._sessionPricing.get(sellerPeerId);
|
|
994
|
-
if (!map)
|
|
995
|
-
return null;
|
|
996
|
-
if (service) {
|
|
997
|
-
const servicePricing = map.services[service];
|
|
998
|
-
if (servicePricing)
|
|
999
|
-
return servicePricing;
|
|
1000
|
-
}
|
|
1001
|
-
return map.defaults;
|
|
1002
|
-
}
|
|
1003
|
-
/** Check if a session has been confirmed via AuthAck. */
|
|
1004
|
-
isAuthorized(sellerPeerId) {
|
|
1005
|
-
return this._confirmedPeers.has(sellerPeerId);
|
|
1006
|
-
}
|
|
1007
|
-
/** Alias for isAuthorized (used by polling loop). */
|
|
1008
|
-
isLockConfirmed(sellerPeerId) {
|
|
1009
|
-
return this.isAuthorized(sellerPeerId);
|
|
1010
|
-
}
|
|
1011
|
-
/** Check if the lock was explicitly rejected (not just never-contacted). */
|
|
1012
|
-
isLockRejected(sellerPeerId) {
|
|
1013
|
-
return this._rejectedPeers.has(sellerPeerId);
|
|
1014
|
-
}
|
|
1015
|
-
/** Mark a peer as having rejected our spending auth. */
|
|
1016
|
-
markRejected(sellerPeerId) {
|
|
1017
|
-
this._rejectedPeers.add(sellerPeerId);
|
|
1018
|
-
debugLog(`[BuyerPayment] Peer ${sellerPeerId.slice(0, 12)}... marked as rejected`);
|
|
1019
|
-
}
|
|
1020
|
-
getSessionHistory(sellerPeerId) {
|
|
1021
|
-
const session = this._channelStore.getLatestChannelByPeerAndBuyer(sellerPeerId, 'buyer', this._identity.wallet.address);
|
|
1022
|
-
return session ? [session] : [];
|
|
1023
|
-
}
|
|
1024
|
-
// ── Deposit operations ──────────────────────────────────────────
|
|
1025
|
-
async deposit(amount) {
|
|
1026
|
-
debugLog(`[BuyerPayment] Depositing ${amount} to deposits`);
|
|
1027
|
-
const buyer = this._identity.wallet.address;
|
|
1028
|
-
return this._depositsClient.deposit(this._signer, buyer, amount);
|
|
1029
|
-
}
|
|
1030
|
-
async withdraw(amount) {
|
|
1031
|
-
debugLog(`[BuyerPayment] Withdrawing ${amount} from deposits`);
|
|
1032
|
-
return this._depositsClient.withdraw(this._signer, this._identity.wallet.address, amount);
|
|
1033
|
-
}
|
|
1034
|
-
async getBalance() {
|
|
1035
|
-
const buyerAddr = this._identity.wallet.address;
|
|
1036
|
-
const info = await this._depositsClient.getBuyerBalance(buyerAddr);
|
|
1037
|
-
return { available: info.available, reserved: info.reserved };
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1
|
+
// Moved to @antseed/buyer-core; re-exported for compatibility.
|
|
2
|
+
export * from '@antseed/buyer-core/buyer-payment-manager';
|
|
1040
3
|
//# sourceMappingURL=buyer-payment-manager.js.map
|