@antseed/node 0.2.26 → 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 +85 -11
- 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 -2
- package/dist/discovery/announcer.d.ts.map +1 -1
- package/dist/discovery/announcer.js +11 -14
- package/dist/discovery/announcer.js.map +1 -1
- package/dist/discovery/index.d.ts +0 -2
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/index.js +0 -2
- 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 -22
- package/dist/discovery/reputation-verifier.d.ts.map +1 -1
- package/dist/discovery/reputation-verifier.js +2 -24
- 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 +17 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -3
- 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 +83 -104
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +313 -1026
- 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 +12 -6
- package/dist/p2p/connection-manager.js.map +1 -1
- package/dist/p2p/identity.d.ts +44 -13
- package/dist/p2p/identity.d.ts.map +1 -1
- package/dist/p2p/identity.js +103 -49
- package/dist/p2p/identity.js.map +1 -1
- package/dist/p2p/index.d.ts +1 -3
- package/dist/p2p/index.d.ts.map +1 -1
- package/dist/p2p/index.js +1 -3
- package/dist/p2p/index.js.map +1 -1
- package/dist/p2p/payment-codec.d.ts +9 -19
- package/dist/p2p/payment-codec.d.ts.map +1 -1
- package/dist/p2p/payment-codec.js +41 -89
- package/dist/p2p/payment-codec.js.map +1 -1
- package/dist/p2p/payment-mux.d.ts +14 -29
- package/dist/p2p/payment-mux.d.ts.map +1 -1
- package/dist/p2p/payment-mux.js +41 -79
- 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 +157 -83
- package/dist/payments/buyer-payment-manager.d.ts.map +1 -1
- package/dist/payments/buyer-payment-manager.js +573 -204
- 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 +44 -0
- package/dist/payments/chain-config.d.ts.map +1 -0
- package/dist/payments/chain-config.js +70 -0
- package/dist/payments/chain-config.js.map +1 -0
- 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 +16 -0
- package/dist/payments/evm/ants-token-client.d.ts.map +1 -0
- package/dist/payments/evm/ants-token-client.js +65 -0
- package/dist/payments/evm/ants-token-client.js.map +1 -0
- package/dist/payments/evm/base-evm-client.d.ts +22 -0
- package/dist/payments/evm/base-evm-client.d.ts.map +1 -0
- package/dist/payments/evm/base-evm-client.js +71 -0
- package/dist/payments/evm/base-evm-client.js.map +1 -0
- 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 +22 -0
- package/dist/payments/evm/emissions-client.d.ts.map +1 -0
- package/dist/payments/evm/emissions-client.js +65 -0
- package/dist/payments/evm/emissions-client.js.map +1 -0
- package/dist/payments/evm/escrow-client.d.ts +57 -36
- package/dist/payments/evm/escrow-client.d.ts.map +1 -1
- package/dist/payments/evm/escrow-client.js +200 -93
- package/dist/payments/evm/escrow-client.js.map +1 -1
- package/dist/payments/evm/identity-client.d.ts +21 -0
- package/dist/payments/evm/identity-client.d.ts.map +1 -0
- package/dist/payments/evm/identity-client.js +68 -0
- package/dist/payments/evm/identity-client.js.map +1 -0
- 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 +54 -10
- package/dist/payments/evm/signatures.d.ts.map +1 -1
- package/dist/payments/evm/signatures.js +80 -54
- 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/evm/subpool-client.d.ts +30 -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 +29 -9
- package/dist/payments/index.d.ts.map +1 -1
- package/dist/payments/index.js +27 -9
- 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 +13 -0
- package/dist/payments/readiness.d.ts.map +1 -0
- package/dist/payments/readiness.js +57 -0
- package/dist/payments/readiness.js.map +1 -0
- package/dist/payments/seller-payment-manager.d.ts +101 -36
- package/dist/payments/seller-payment-manager.d.ts.map +1 -1
- package/dist/payments/seller-payment-manager.js +612 -120
- package/dist/payments/seller-payment-manager.js.map +1 -1
- package/dist/payments/session-store.d.ts +68 -0
- package/dist/payments/session-store.d.ts.map +1 -0
- package/dist/payments/session-store.js +272 -0
- package/dist/payments/session-store.js.map +1 -0
- package/dist/payments/types.d.ts +5 -3
- package/dist/payments/types.d.ts.map +1 -1
- package/dist/payments/usdc-utils.d.ts +9 -0
- package/dist/payments/usdc-utils.d.ts.map +1 -0
- package/dist/payments/usdc-utils.js +17 -0
- package/dist/payments/usdc-utils.js.map +1 -0
- 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/http.d.ts +2 -0
- package/dist/types/http.d.ts.map +1 -1
- package/dist/types/http.js +2 -0
- package/dist/types/http.js.map +1 -1
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/metering.d.ts +2 -2
- package/dist/types/metering.d.ts.map +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 +32 -97
- package/dist/types/protocol.d.ts.map +1 -1
- package/dist/types/protocol.js +5 -10
- 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 +4 -3
|
@@ -1,280 +1,649 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { buildLockMessageHash, buildSettlementMessageHash, buildExtendLockMessageHash, signMessageEcdsa, buildAckMessage, signMessageEd25519, } from './evm/signatures.js';
|
|
5
|
-
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';
|
|
6
4
|
import { debugLog, debugWarn } from '../utils/debug.js';
|
|
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;
|
|
7
16
|
/**
|
|
8
|
-
* Manages buyer-side
|
|
9
|
-
*
|
|
10
|
-
* Handles the full lifecycle: lock initiation, receipt acknowledgement,
|
|
11
|
-
* top-up approval, and session settlement.
|
|
17
|
+
* Manages buyer-side payment sessions using EIP-712 SpendingAuth
|
|
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
|
-
|
|
19
|
-
|
|
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
|
+
/** 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) {
|
|
20
47
|
this._identity = identity;
|
|
21
48
|
this._config = config;
|
|
22
|
-
this._signer =
|
|
23
|
-
this.
|
|
49
|
+
this._signer = identity.wallet;
|
|
50
|
+
this._depositsClient = new DepositsClient({
|
|
24
51
|
rpcUrl: config.rpcUrl,
|
|
25
|
-
contractAddress: config.
|
|
52
|
+
contractAddress: config.depositsContractAddress,
|
|
26
53
|
usdcAddress: config.usdcAddress,
|
|
27
54
|
});
|
|
55
|
+
this._channelStore = channelStore;
|
|
56
|
+
this._channelsDomain = makeChannelsDomain(config.chainId, config.channelsContractAddress);
|
|
57
|
+
// Hydrate cumulative maps from persisted active sessions
|
|
58
|
+
this._hydrateFromStore();
|
|
28
59
|
}
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
}
|
|
31
75
|
}
|
|
32
|
-
|
|
33
|
-
get wallet() {
|
|
76
|
+
get signer() {
|
|
34
77
|
return this._signer;
|
|
35
78
|
}
|
|
36
|
-
/** Replace the signer at runtime (e.g. with a WalletConnect signer). */
|
|
37
79
|
setSigner(signer) {
|
|
38
80
|
this._signer = signer;
|
|
39
81
|
}
|
|
40
|
-
get
|
|
41
|
-
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);
|
|
42
115
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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;
|
|
46
139
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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;
|
|
50
167
|
}
|
|
51
|
-
// ──
|
|
168
|
+
// ── Spending Authorization ────────────────────────────────────
|
|
52
169
|
/**
|
|
53
|
-
*
|
|
54
|
-
* to the seller
|
|
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.
|
|
55
174
|
*/
|
|
56
|
-
async
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
}
|
|
188
|
+
// Clear confirmation state so we wait for a fresh AuthAck on the new session
|
|
189
|
+
this._confirmedPeers.delete(sellerPeerId);
|
|
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);
|
|
198
|
+
const deadline = Math.floor(Date.now() / 1000) + this._config.defaultAuthDurationSecs;
|
|
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),
|
|
207
|
+
};
|
|
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);
|
|
215
|
+
// Store session
|
|
67
216
|
const now = Date.now();
|
|
68
217
|
const session = {
|
|
69
|
-
sessionId,
|
|
70
|
-
sellerPeerId,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
218
|
+
sessionId: channelId,
|
|
219
|
+
peerId: sellerPeerId,
|
|
220
|
+
role: 'buyer',
|
|
221
|
+
sellerEvmAddr: peerIdToAddress(sellerPeerId),
|
|
222
|
+
buyerEvmAddr: this._identity.wallet.address,
|
|
223
|
+
nonce: 0,
|
|
224
|
+
authMax: minBudgetPerRequest.toString(),
|
|
225
|
+
deadline,
|
|
226
|
+
previousSessionId: '0x' + '0'.repeat(64),
|
|
227
|
+
previousConsumption: '0',
|
|
228
|
+
tokensDelivered: '0',
|
|
229
|
+
requestCount: 0,
|
|
230
|
+
reservedAt: now,
|
|
231
|
+
settledAt: null,
|
|
232
|
+
settledAmount: null,
|
|
233
|
+
status: 'active',
|
|
234
|
+
latestBuyerSig: null,
|
|
235
|
+
latestSpendingAuthSig: null,
|
|
236
|
+
latestMetadata: null,
|
|
77
237
|
createdAt: now,
|
|
78
238
|
updatedAt: now,
|
|
79
239
|
};
|
|
80
|
-
this.
|
|
81
|
-
// Send
|
|
82
|
-
paymentMux.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
240
|
+
this._channelStore.upsertChannel(session);
|
|
241
|
+
// Send SpendingAuth via PaymentMux — reserve carries ReserveAuth sig
|
|
242
|
+
paymentMux.sendSpendingAuth({
|
|
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,
|
|
86
251
|
});
|
|
87
|
-
return
|
|
252
|
+
return channelId;
|
|
88
253
|
}
|
|
89
|
-
// ──
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
*/
|
|
93
|
-
handleLockConfirm(sellerPeerId, payload) {
|
|
94
|
-
const session = this._sessions.get(sellerPeerId);
|
|
254
|
+
// ── AuthAck handler ───────────────────────────────────────────
|
|
255
|
+
handleAuthAck(sellerPeerId, payload) {
|
|
256
|
+
const session = this.getActiveSession(sellerPeerId);
|
|
95
257
|
if (!session) {
|
|
96
|
-
debugWarn(`[BuyerPayment]
|
|
258
|
+
debugWarn(`[BuyerPayment] AuthAck for unknown seller: ${sellerPeerId.slice(0, 12)}...`);
|
|
97
259
|
return;
|
|
98
260
|
}
|
|
99
|
-
if (session.sessionId !== payload.
|
|
100
|
-
debugWarn(`[BuyerPayment]
|
|
261
|
+
if (session.sessionId !== payload.channelId) {
|
|
262
|
+
debugWarn(`[BuyerPayment] AuthAck channel mismatch: expected=${session.sessionId.slice(0, 18)}... got=${payload.channelId.slice(0, 18)}...`);
|
|
101
263
|
return;
|
|
102
264
|
}
|
|
103
|
-
|
|
104
|
-
session.
|
|
105
|
-
session.updatedAt = Date.now();
|
|
106
|
-
debugLog(`[BuyerPayment] Lock confirmed: session=${session.sessionId.slice(0, 18)}... tx=${payload.txSignature.slice(0, 12)}...`);
|
|
265
|
+
this._confirmedPeers.add(sellerPeerId);
|
|
266
|
+
debugLog(`[BuyerPayment] AuthAck confirmed: channel=${session.sessionId.slice(0, 18)}...`);
|
|
107
267
|
}
|
|
268
|
+
// ── Buyer-side cost verification ──────────────────────────────
|
|
108
269
|
/**
|
|
109
|
-
*
|
|
270
|
+
* Estimate tokens and cost from response content without updating state.
|
|
110
271
|
*/
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
if (!
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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;
|
|
119
318
|
}
|
|
120
|
-
// ── Receipt handling ────────────────────────────────────────────
|
|
121
319
|
/**
|
|
122
|
-
*
|
|
123
|
-
*
|
|
320
|
+
* Check whether the current cumulative amount is approaching the reserve ceiling
|
|
321
|
+
* and a top-up should be triggered.
|
|
124
322
|
*/
|
|
125
|
-
|
|
126
|
-
const
|
|
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);
|
|
127
345
|
if (!session) {
|
|
128
|
-
|
|
129
|
-
return;
|
|
346
|
+
throw new Error(`[BuyerPayment] No active session for seller ${sellerPeerId.slice(0, 12)}... — call authorizeSpending() first`);
|
|
130
347
|
}
|
|
131
|
-
|
|
132
|
-
|
|
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);
|
|
133
355
|
}
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const ackMsg = buildAckMessage(sessionIdBytes, BigInt(receipt.runningTotal), receipt.requestCount);
|
|
144
|
-
const sigBytes = await signMessageEd25519(this._identity, ackMsg);
|
|
145
|
-
const buyerSig = bytesToHex(sigBytes);
|
|
146
|
-
paymentMux.sendBuyerAck({
|
|
147
|
-
sessionId: session.sessionId,
|
|
148
|
-
runningTotal: receipt.runningTotal,
|
|
149
|
-
requestCount: receipt.requestCount,
|
|
150
|
-
buyerSig,
|
|
151
|
-
});
|
|
152
|
-
debugLog(`[BuyerPayment] Auto-ack sent for session=${session.sessionId.slice(0, 18)}...`);
|
|
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
|
+
}
|
|
153
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 };
|
|
154
419
|
}
|
|
155
|
-
// ──
|
|
420
|
+
// ── NeedAuth handler ───────────────────────────────────────────
|
|
156
421
|
/**
|
|
157
|
-
* Handle
|
|
158
|
-
*
|
|
159
|
-
* Otherwise, end the session.
|
|
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).
|
|
160
424
|
*/
|
|
161
|
-
async
|
|
162
|
-
const session = this.
|
|
425
|
+
async handleNeedAuth(sellerPeerId, payload, paymentMux) {
|
|
426
|
+
const session = this.getActiveSession(sellerPeerId);
|
|
163
427
|
if (!session) {
|
|
164
|
-
debugWarn(`[BuyerPayment]
|
|
428
|
+
debugWarn(`[BuyerPayment] NeedAuth for unknown seller: ${sellerPeerId.slice(0, 12)}...`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
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`);
|
|
165
436
|
return;
|
|
166
437
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (account.available >= additionalAmount) {
|
|
177
|
-
// Sign extend-lock authorization
|
|
178
|
-
const messageHash = buildExtendLockMessageHash(session.sessionId, session.sellerEvmAddress, additionalAmount);
|
|
179
|
-
const buyerSig = await signMessageEcdsa(this._signer, messageHash);
|
|
180
|
-
session.lockedAmount = newTotal;
|
|
181
|
-
session.updatedAt = Date.now();
|
|
182
|
-
paymentMux.sendTopUpAuth({
|
|
183
|
-
sessionId: session.sessionId,
|
|
184
|
-
additionalAmount: request.additionalAmount,
|
|
185
|
-
buyerSig,
|
|
186
|
-
});
|
|
187
|
-
debugLog(`[BuyerPayment] Top-up authorized: session=${session.sessionId.slice(0, 18)}...`);
|
|
188
|
-
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);
|
|
444
|
+
}
|
|
445
|
+
catch (err) {
|
|
446
|
+
debugWarn(`[BuyerPayment] NeedAuth: topUpReserve failed: ${err instanceof Error ? err.message : err}`);
|
|
189
447
|
}
|
|
190
|
-
|
|
448
|
+
maxSignable = this._maxSignable(sellerPeerId);
|
|
449
|
+
}
|
|
450
|
+
if (maxSignable <= currentCumulative) {
|
|
451
|
+
debugWarn(`[BuyerPayment] NeedAuth: maxSignable=${maxSignable} <= currentCumulative=${currentCumulative} — cannot authorize more (overdraft limit reached)`);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
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(),
|
|
475
|
+
});
|
|
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,
|
|
484
|
+
});
|
|
485
|
+
debugLog(`[BuyerPayment] NeedAuth responded: new cumulativeAmount=${effectiveAmount}`);
|
|
486
|
+
}
|
|
487
|
+
catch {
|
|
488
|
+
debugLog(`[BuyerPayment] NeedAuth: connection closed before SpendingAuth could be sent`);
|
|
191
489
|
}
|
|
192
|
-
// Cannot or will not top up — end the session
|
|
193
|
-
debugLog(`[BuyerPayment] Declining top-up, ending session=${session.sessionId.slice(0, 18)}...`);
|
|
194
|
-
await this.endSession(sellerPeerId, paymentMux, 80);
|
|
195
490
|
}
|
|
196
|
-
// ──
|
|
491
|
+
// ── Reserve top-up ─────────────────────────────────────────────
|
|
197
492
|
/**
|
|
198
|
-
*
|
|
199
|
-
* with
|
|
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).
|
|
200
496
|
*/
|
|
201
|
-
async
|
|
202
|
-
const session = this.
|
|
497
|
+
async topUpReserve(sellerPeerId, paymentMux) {
|
|
498
|
+
const session = this.getActiveSession(sellerPeerId);
|
|
203
499
|
if (!session) {
|
|
204
|
-
debugWarn(`[BuyerPayment]
|
|
500
|
+
debugWarn(`[BuyerPayment] topUpReserve: no active session for ${sellerPeerId.slice(0, 12)}...`);
|
|
205
501
|
return;
|
|
206
502
|
}
|
|
207
|
-
|
|
208
|
-
|
|
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`);
|
|
209
539
|
}
|
|
210
|
-
session.status = 'ending';
|
|
211
|
-
session.updatedAt = Date.now();
|
|
212
|
-
debugLog(`[BuyerPayment] Ending session=${session.sessionId.slice(0, 18)}... total=${session.lastRunningTotal} score=${score}`);
|
|
213
|
-
// Sign settlement message with ECDSA
|
|
214
|
-
const messageHash = buildSettlementMessageHash(session.sessionId, session.lastRunningTotal, score);
|
|
215
|
-
const buyerSig = await signMessageEcdsa(this._signer, messageHash);
|
|
216
|
-
paymentMux.sendSessionEnd({
|
|
217
|
-
sessionId: session.sessionId,
|
|
218
|
-
runningTotal: session.lastRunningTotal.toString(),
|
|
219
|
-
requestCount: session.lastRequestCount,
|
|
220
|
-
score,
|
|
221
|
-
buyerSig,
|
|
222
|
-
});
|
|
223
|
-
session.status = 'ended';
|
|
224
|
-
session.updatedAt = Date.now();
|
|
225
|
-
debugLog(`[BuyerPayment] Session ended: ${session.sessionId.slice(0, 18)}...`);
|
|
226
540
|
}
|
|
227
|
-
// ──
|
|
228
|
-
/**
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
*/
|
|
232
|
-
async deposit(amount) {
|
|
233
|
-
debugLog(`[BuyerPayment] Depositing ${amount} to escrow`);
|
|
234
|
-
return this._escrowClient.deposit(this._signer, amount);
|
|
541
|
+
// ── Queries ───────────────────────────────────────────────────
|
|
542
|
+
/** Max USDC overdraft (unverified exposure) from buyer config. */
|
|
543
|
+
get maxPerRequestUsdc() {
|
|
544
|
+
return this._config.maxPerRequestUsdc;
|
|
235
545
|
}
|
|
236
|
-
/**
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
*/
|
|
240
|
-
async withdraw(amount) {
|
|
241
|
-
debugLog(`[BuyerPayment] Withdrawing ${amount} from escrow`);
|
|
242
|
-
return this._escrowClient.withdraw(this._signer, amount);
|
|
546
|
+
/** Max USDC per ReserveAuth signature from buyer config. */
|
|
547
|
+
get maxReserveAmountUsdc() {
|
|
548
|
+
return this._config.maxReserveAmountUsdc;
|
|
243
549
|
}
|
|
244
|
-
/**
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
async getBalance() {
|
|
248
|
-
const buyerAddr = identityToEvmAddress(this._identity);
|
|
249
|
-
return this._escrowClient.getBuyerAccount(buyerAddr);
|
|
550
|
+
/** Current buyer-verified cost for a seller. */
|
|
551
|
+
getVerifiedCost(sellerPeerId) {
|
|
552
|
+
return this._verifiedCost.get(sellerPeerId) ?? 0n;
|
|
250
553
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
*/
|
|
255
|
-
async releaseExpiredLock(sessionId) {
|
|
256
|
-
debugLog(`[BuyerPayment] Releasing expired lock: session=${sessionId.slice(0, 18)}...`);
|
|
257
|
-
return this._escrowClient.releaseExpiredLock(this._signer, sessionId);
|
|
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;
|
|
258
557
|
}
|
|
259
|
-
/**
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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 };
|
|
265
566
|
}
|
|
266
567
|
/**
|
|
267
|
-
*
|
|
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.
|
|
268
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
|
+
}
|
|
595
|
+
/** Check if a session has been confirmed via AuthAck. */
|
|
596
|
+
isAuthorized(sellerPeerId) {
|
|
597
|
+
return this._confirmedPeers.has(sellerPeerId);
|
|
598
|
+
}
|
|
599
|
+
/** Alias for isAuthorized (used by polling loop). */
|
|
269
600
|
isLockConfirmed(sellerPeerId) {
|
|
270
|
-
|
|
271
|
-
return session?.status === 'confirmed' || session?.status === 'active';
|
|
601
|
+
return this.isAuthorized(sellerPeerId);
|
|
272
602
|
}
|
|
273
|
-
/**
|
|
274
|
-
* Check if a session lock has been rejected (for polling).
|
|
275
|
-
*/
|
|
603
|
+
/** Check if the lock was explicitly rejected (not just never-contacted). */
|
|
276
604
|
isLockRejected(sellerPeerId) {
|
|
277
|
-
return
|
|
605
|
+
return this._rejectedPeers.has(sellerPeerId);
|
|
606
|
+
}
|
|
607
|
+
/** Mark a peer as having rejected our spending auth. */
|
|
608
|
+
markRejected(sellerPeerId) {
|
|
609
|
+
this._rejectedPeers.add(sellerPeerId);
|
|
610
|
+
debugLog(`[BuyerPayment] Peer ${sellerPeerId.slice(0, 12)}... marked as rejected`);
|
|
611
|
+
}
|
|
612
|
+
getSessionHistory(sellerPeerId) {
|
|
613
|
+
const session = this._channelStore.getLatestChannelByPeerAndBuyer(sellerPeerId, 'buyer', this._identity.wallet.address);
|
|
614
|
+
return session ? [session] : [];
|
|
615
|
+
}
|
|
616
|
+
// ── Deposit operations ──────────────────────────────────────────
|
|
617
|
+
async deposit(amount) {
|
|
618
|
+
debugLog(`[BuyerPayment] Depositing ${amount} to deposits`);
|
|
619
|
+
const buyer = this._identity.wallet.address;
|
|
620
|
+
return this._depositsClient.deposit(this._signer, buyer, amount);
|
|
621
|
+
}
|
|
622
|
+
async withdraw(amount) {
|
|
623
|
+
debugLog(`[BuyerPayment] Withdrawing ${amount} from deposits`);
|
|
624
|
+
return this._depositsClient.withdraw(this._signer, this._identity.wallet.address, amount);
|
|
625
|
+
}
|
|
626
|
+
async getBalance() {
|
|
627
|
+
const buyerAddr = this._identity.wallet.address;
|
|
628
|
+
const info = await this._depositsClient.getBuyerBalance(buyerAddr);
|
|
629
|
+
return { available: info.available, reserved: info.reserved };
|
|
630
|
+
}
|
|
631
|
+
// ── Response cost parsing ──────────────────────────────────────
|
|
632
|
+
static parseResponseCost(headers) {
|
|
633
|
+
const costStr = headers[HEADER_COST];
|
|
634
|
+
if (costStr === undefined || costStr === '')
|
|
635
|
+
return null;
|
|
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
|
+
}
|
|
278
647
|
}
|
|
279
648
|
}
|
|
280
649
|
//# sourceMappingURL=buyer-payment-manager.js.map
|