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