@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
package/dist/node.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
3
2
|
import { homedir } from "node:os";
|
|
4
3
|
import { join } from "node:path";
|
|
5
4
|
import { loadOrCreateIdentity } from "./p2p/identity.js";
|
|
6
|
-
import {
|
|
5
|
+
import { peerIdToAddress } from "./types/peer.js";
|
|
7
6
|
import { MeteringStorage } from "./metering/storage.js";
|
|
8
7
|
import { ReceiptGenerator } from "./metering/receipt-generator.js";
|
|
8
|
+
import { SellerSessionTracker, } from "./metering/seller-session-tracker.js";
|
|
9
9
|
import { ConnectionState } from "./types/connection.js";
|
|
10
10
|
import { DHTNode, DEFAULT_DHT_CONFIG, } from "./discovery/dht-node.js";
|
|
11
11
|
import { toBootstrapConfig, OFFICIAL_BOOTSTRAP_NODES, mergeBootstrapNodes } from "./discovery/bootstrap.js";
|
|
@@ -19,16 +19,17 @@ import { FrameDecoder, encodeFrame } from "./p2p/message-protocol.js";
|
|
|
19
19
|
import { KeepaliveManager, buildPongPayload } from "./p2p/keepalive.js";
|
|
20
20
|
import { MessageType } from "./types/protocol.js";
|
|
21
21
|
import { NatTraversal } from "./p2p/nat-traversal.js";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import { BalanceManager, BaseEscrowClient, identityToEvmWallet, buildLockMessageHash, buildReceiptMessage, buildAckMessage, signMessageEd25519, verifyMessageEd25519, } from "./payments/index.js";
|
|
25
|
-
import { hexToBytes, bytesToHex } from "./utils/hex.js";
|
|
22
|
+
import { signUtf8 } from "./p2p/identity.js";
|
|
23
|
+
import { BalanceManager, DepositsClient, ChannelsClient, StakingClient, ChannelStore, } from "./payments/index.js";
|
|
26
24
|
import { debugLog, debugWarn } from "./utils/debug.js";
|
|
27
25
|
import { parsePublicAddress } from "./discovery/public-address.js";
|
|
28
26
|
import { BuyerPaymentManager } from "./payments/buyer-payment-manager.js";
|
|
29
|
-
import {
|
|
27
|
+
import { BuyerPaymentNegotiator } from "./payments/buyer-payment-negotiator.js";
|
|
28
|
+
import { SellerPaymentManager } from "./payments/seller-payment-manager.js";
|
|
29
|
+
import { IdentityClient } from "./payments/evm/identity-client.js";
|
|
30
|
+
import { SellerRequestHandler } from "./seller-request-handler.js";
|
|
31
|
+
import { BuyerRequestHandler, } from "./buyer-request-handler.js";
|
|
30
32
|
export class AntseedNode extends EventEmitter {
|
|
31
|
-
static _METADATA_REFRESH_DEBOUNCE_MS = 200;
|
|
32
33
|
_config;
|
|
33
34
|
_identity = null;
|
|
34
35
|
_dht = null;
|
|
@@ -45,17 +46,29 @@ export class AntseedNode extends EventEmitter {
|
|
|
45
46
|
_metering = null;
|
|
46
47
|
_receiptGenerator = null;
|
|
47
48
|
_balanceManager = null;
|
|
48
|
-
|
|
49
|
+
_depositsClient = null;
|
|
50
|
+
_channelsClient = null;
|
|
51
|
+
_stakingClient = null;
|
|
52
|
+
_identityClient = null;
|
|
49
53
|
_paymentMuxes = new Map();
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
/** Per-buyer session tracking: buyerPeerId → seller session state */
|
|
53
|
-
_sessions = new Map();
|
|
54
|
-
_settlementTimers = new Map();
|
|
54
|
+
/** Seller-side request handler (provider matching, execution, load tracking). */
|
|
55
|
+
_sellerHandler = null;
|
|
55
56
|
/** Buyer-side payment manager (initialized when buyer has payment config). */
|
|
56
57
|
_buyerPaymentManager = null;
|
|
57
|
-
/**
|
|
58
|
-
|
|
58
|
+
/** Buyer-side payment negotiation (402 handling, SpendingAuth, cost tracking). */
|
|
59
|
+
_buyerNegotiator = null;
|
|
60
|
+
/** Buyer-side request execution (streaming, timeouts, 402 retry). */
|
|
61
|
+
_buyerHandler = null;
|
|
62
|
+
/** Seller-side payment manager (initialized when seller has payment config). */
|
|
63
|
+
_sellerPaymentManager = null;
|
|
64
|
+
/** Shared channel store for payment persistence. */
|
|
65
|
+
_channelStore = null;
|
|
66
|
+
/** Periodic timeout checker interval. */
|
|
67
|
+
_timeoutCheckerInterval = null;
|
|
68
|
+
/** Block cursor for CloseRequested event polling. */
|
|
69
|
+
_closeRequestedFromBlock = 0;
|
|
70
|
+
/** Seller session lifecycle tracking (metering, settlement). */
|
|
71
|
+
_sessionTracker = null;
|
|
59
72
|
constructor(config) {
|
|
60
73
|
super();
|
|
61
74
|
this._config = config;
|
|
@@ -79,6 +92,10 @@ export class AntseedNode extends EventEmitter {
|
|
|
79
92
|
get buyerPaymentManager() {
|
|
80
93
|
return this._buyerPaymentManager;
|
|
81
94
|
}
|
|
95
|
+
/** Buyer-side payment negotiator (null if payments not configured for buyer). */
|
|
96
|
+
get buyerNegotiator() {
|
|
97
|
+
return this._buyerNegotiator;
|
|
98
|
+
}
|
|
82
99
|
/** Actual DHT port after binding (0 means not started). */
|
|
83
100
|
get dhtPort() {
|
|
84
101
|
return this._dht?.getPort() ?? 0;
|
|
@@ -87,6 +104,10 @@ export class AntseedNode extends EventEmitter {
|
|
|
87
104
|
get signalingPort() {
|
|
88
105
|
return this._connectionManager?.getListeningPort() ?? 0;
|
|
89
106
|
}
|
|
107
|
+
/** ERC-8004 IdentityRegistry client (null if not configured). */
|
|
108
|
+
get identityClient() {
|
|
109
|
+
return this._identityClient;
|
|
110
|
+
}
|
|
90
111
|
/** Current connection state for a peer if a connection exists, otherwise null. */
|
|
91
112
|
getPeerConnectionState(peerId) {
|
|
92
113
|
return this._connectionManager?.getConnection(peerId)?.state ?? null;
|
|
@@ -96,35 +117,11 @@ export class AntseedNode extends EventEmitter {
|
|
|
96
117
|
* Includes open sessions before they are finalized/settled.
|
|
97
118
|
*/
|
|
98
119
|
getActiveSellerSessions() {
|
|
99
|
-
|
|
100
|
-
for (const [buyerPeerId, session] of this._sessions.entries()) {
|
|
101
|
-
snapshots.push({
|
|
102
|
-
sessionId: session.sessionId,
|
|
103
|
-
buyerPeerId,
|
|
104
|
-
provider: session.provider,
|
|
105
|
-
startedAt: session.startedAt,
|
|
106
|
-
lastActivityAt: session.lastActivityAt,
|
|
107
|
-
totalRequests: session.totalRequests,
|
|
108
|
-
totalTokens: session.totalTokens,
|
|
109
|
-
avgLatencyMs: session.totalRequests > 0 ? session.totalLatencyMs / session.totalRequests : 0,
|
|
110
|
-
settling: Boolean(session.settling),
|
|
111
|
-
lockCommitted: session.lockCommitted,
|
|
112
|
-
lockedAmountUSDC: session.lockedAmount.toString(),
|
|
113
|
-
runningTotalUSDC: session.runningTotal.toString(),
|
|
114
|
-
ackedRequestCount: session.ackedRequestCount,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
return snapshots;
|
|
120
|
+
return this._sessionTracker?.getActiveSessions() ?? [];
|
|
118
121
|
}
|
|
119
|
-
/** Number of active in-memory seller
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
for (const session of this._sessions.values()) {
|
|
123
|
-
if (!session.settling) {
|
|
124
|
-
count += 1;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return count;
|
|
122
|
+
/** Number of active in-memory seller channels that are not currently settling. */
|
|
123
|
+
getActiveSellerChannelCount() {
|
|
124
|
+
return this._sessionTracker?.getActiveChannelCount() ?? 0;
|
|
128
125
|
}
|
|
129
126
|
async start() {
|
|
130
127
|
if (this._started) {
|
|
@@ -132,7 +129,7 @@ export class AntseedNode extends EventEmitter {
|
|
|
132
129
|
}
|
|
133
130
|
const dataDir = this._config.dataDir ?? join(homedir(), ".antseed");
|
|
134
131
|
// Load or create identity
|
|
135
|
-
this._identity = await loadOrCreateIdentity(dataDir);
|
|
132
|
+
this._identity = await loadOrCreateIdentity(this._config.identityStore ?? dataDir);
|
|
136
133
|
debugLog(`[Node] Identity loaded: ${this._identity.peerId.slice(0, 12)}...`);
|
|
137
134
|
// Determine bootstrap nodes — merge official + any user-configured nodes unless
|
|
138
135
|
// noOfficialBootstrap is set (e.g. isolated local testing).
|
|
@@ -155,17 +152,17 @@ export class AntseedNode extends EventEmitter {
|
|
|
155
152
|
return;
|
|
156
153
|
}
|
|
157
154
|
// End all active buyer payment sessions before shutdown
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
for (const timer of this._settlementTimers.values()) {
|
|
161
|
-
clearTimeout(timer);
|
|
155
|
+
if (this._buyerNegotiator) {
|
|
156
|
+
this._buyerNegotiator.cleanup();
|
|
162
157
|
}
|
|
163
|
-
this.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
158
|
+
if (this._sessionTracker) {
|
|
159
|
+
await this._sessionTracker.finalizeAllSessions("node-stop");
|
|
160
|
+
this._sessionTracker.clearTimers();
|
|
161
|
+
}
|
|
162
|
+
if (this._sellerHandler) {
|
|
163
|
+
this._sellerHandler.clearMetadataRefreshTimer();
|
|
164
|
+
this._sellerHandler = null;
|
|
167
165
|
}
|
|
168
|
-
this._providerLoadCounts.clear();
|
|
169
166
|
// Remove NAT port mappings
|
|
170
167
|
if (this._nat) {
|
|
171
168
|
await this._nat.cleanup();
|
|
@@ -213,12 +210,31 @@ export class AntseedNode extends EventEmitter {
|
|
|
213
210
|
}
|
|
214
211
|
this._metering = null;
|
|
215
212
|
}
|
|
213
|
+
if (this._timeoutCheckerInterval) {
|
|
214
|
+
clearInterval(this._timeoutCheckerInterval);
|
|
215
|
+
this._timeoutCheckerInterval = null;
|
|
216
|
+
}
|
|
217
|
+
if (this._channelStore) {
|
|
218
|
+
try {
|
|
219
|
+
this._channelStore.close();
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// ignore close errors
|
|
223
|
+
}
|
|
224
|
+
this._channelStore = null;
|
|
225
|
+
}
|
|
216
226
|
this._peerLookup = null;
|
|
217
227
|
this._receiptGenerator = null;
|
|
218
228
|
this._balanceManager = null;
|
|
219
|
-
this.
|
|
229
|
+
this._depositsClient = null;
|
|
230
|
+
this._channelsClient = null;
|
|
231
|
+
this._stakingClient = null;
|
|
232
|
+
this._identityClient = null;
|
|
220
233
|
this._buyerPaymentManager = null;
|
|
221
|
-
this.
|
|
234
|
+
this._buyerNegotiator = null;
|
|
235
|
+
this._buyerHandler = null;
|
|
236
|
+
this._sellerPaymentManager = null;
|
|
237
|
+
this._sessionTracker = null;
|
|
222
238
|
this._started = false;
|
|
223
239
|
this.emit("stopped");
|
|
224
240
|
}
|
|
@@ -247,20 +263,18 @@ export class AntseedNode extends EventEmitter {
|
|
|
247
263
|
peers.push(p);
|
|
248
264
|
}
|
|
249
265
|
}
|
|
250
|
-
//
|
|
251
|
-
if (this.
|
|
266
|
+
// Verify claimed on-chain stats against actual contract data
|
|
267
|
+
if (this._channelsClient && this._stakingClient) {
|
|
252
268
|
for (const p of peers) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
// Use claimed data if verification fails
|
|
263
|
-
}
|
|
269
|
+
try {
|
|
270
|
+
const evmAddress = peerIdToAddress(p.peerId);
|
|
271
|
+
const agentId = await this._stakingClient.getAgentId(evmAddress);
|
|
272
|
+
const stats = await this._channelsClient.getAgentStats(agentId);
|
|
273
|
+
p.onChainChannelCount = stats.channelCount;
|
|
274
|
+
p.onChainGhostCount = stats.ghostCount;
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// Contract lookup failed for this peer — keep claimed data
|
|
264
278
|
}
|
|
265
279
|
}
|
|
266
280
|
}
|
|
@@ -276,203 +290,64 @@ export class AntseedNode extends EventEmitter {
|
|
|
276
290
|
async connectToPeer(peer) {
|
|
277
291
|
const conn = await this._getOrCreateConnection(peer);
|
|
278
292
|
this._getOrCreateMux(peer.peerId, conn);
|
|
293
|
+
const negotiator = this._buyerNegotiator;
|
|
294
|
+
if (negotiator) {
|
|
295
|
+
this._paymentMuxes.set(peer.peerId, negotiator.getOrCreatePaymentMux(peer.peerId, conn));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Query session stats for a specific seller peer.
|
|
300
|
+
* Combines channel store data (authoritative payment/session info) with
|
|
301
|
+
* metering events when available.
|
|
302
|
+
*/
|
|
303
|
+
getMeteringStatsByPeer(sellerPeerId) {
|
|
304
|
+
const buyerAddress = this._identity?.wallet.address ?? null;
|
|
305
|
+
const channel = (buyerAddress != null)
|
|
306
|
+
? (this._channelStore?.getActiveChannelByPeerAndBuyer(sellerPeerId, 'buyer', buyerAddress)
|
|
307
|
+
?? this._channelStore?.getLatestChannelByPeerAndBuyer(sellerPeerId, 'buyer', buyerAddress))
|
|
308
|
+
: (this._channelStore?.getActiveChannelByPeer(sellerPeerId, 'buyer')
|
|
309
|
+
?? this._channelStore?.getLatestChannel(sellerPeerId, 'buyer'))
|
|
310
|
+
?? null;
|
|
311
|
+
const lifetime = (buyerAddress != null)
|
|
312
|
+
? this._channelStore?.getTotalsByPeerAndBuyer(sellerPeerId, 'buyer', buyerAddress)
|
|
313
|
+
: this._channelStore?.getTotalsByPeer(sellerPeerId, 'buyer')
|
|
314
|
+
?? null;
|
|
315
|
+
if (!channel && !lifetime)
|
|
316
|
+
return null;
|
|
317
|
+
const liveTotals = this._buyerPaymentManager?.getResponseTokenTotals(sellerPeerId);
|
|
318
|
+
const inputTokens = (liveTotals != null) ? liveTotals.input
|
|
319
|
+
: (channel != null) ? Number(channel.tokensDelivered || '0')
|
|
320
|
+
: 0;
|
|
321
|
+
const outputTokens = (liveTotals != null) ? liveTotals.output
|
|
322
|
+
: (channel != null) ? Number(channel.previousConsumption || '0')
|
|
323
|
+
: 0;
|
|
324
|
+
return {
|
|
325
|
+
totalRequests: channel?.requestCount ?? 0,
|
|
326
|
+
inputTokens,
|
|
327
|
+
outputTokens,
|
|
328
|
+
totalTokens: inputTokens + outputTokens,
|
|
329
|
+
reservedUsdc: this._buyerPaymentManager?.getReserveCeiling(sellerPeerId)?.toString() ?? null,
|
|
330
|
+
consumedUsdc: channel?.authMax ?? null,
|
|
331
|
+
channelStatus: channel?.status ?? null,
|
|
332
|
+
reservedAt: channel?.reservedAt ?? null,
|
|
333
|
+
lifetimeSessions: lifetime?.totalSessions ?? 0,
|
|
334
|
+
lifetimeRequests: lifetime?.totalRequests ?? 0,
|
|
335
|
+
lifetimeInputTokens: lifetime?.totalInputTokens ?? 0,
|
|
336
|
+
lifetimeOutputTokens: lifetime?.totalOutputTokens ?? 0,
|
|
337
|
+
lifetimeTotalTokens: (lifetime?.totalInputTokens ?? 0) + (lifetime?.totalOutputTokens ?? 0),
|
|
338
|
+
lifetimeAuthorizedUsdc: (lifetime?.totalAuthorizedUsdc ?? 0n).toString(),
|
|
339
|
+
lifetimeFirstSessionAt: lifetime?.firstSessionAt ?? null,
|
|
340
|
+
};
|
|
279
341
|
}
|
|
280
342
|
async sendRequest(peer, req, options) {
|
|
281
|
-
|
|
343
|
+
if (!this._buyerHandler)
|
|
344
|
+
throw new Error("Node not started or not in buyer mode");
|
|
345
|
+
return this._buyerHandler.sendRequest(peer, req, undefined, options);
|
|
282
346
|
}
|
|
283
347
|
async sendRequestStream(peer, req, callbacks, options) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (!req.requestId || typeof req.requestId !== "string") {
|
|
288
|
-
throw new Error("requestId must be a non-empty string");
|
|
289
|
-
}
|
|
290
|
-
if (!this._connectionManager || !this._identity) {
|
|
291
|
-
throw new Error("Node not started");
|
|
292
|
-
}
|
|
293
|
-
const opName = callbacks ? "sendRequestStream" : "sendRequest";
|
|
294
|
-
debugLog(`[Node] ${opName} ${req.method} ${req.path} → peer ${peer.peerId.slice(0, 12)}... (reqId=${req.requestId.slice(0, 8)})`);
|
|
295
|
-
const conn = await this._getOrCreateConnection(peer);
|
|
296
|
-
debugLog(`[Node] Connection to ${peer.peerId.slice(0, 12)}... state=${conn.state}`);
|
|
297
|
-
const mux = this._getOrCreateMux(peer.peerId, conn);
|
|
298
|
-
// Buyer-side: initiate lock and wait for confirmation on first request to a new peer
|
|
299
|
-
if (this._buyerPaymentManager && !this._buyerLockedPeers.has(peer.peerId)) {
|
|
300
|
-
await this._initiateBuyerLock(peer, conn);
|
|
301
|
-
}
|
|
302
|
-
const startTime = Date.now();
|
|
303
|
-
return new Promise((resolve, reject) => {
|
|
304
|
-
const timeoutMs = this._config.requestTimeoutMs ?? 30_000;
|
|
305
|
-
const maxStreamBufferBytes = Math.max(1, this._config.maxStreamBufferBytes ?? 16 * 1024 * 1024);
|
|
306
|
-
const maxStreamDurationMs = Math.max(1, this._config.maxStreamDurationMs ?? 5 * 60_000);
|
|
307
|
-
const streamInitialResponseTimeoutMs = callbacks ? Math.max(timeoutMs, 90_000) : timeoutMs;
|
|
308
|
-
// Idle timeout for streaming: resets on each chunk so long-running
|
|
309
|
-
// streams (thinking models, large outputs) stay alive as long as
|
|
310
|
-
// data keeps flowing.
|
|
311
|
-
const streamIdleTimeoutMs = Math.max(timeoutMs, 60_000);
|
|
312
|
-
let settled = false;
|
|
313
|
-
let streamStarted = false;
|
|
314
|
-
let streamStartedAtMs = 0;
|
|
315
|
-
let streamBufferedBytes = 0;
|
|
316
|
-
let streamStartResponse = null;
|
|
317
|
-
const streamChunks = [];
|
|
318
|
-
let activeTimeout = null;
|
|
319
|
-
let activeTimeoutMs = streamInitialResponseTimeoutMs;
|
|
320
|
-
const abortSignal = options?.signal;
|
|
321
|
-
let abortListenerAttached = false;
|
|
322
|
-
let connectionStateListenerAttached = false;
|
|
323
|
-
const hasConnectionStateEvents = typeof conn.on === "function"
|
|
324
|
-
&& typeof conn.off === "function";
|
|
325
|
-
const cleanupAbortListener = () => {
|
|
326
|
-
if (abortSignal && abortListenerAttached) {
|
|
327
|
-
abortSignal.removeEventListener("abort", onAbort);
|
|
328
|
-
abortListenerAttached = false;
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
const cleanupConnectionListener = () => {
|
|
332
|
-
if (!connectionStateListenerAttached)
|
|
333
|
-
return;
|
|
334
|
-
conn.off("stateChange", onConnectionStateChange);
|
|
335
|
-
connectionStateListenerAttached = false;
|
|
336
|
-
};
|
|
337
|
-
const onConnectionStateChange = (state) => {
|
|
338
|
-
if (settled)
|
|
339
|
-
return;
|
|
340
|
-
if (state !== ConnectionState.Closed && state !== ConnectionState.Failed) {
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
settled = true;
|
|
344
|
-
if (activeTimeout)
|
|
345
|
-
clearTimeout(activeTimeout);
|
|
346
|
-
cleanupAbortListener();
|
|
347
|
-
cleanupConnectionListener();
|
|
348
|
-
mux.cancelProxyRequest(req.requestId);
|
|
349
|
-
reject(new Error(`Connection to ${peer.peerId} ${state.toLowerCase()} during request ${req.requestId}`));
|
|
350
|
-
};
|
|
351
|
-
const onAbort = () => {
|
|
352
|
-
if (settled)
|
|
353
|
-
return;
|
|
354
|
-
settled = true;
|
|
355
|
-
if (activeTimeout)
|
|
356
|
-
clearTimeout(activeTimeout);
|
|
357
|
-
cleanupAbortListener();
|
|
358
|
-
cleanupConnectionListener();
|
|
359
|
-
debugWarn(`[Node] Request ${req.requestId.slice(0, 8)} aborted by caller`);
|
|
360
|
-
mux.cancelProxyRequest(req.requestId);
|
|
361
|
-
reject(new Error(`Request ${req.requestId} aborted`));
|
|
362
|
-
};
|
|
363
|
-
if (abortSignal) {
|
|
364
|
-
if (abortSignal.aborted) {
|
|
365
|
-
onAbort();
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
369
|
-
abortListenerAttached = true;
|
|
370
|
-
}
|
|
371
|
-
if (hasConnectionStateEvents) {
|
|
372
|
-
conn.on("stateChange", onConnectionStateChange);
|
|
373
|
-
connectionStateListenerAttached = true;
|
|
374
|
-
}
|
|
375
|
-
const resetTimeout = (ms) => {
|
|
376
|
-
if (activeTimeout)
|
|
377
|
-
clearTimeout(activeTimeout);
|
|
378
|
-
activeTimeoutMs = ms;
|
|
379
|
-
activeTimeout = setTimeout(() => {
|
|
380
|
-
if (settled)
|
|
381
|
-
return;
|
|
382
|
-
settled = true;
|
|
383
|
-
cleanupAbortListener();
|
|
384
|
-
cleanupConnectionListener();
|
|
385
|
-
debugWarn(`[Node] Request ${req.requestId.slice(0, 8)} timed out after ${Date.now() - startTime}ms `
|
|
386
|
-
+ `(timeout=${activeTimeoutMs}ms, stream=${callbacks ? "true" : "false"}, streamStarted=${streamStarted ? "true" : "false"}, buffered=${streamBufferedBytes}b)`);
|
|
387
|
-
mux.cancelProxyRequest(req.requestId);
|
|
388
|
-
reject(new Error(`Request ${req.requestId} timed out`));
|
|
389
|
-
}, ms);
|
|
390
|
-
};
|
|
391
|
-
// Initial timeout: wait for the first response frame.
|
|
392
|
-
resetTimeout(streamInitialResponseTimeoutMs);
|
|
393
|
-
const finish = (response) => {
|
|
394
|
-
if (settled)
|
|
395
|
-
return;
|
|
396
|
-
settled = true;
|
|
397
|
-
if (activeTimeout)
|
|
398
|
-
clearTimeout(activeTimeout);
|
|
399
|
-
cleanupAbortListener();
|
|
400
|
-
cleanupConnectionListener();
|
|
401
|
-
const cleaned = this._stripStreamingHeader(response);
|
|
402
|
-
debugLog(`[Node] Response for ${req.requestId.slice(0, 8)}: status=${cleaned.statusCode} (${Date.now() - startTime}ms, ${cleaned.body.length}b)`);
|
|
403
|
-
resolve(cleaned);
|
|
404
|
-
};
|
|
405
|
-
const fail = (error) => {
|
|
406
|
-
if (settled)
|
|
407
|
-
return;
|
|
408
|
-
settled = true;
|
|
409
|
-
if (activeTimeout)
|
|
410
|
-
clearTimeout(activeTimeout);
|
|
411
|
-
cleanupAbortListener();
|
|
412
|
-
cleanupConnectionListener();
|
|
413
|
-
reject(error);
|
|
414
|
-
};
|
|
415
|
-
mux.sendProxyRequest(req, (response, metadata) => {
|
|
416
|
-
if (settled)
|
|
417
|
-
return;
|
|
418
|
-
if (metadata.streamingStart) {
|
|
419
|
-
streamStarted = true;
|
|
420
|
-
streamStartedAtMs = Date.now();
|
|
421
|
-
streamBufferedBytes = 0;
|
|
422
|
-
streamStartResponse = this._stripStreamingHeader(response);
|
|
423
|
-
debugLog(`[Node] Stream started for ${req.requestId.slice(0, 8)}; idle-timeout=${streamIdleTimeoutMs}ms`);
|
|
424
|
-
// Switch to streaming idle timeout: resets on each chunk.
|
|
425
|
-
resetTimeout(streamIdleTimeoutMs);
|
|
426
|
-
callbacks?.onResponseStart?.(streamStartResponse, { streaming: true });
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
callbacks?.onResponseStart?.(this._stripStreamingHeader(response), { streaming: false });
|
|
430
|
-
finish(response);
|
|
431
|
-
}, (chunk) => {
|
|
432
|
-
if (settled)
|
|
433
|
-
return;
|
|
434
|
-
if (!streamStarted)
|
|
435
|
-
return;
|
|
436
|
-
// Reset idle timeout on each chunk so streaming stays alive.
|
|
437
|
-
resetTimeout(streamIdleTimeoutMs);
|
|
438
|
-
if (Date.now() - streamStartedAtMs > maxStreamDurationMs) {
|
|
439
|
-
mux.cancelProxyRequest(req.requestId);
|
|
440
|
-
fail(new Error(`Stream ${req.requestId} exceeded max duration (${maxStreamDurationMs}ms)`));
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
callbacks?.onResponseChunk?.(chunk);
|
|
444
|
-
if (chunk.data.length > 0) {
|
|
445
|
-
if (callbacks?.onResponseChunk) {
|
|
446
|
-
// Streaming mode: chunks already delivered to caller via callback.
|
|
447
|
-
// Track byte count for the debug timeout log only — do not
|
|
448
|
-
// enforce maxStreamBufferBytes so large streams aren't rejected.
|
|
449
|
-
streamBufferedBytes += chunk.data.length;
|
|
450
|
-
streamChunks.push(chunk.data);
|
|
451
|
-
}
|
|
452
|
-
else {
|
|
453
|
-
// Non-streaming: accumulate chunks for the final response body.
|
|
454
|
-
const nextBufferedBytes = streamBufferedBytes + chunk.data.length;
|
|
455
|
-
if (nextBufferedBytes > maxStreamBufferBytes) {
|
|
456
|
-
mux.cancelProxyRequest(req.requestId);
|
|
457
|
-
fail(new Error(`Stream ${req.requestId} exceeded max buffered size (${maxStreamBufferBytes} bytes)`));
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
streamBufferedBytes = nextBufferedBytes;
|
|
461
|
-
streamChunks.push(chunk.data);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
if (!chunk.done)
|
|
465
|
-
return;
|
|
466
|
-
if (!streamStartResponse) {
|
|
467
|
-
fail(new Error(`Stream ${req.requestId} ended before response start`));
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
finish({
|
|
471
|
-
...streamStartResponse,
|
|
472
|
-
body: concatChunks(streamChunks),
|
|
473
|
-
});
|
|
474
|
-
});
|
|
475
|
-
});
|
|
348
|
+
if (!this._buyerHandler)
|
|
349
|
+
throw new Error("Node not started or not in buyer mode");
|
|
350
|
+
return this._buyerHandler.sendRequest(peer, req, callbacks, options);
|
|
476
351
|
}
|
|
477
352
|
_createDHTConfig(port, bootstrapNodes) {
|
|
478
353
|
return {
|
|
@@ -546,8 +421,15 @@ export class AntseedNode extends EventEmitter {
|
|
|
546
421
|
this._muxes.delete(peerId);
|
|
547
422
|
this._paymentMuxes.delete(peerId);
|
|
548
423
|
this._decoders.delete(peerId);
|
|
549
|
-
//
|
|
550
|
-
|
|
424
|
+
// Clean up buyer-side payment state on disconnect
|
|
425
|
+
this._buyerNegotiator?.onPeerDisconnect(peerId);
|
|
426
|
+
// Handle buyer disconnect (seller side)
|
|
427
|
+
if (this._sellerPaymentManager) {
|
|
428
|
+
this._sellerPaymentManager.onBuyerDisconnect(peerId);
|
|
429
|
+
}
|
|
430
|
+
if (this._sessionTracker) {
|
|
431
|
+
void this._sessionTracker.finalizeSession(peerId, "disconnect");
|
|
432
|
+
}
|
|
551
433
|
}
|
|
552
434
|
});
|
|
553
435
|
// Start keepalive pings on outbound (buyer-initiated) connections to
|
|
@@ -580,7 +462,6 @@ export class AntseedNode extends EventEmitter {
|
|
|
580
462
|
const dhtPort = this._config.dhtPort ?? 6881;
|
|
581
463
|
const signalingPort = this._config.signalingPort ?? 6882;
|
|
582
464
|
debugLog(`[Node] Starting seller — DHT port=${dhtPort}, signaling port=${signalingPort}`);
|
|
583
|
-
// Initialize metering storage
|
|
584
465
|
const dataDir = this._config.dataDir ?? join(homedir(), ".antseed");
|
|
585
466
|
try {
|
|
586
467
|
this._metering = new MeteringStorage(join(dataDir, "metering.db"));
|
|
@@ -592,10 +473,25 @@ export class AntseedNode extends EventEmitter {
|
|
|
592
473
|
if (this._metering) {
|
|
593
474
|
this._receiptGenerator = new ReceiptGenerator({
|
|
594
475
|
peerId: identity.peerId,
|
|
595
|
-
sign: (message) =>
|
|
476
|
+
sign: (message) => signUtf8(identity.wallet, message),
|
|
596
477
|
});
|
|
597
478
|
}
|
|
479
|
+
// Initialize seller session tracker
|
|
480
|
+
this._sessionTracker = new SellerSessionTracker(identity, this._metering, this._receiptGenerator, { settlementIdleMs: this._config.payments?.settlementIdleMs }, {
|
|
481
|
+
onSessionUpdated: (snapshot) => this.emit("session:updated", snapshot),
|
|
482
|
+
onSessionFinalized: (info) => this.emit("session:finalized", info),
|
|
483
|
+
});
|
|
598
484
|
await this._initializePayments(dataDir);
|
|
485
|
+
// Wire idle-timeout session finalization to on-chain settlement
|
|
486
|
+
if (this._sellerPaymentManager) {
|
|
487
|
+
const spm = this._sellerPaymentManager;
|
|
488
|
+
this.on("session:finalized", (info) => {
|
|
489
|
+
if (info.reason === "idle-timeout") {
|
|
490
|
+
debugLog(`[Node] Idle timeout for buyer ${info.buyerPeerId.slice(0, 12)}... — settling channel`);
|
|
491
|
+
void spm.settleSession(info.buyerPeerId, { cleanupOnFailure: true });
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
}
|
|
599
495
|
// Start DHT
|
|
600
496
|
this._dht = new DHTNode(this._createDHTConfig(dhtPort, bootstrapNodes));
|
|
601
497
|
await this._dht.start();
|
|
@@ -654,12 +550,23 @@ export class AntseedNode extends EventEmitter {
|
|
|
654
550
|
])),
|
|
655
551
|
reannounceIntervalMs: DEFAULT_DHT_CONFIG.reannounceIntervalMs,
|
|
656
552
|
signalingPort: actualSignalingPort,
|
|
553
|
+
...(this._channelsClient ? { channelsClient: this._channelsClient } : {}),
|
|
554
|
+
...(this._stakingClient ? { stakingClient: this._stakingClient, paymentsEnabled: true } : {}),
|
|
657
555
|
};
|
|
658
556
|
this._announcer = new PeerAnnouncer(announcerConfig);
|
|
659
557
|
this._announcer.startPeriodicAnnounce();
|
|
660
558
|
// Serve metadata on the signaling port (HTTP requests are auto-detected)
|
|
661
559
|
this._connectionManager.setMetadataProvider(() => this._announcer?.getLatestMetadata() ?? null);
|
|
662
560
|
}
|
|
561
|
+
// Create seller request handler
|
|
562
|
+
this._sellerHandler = new SellerRequestHandler({
|
|
563
|
+
providers: this._providers,
|
|
564
|
+
sellerPaymentManager: this._sellerPaymentManager,
|
|
565
|
+
sessionTracker: this._sessionTracker,
|
|
566
|
+
channelsClient: this._channelsClient,
|
|
567
|
+
announcer: this._announcer,
|
|
568
|
+
emit: (event, ...args) => this.emit(event, ...args),
|
|
569
|
+
});
|
|
663
570
|
// Listen for incoming connections
|
|
664
571
|
this._connectionManager.on("connection", (conn) => {
|
|
665
572
|
this._handleIncomingConnection(conn);
|
|
@@ -670,6 +577,8 @@ export class AntseedNode extends EventEmitter {
|
|
|
670
577
|
const identity = this._identity;
|
|
671
578
|
const dhtPort = this._config.dhtPort ?? 0;
|
|
672
579
|
debugLog(`[Node] Starting buyer — DHT port=${dhtPort}`);
|
|
580
|
+
const dataDir = this._config.dataDir ?? join(homedir(), ".antseed");
|
|
581
|
+
await this._initializePayments(dataDir);
|
|
673
582
|
// Start DHT with ephemeral port
|
|
674
583
|
this._dht = new DHTNode(this._createDHTConfig(dhtPort, bootstrapNodes));
|
|
675
584
|
await this._dht.start();
|
|
@@ -692,464 +601,174 @@ export class AntseedNode extends EventEmitter {
|
|
|
692
601
|
this._peerLookup = new PeerLookup(lookupConfig);
|
|
693
602
|
// Initialize buyer-side payment manager if payments config is provided
|
|
694
603
|
const payments = this._config.payments;
|
|
695
|
-
if (payments?.enabled && payments.rpcUrl && payments.
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
604
|
+
if (payments?.enabled && payments.rpcUrl && payments.depositsAddress && payments.channelsAddress && payments.usdcAddress) {
|
|
605
|
+
const paymentsDir = join(dataDir, "payments");
|
|
606
|
+
// Create shared ChannelStore for both buyer and seller payment managers
|
|
607
|
+
if (!this._channelStore) {
|
|
608
|
+
try {
|
|
609
|
+
this._channelStore = new ChannelStore(paymentsDir);
|
|
610
|
+
debugLog("[Node] ChannelStore initialized (buyer)");
|
|
611
|
+
}
|
|
612
|
+
catch (err) {
|
|
613
|
+
debugWarn(`[Node] ChannelStore unavailable: ${err instanceof Error ? err.message : err}`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (this._channelStore) {
|
|
617
|
+
const buyerPaymentConfig = {
|
|
618
|
+
rpcUrl: payments.rpcUrl,
|
|
619
|
+
depositsContractAddress: payments.depositsAddress,
|
|
620
|
+
channelsContractAddress: payments.channelsAddress,
|
|
621
|
+
usdcAddress: payments.usdcAddress,
|
|
622
|
+
identityRegistryAddress: payments.identityRegistryAddress ?? '',
|
|
623
|
+
chainId: payments.chainId ?? 8453,
|
|
624
|
+
defaultAuthDurationSecs: payments.defaultAuthDurationSecs ?? 900, // 15 min — seller must call reserve() promptly
|
|
625
|
+
maxPerRequestUsdc: BigInt(payments.maxPerRequestUsdc ?? "500000"), // $0.50 default — covers most LLM requests
|
|
626
|
+
maxReserveAmountUsdc: BigInt(payments.maxReserveAmountUsdc ?? "1000000"), // $1.00 default per session (matches FIRST_SIGN_CAP)
|
|
627
|
+
dataDir: paymentsDir,
|
|
628
|
+
};
|
|
629
|
+
this._buyerPaymentManager = new BuyerPaymentManager(identity, buyerPaymentConfig, this._channelStore);
|
|
630
|
+
debugLog(`[Node] Buyer payment manager initialized (wallet=${identity.wallet.address.slice(0, 10)}... chainId=${buyerPaymentConfig.chainId} deposits=${buyerPaymentConfig.depositsContractAddress.slice(0, 10)}...)`);
|
|
631
|
+
// Create negotiator that wraps the BPM with 402 handling and per-request auth
|
|
632
|
+
this._buyerNegotiator = new BuyerPaymentNegotiator(identity, this._buyerPaymentManager, this._depositsClient, this._channelsClient, this._channelStore, {}, this);
|
|
633
|
+
debugLog(`[Node] Buyer payment negotiator initialized`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// Create buyer request handler
|
|
637
|
+
this._buyerHandler = new BuyerRequestHandler({
|
|
638
|
+
requestTimeoutMs: this._config.requestTimeoutMs,
|
|
639
|
+
maxStreamBufferBytes: this._config.maxStreamBufferBytes,
|
|
640
|
+
maxStreamDurationMs: this._config.maxStreamDurationMs,
|
|
641
|
+
}, {
|
|
642
|
+
negotiator: this._buyerNegotiator,
|
|
643
|
+
getConnection: (peer) => this._getOrCreateConnection(peer),
|
|
644
|
+
getMux: (peerId, conn) => this._getOrCreateMux(peerId, conn),
|
|
645
|
+
registerPaymentMux: (peerId, pmux) => this._paymentMuxes.set(peerId, pmux),
|
|
646
|
+
});
|
|
705
647
|
debugLog(`[Node] Buyer ready — DHT running on port ${this._dht.getPort()}`);
|
|
706
648
|
}
|
|
707
649
|
_handleIncomingConnection(conn) {
|
|
708
650
|
debugLog(`[Node] Incoming connection from ${conn.remotePeerId.slice(0, 12)}...`);
|
|
709
651
|
const buyerPeerId = conn.remotePeerId;
|
|
710
|
-
const mux = new ProxyMux(conn);
|
|
711
652
|
// Create PaymentMux alongside ProxyMux (seller-side)
|
|
712
653
|
const paymentMux = new PaymentMux(conn);
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
});
|
|
722
|
-
paymentMux.onTopUpAuth((payload) => {
|
|
723
|
-
void this._handleTopUpAuth(buyerPeerId, payload);
|
|
724
|
-
});
|
|
725
|
-
this._paymentMuxes.set(buyerPeerId, paymentMux);
|
|
726
|
-
// Register the ProxyMux request handler that routes to providers
|
|
727
|
-
mux.onProxyRequest(async (request) => {
|
|
728
|
-
debugLog(`[Node] Seller received request: ${request.method} ${request.path} (reqId=${request.requestId.slice(0, 8)})`);
|
|
729
|
-
// Reject with 402 if lock not committed and escrow client is configured
|
|
730
|
-
const session = this._sessions.get(buyerPeerId);
|
|
731
|
-
if (this._escrowClient && (!session || !session.lockCommitted)) {
|
|
732
|
-
debugWarn(`[Node] Rejecting request from ${buyerPeerId.slice(0, 12)}... — lock not committed`);
|
|
733
|
-
mux.sendProxyResponse({
|
|
734
|
-
requestId: request.requestId,
|
|
735
|
-
statusCode: 402,
|
|
736
|
-
headers: { "content-type": "text/plain" },
|
|
737
|
-
body: new TextEncoder().encode("Payment required: session lock not committed"),
|
|
738
|
-
});
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
const requestedService = this._extractRequestedService(request);
|
|
742
|
-
const requestedProvider = this._extractRequestedProvider(request);
|
|
743
|
-
const matchesService = (provider) => provider.services.length === 0
|
|
744
|
-
|| (requestedService !== null && provider.services.includes(requestedService))
|
|
745
|
-
|| this._providers.length === 1;
|
|
746
|
-
let provider;
|
|
747
|
-
if (requestedProvider) {
|
|
748
|
-
provider = this._providers.find((candidate) => candidate.name.toLowerCase() === requestedProvider && matchesService(candidate));
|
|
749
|
-
}
|
|
750
|
-
if (!provider) {
|
|
751
|
-
provider = this._providers.find((candidate) => matchesService(candidate));
|
|
752
|
-
}
|
|
753
|
-
if (!provider) {
|
|
754
|
-
debugWarn(`[Node] No matching provider for ${request.path}`);
|
|
755
|
-
mux.sendProxyResponse({
|
|
756
|
-
requestId: request.requestId,
|
|
757
|
-
statusCode: 502,
|
|
758
|
-
headers: { "content-type": "text/plain" },
|
|
759
|
-
body: new TextEncoder().encode("No matching provider"),
|
|
760
|
-
});
|
|
761
|
-
return;
|
|
762
|
-
}
|
|
763
|
-
// Track active seller session at request start so runtime state reflects
|
|
764
|
-
// in-flight work immediately (not only after metering persistence).
|
|
765
|
-
this._getOrCreateSellerSession(buyerPeerId, provider.name);
|
|
766
|
-
request.headers['x-antseed-buyer-peer-id'] = buyerPeerId;
|
|
767
|
-
debugLog(`[Node] Routing to provider "${provider.name}"`);
|
|
768
|
-
const startTime = Date.now();
|
|
769
|
-
let statusCode = 500;
|
|
770
|
-
let responseBody = new Uint8Array(0);
|
|
771
|
-
let streamedResponseStarted = false;
|
|
772
|
-
this._adjustProviderLoad(provider.name, 1);
|
|
773
|
-
try {
|
|
774
|
-
try {
|
|
775
|
-
const response = await this._executeProviderRequest(provider, request, {
|
|
776
|
-
onResponseStart: (streamResponseStart) => {
|
|
777
|
-
streamedResponseStarted = true;
|
|
778
|
-
statusCode = streamResponseStart.statusCode;
|
|
779
|
-
mux.sendProxyResponse(streamResponseStart);
|
|
780
|
-
},
|
|
781
|
-
onResponseChunk: (chunk) => {
|
|
782
|
-
if (!streamedResponseStarted)
|
|
783
|
-
return;
|
|
784
|
-
mux.sendProxyChunk(chunk);
|
|
785
|
-
},
|
|
786
|
-
});
|
|
787
|
-
statusCode = response.statusCode;
|
|
788
|
-
responseBody = response.body;
|
|
789
|
-
debugLog(`[Node] Provider responded: status=${statusCode} (${Date.now() - startTime}ms, ${responseBody.length}b)`);
|
|
790
|
-
if (!streamedResponseStarted) {
|
|
791
|
-
mux.sendProxyResponse(response);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
catch (err) {
|
|
795
|
-
const message = err instanceof Error ? err.message : "Internal error";
|
|
796
|
-
debugWarn(`[Node] Provider error after ${Date.now() - startTime}ms: ${message}`);
|
|
797
|
-
responseBody = new TextEncoder().encode(message);
|
|
798
|
-
if (streamedResponseStarted) {
|
|
799
|
-
mux.sendProxyChunk({
|
|
800
|
-
requestId: request.requestId,
|
|
801
|
-
data: new TextEncoder().encode(`event: error\ndata: ${message}\n\n`),
|
|
802
|
-
done: false,
|
|
803
|
-
});
|
|
804
|
-
mux.sendProxyChunk({
|
|
805
|
-
requestId: request.requestId,
|
|
806
|
-
data: new Uint8Array(0),
|
|
807
|
-
done: true,
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
else {
|
|
811
|
-
statusCode = 500;
|
|
812
|
-
mux.sendProxyResponse({
|
|
813
|
-
requestId: request.requestId,
|
|
814
|
-
statusCode: 500,
|
|
815
|
-
headers: { "content-type": "text/plain" },
|
|
816
|
-
body: responseBody,
|
|
817
|
-
});
|
|
654
|
+
if (this._sellerPaymentManager) {
|
|
655
|
+
const spm = this._sellerPaymentManager;
|
|
656
|
+
paymentMux.onSpendingAuth((payload) => {
|
|
657
|
+
void spm.handleSpendingAuth(buyerPeerId, payload, paymentMux)
|
|
658
|
+
.then((status) => {
|
|
659
|
+
if (status === 'rejected') {
|
|
660
|
+
debugWarn(`[Node] SpendingAuth rejected for buyer ${buyerPeerId.slice(0, 12)}... — notifying via payment:auth-rejected event`);
|
|
661
|
+
this.emit('payment:auth-rejected', { buyerPeerId, reason: 'invalid_or_non_monotonic' });
|
|
818
662
|
}
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
});
|
|
663
|
+
})
|
|
664
|
+
.catch((err) => {
|
|
665
|
+
debugWarn(`[Node] SpendingAuth handler error for ${buyerPeerId.slice(0, 12)}...: ${err instanceof Error ? err.message : err}`);
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
else {
|
|
670
|
+
paymentMux.onSpendingAuth(() => {
|
|
671
|
+
debugWarn(`[Node] SpendingAuth rejected — SellerPaymentManager not configured`);
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
this._paymentMuxes.set(buyerPeerId, paymentMux);
|
|
675
|
+
const { mux } = this._sellerHandler.handleConnection(conn, buyerPeerId, paymentMux);
|
|
834
676
|
this._muxes.set(buyerPeerId, mux);
|
|
835
677
|
this._wireConnection(conn, buyerPeerId);
|
|
836
678
|
this.emit("connection", conn);
|
|
837
679
|
}
|
|
838
|
-
async _executeProviderRequest(provider, request, streamCallbacks) {
|
|
839
|
-
if (streamCallbacks && provider.handleRequestStream) {
|
|
840
|
-
return provider.handleRequestStream(request, streamCallbacks);
|
|
841
|
-
}
|
|
842
|
-
return provider.handleRequest(request);
|
|
843
|
-
}
|
|
844
|
-
_stripStreamingHeader(response) {
|
|
845
|
-
if (response.headers[ANTSEED_STREAMING_RESPONSE_HEADER] !== "1") {
|
|
846
|
-
return response;
|
|
847
|
-
}
|
|
848
|
-
const headers = { ...response.headers };
|
|
849
|
-
delete headers[ANTSEED_STREAMING_RESPONSE_HEADER];
|
|
850
|
-
return {
|
|
851
|
-
...response,
|
|
852
|
-
headers,
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
_parseJsonBody(body) {
|
|
856
|
-
try {
|
|
857
|
-
return JSON.parse(new TextDecoder().decode(body));
|
|
858
|
-
}
|
|
859
|
-
catch {
|
|
860
|
-
return null;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
_extractRequestedService(request) {
|
|
864
|
-
const contentType = request.headers["content-type"] ?? request.headers["Content-Type"] ?? "";
|
|
865
|
-
if (!contentType.toLowerCase().includes("application/json")) {
|
|
866
|
-
return null;
|
|
867
|
-
}
|
|
868
|
-
const parsed = this._parseJsonBody(request.body);
|
|
869
|
-
if (!parsed || typeof parsed !== "object") {
|
|
870
|
-
return null;
|
|
871
|
-
}
|
|
872
|
-
const service = parsed["model"];
|
|
873
|
-
if (typeof service !== "string" || service.trim().length === 0) {
|
|
874
|
-
return null;
|
|
875
|
-
}
|
|
876
|
-
return service.trim();
|
|
877
|
-
}
|
|
878
|
-
_extractRequestedProvider(request) {
|
|
879
|
-
const providers = Object.entries(request.headers)
|
|
880
|
-
.filter(([header]) => header.toLowerCase() === "x-antseed-provider")
|
|
881
|
-
.map(([, value]) => value.trim().toLowerCase())
|
|
882
|
-
.filter((value) => value.length > 0);
|
|
883
|
-
return providers[0] ?? null;
|
|
884
|
-
}
|
|
885
|
-
_resolveProviderPricing(provider, request) {
|
|
886
|
-
const requestedService = this._extractRequestedService(request);
|
|
887
|
-
if (requestedService) {
|
|
888
|
-
const servicePricing = provider.pricing.services?.[requestedService];
|
|
889
|
-
if (servicePricing) {
|
|
890
|
-
return servicePricing;
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
return provider.pricing.defaults;
|
|
894
|
-
}
|
|
895
|
-
_getOrCreateSellerSession(buyerPeerId, providerName) {
|
|
896
|
-
if (!this._identity) {
|
|
897
|
-
return null;
|
|
898
|
-
}
|
|
899
|
-
let session = this._sessions.get(buyerPeerId);
|
|
900
|
-
if (!session) {
|
|
901
|
-
const now = Date.now();
|
|
902
|
-
const sessionId = randomUUID();
|
|
903
|
-
// Generate 32-byte sessionIdBytes from UUID for on-chain use
|
|
904
|
-
const sessionIdBytes = createHash("sha256").update(sessionId).digest();
|
|
905
|
-
session = {
|
|
906
|
-
sessionId,
|
|
907
|
-
sessionIdBytes: new Uint8Array(sessionIdBytes),
|
|
908
|
-
startedAt: now,
|
|
909
|
-
lastActivityAt: now,
|
|
910
|
-
totalRequests: 0,
|
|
911
|
-
totalTokens: 0,
|
|
912
|
-
totalLatencyMs: 0,
|
|
913
|
-
totalCostCents: 0,
|
|
914
|
-
provider: providerName,
|
|
915
|
-
lockCommitted: false,
|
|
916
|
-
lockedAmount: 0n,
|
|
917
|
-
runningTotal: 0n,
|
|
918
|
-
ackedRequestCount: 0,
|
|
919
|
-
lastAckedTotal: 0n,
|
|
920
|
-
awaitingAck: false,
|
|
921
|
-
buyerEvmAddress: null,
|
|
922
|
-
};
|
|
923
|
-
this._sessions.set(buyerPeerId, session);
|
|
924
|
-
}
|
|
925
|
-
session.provider = providerName;
|
|
926
|
-
session.lastActivityAt = Date.now();
|
|
927
|
-
this._emitSellerSessionUpdated(buyerPeerId, session);
|
|
928
|
-
return session;
|
|
929
|
-
}
|
|
930
|
-
_emitSellerSessionUpdated(buyerPeerId, session) {
|
|
931
|
-
this.emit("session:updated", {
|
|
932
|
-
buyerPeerId,
|
|
933
|
-
sessionId: session.sessionId,
|
|
934
|
-
provider: session.provider,
|
|
935
|
-
startedAt: session.startedAt,
|
|
936
|
-
lastActivityAt: session.lastActivityAt,
|
|
937
|
-
totalRequests: session.totalRequests,
|
|
938
|
-
totalTokens: session.totalTokens,
|
|
939
|
-
avgLatencyMs: session.totalRequests > 0 ? session.totalLatencyMs / session.totalRequests : 0,
|
|
940
|
-
settling: Boolean(session.settling),
|
|
941
|
-
});
|
|
942
|
-
}
|
|
943
|
-
/** Estimate tokens from byte lengths (rough: ~4 chars per token). */
|
|
944
|
-
_estimateTokens(inputBytes, outputBytes) {
|
|
945
|
-
const inputTokens = Math.max(1, Math.round(inputBytes / 4));
|
|
946
|
-
const outputTokens = Math.max(1, Math.round(outputBytes / 4));
|
|
947
|
-
return { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens };
|
|
948
|
-
}
|
|
949
|
-
async _recordMetering(buyerPeerId, providerName, providerPricingUsdPerMillion, request, statusCode, latencyMs, inputBytes, outputBytes) {
|
|
950
|
-
if (!this._identity)
|
|
951
|
-
return;
|
|
952
|
-
const sellerPeerId = this._identity.peerId;
|
|
953
|
-
const isSSE = request.headers["accept"]?.includes("text/event-stream") ?? false;
|
|
954
|
-
const tokens = this._estimateTokens(inputBytes, outputBytes);
|
|
955
|
-
// Get or create session for this buyer
|
|
956
|
-
const session = this._getOrCreateSellerSession(buyerPeerId, providerName);
|
|
957
|
-
if (!session)
|
|
958
|
-
return;
|
|
959
|
-
session.totalRequests++;
|
|
960
|
-
session.totalTokens += tokens.totalTokens;
|
|
961
|
-
session.totalLatencyMs += latencyMs;
|
|
962
|
-
session.provider = providerName;
|
|
963
|
-
session.lastActivityAt = Date.now();
|
|
964
|
-
this._emitSellerSessionUpdated(buyerPeerId, session);
|
|
965
|
-
const metering = this._metering;
|
|
966
|
-
if (!metering) {
|
|
967
|
-
this._scheduleSettlementTimer(buyerPeerId);
|
|
968
|
-
return;
|
|
969
|
-
}
|
|
970
|
-
// Record metering event
|
|
971
|
-
const event = {
|
|
972
|
-
eventId: randomUUID(),
|
|
973
|
-
sessionId: session.sessionId,
|
|
974
|
-
timestamp: Date.now(),
|
|
975
|
-
provider: providerName,
|
|
976
|
-
sellerPeerId,
|
|
977
|
-
buyerPeerId,
|
|
978
|
-
tokens: { ...tokens, method: "content-length", confidence: "low" },
|
|
979
|
-
latencyMs,
|
|
980
|
-
statusCode,
|
|
981
|
-
wasStreaming: isSSE,
|
|
982
|
-
};
|
|
983
|
-
try {
|
|
984
|
-
metering.insertEvent(event);
|
|
985
|
-
}
|
|
986
|
-
catch (err) {
|
|
987
|
-
debugWarn(`[Node] Failed to record metering event: ${err instanceof Error ? err.message : err}`);
|
|
988
|
-
}
|
|
989
|
-
if (this._receiptGenerator) {
|
|
990
|
-
const estimatedCostUsd = (tokens.inputTokens * providerPricingUsdPerMillion.inputUsdPerMillion +
|
|
991
|
-
tokens.outputTokens * providerPricingUsdPerMillion.outputUsdPerMillion) /
|
|
992
|
-
1_000_000;
|
|
993
|
-
const effectiveUsdPerThousandTokens = tokens.totalTokens > 0 ? (estimatedCostUsd / tokens.totalTokens) * 1000 : 0;
|
|
994
|
-
// Receipt unit pricing uses USD cents per 1,000 tokens.
|
|
995
|
-
const unitPriceCentsPerThousandTokens = Math.max(0, effectiveUsdPerThousandTokens * 100);
|
|
996
|
-
const receipt = this._receiptGenerator.generate(session.sessionId, event.eventId, providerName, buyerPeerId, event.tokens, unitPriceCentsPerThousandTokens);
|
|
997
|
-
try {
|
|
998
|
-
metering.insertReceipt(receipt);
|
|
999
|
-
session.totalCostCents += receipt.costCents;
|
|
1000
|
-
}
|
|
1001
|
-
catch (err) {
|
|
1002
|
-
debugWarn(`[Node] Failed to record usage receipt: ${err instanceof Error ? err.message : err}`);
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
// Upsert session
|
|
1006
|
-
const sessionMetrics = {
|
|
1007
|
-
sessionId: session.sessionId,
|
|
1008
|
-
sellerPeerId,
|
|
1009
|
-
buyerPeerId,
|
|
1010
|
-
provider: providerName,
|
|
1011
|
-
startedAt: session.startedAt,
|
|
1012
|
-
endedAt: null,
|
|
1013
|
-
totalRequests: session.totalRequests,
|
|
1014
|
-
totalTokens: session.totalTokens,
|
|
1015
|
-
totalCostCents: session.totalCostCents,
|
|
1016
|
-
avgLatencyMs: session.totalLatencyMs / session.totalRequests,
|
|
1017
|
-
peerSwitches: 0,
|
|
1018
|
-
disputedReceipts: 0,
|
|
1019
|
-
};
|
|
1020
|
-
try {
|
|
1021
|
-
metering.upsertSession(sessionMetrics);
|
|
1022
|
-
}
|
|
1023
|
-
catch (err) {
|
|
1024
|
-
debugWarn(`[Node] Failed to upsert session: ${err instanceof Error ? err.message : err}`);
|
|
1025
|
-
}
|
|
1026
|
-
this._scheduleSettlementTimer(buyerPeerId);
|
|
1027
|
-
}
|
|
1028
680
|
async _initializePayments(dataDir) {
|
|
1029
681
|
const payments = this._config.payments;
|
|
1030
682
|
if (!payments || !payments.enabled) {
|
|
1031
683
|
return;
|
|
1032
684
|
}
|
|
1033
|
-
// Initialize
|
|
1034
|
-
if (payments.rpcUrl && payments.
|
|
1035
|
-
this.
|
|
685
|
+
// Initialize DepositsClient
|
|
686
|
+
if (payments.rpcUrl && payments.depositsAddress && payments.usdcAddress) {
|
|
687
|
+
this._depositsClient = new DepositsClient({
|
|
1036
688
|
rpcUrl: payments.rpcUrl,
|
|
1037
|
-
contractAddress: payments.
|
|
689
|
+
contractAddress: payments.depositsAddress,
|
|
1038
690
|
usdcAddress: payments.usdcAddress,
|
|
1039
691
|
});
|
|
1040
|
-
debugLog(`[Node]
|
|
1041
|
-
}
|
|
1042
|
-
if (!this._metering) {
|
|
1043
|
-
debugWarn("[Node] Payments enabled but metering storage is unavailable; skipping balance manager wiring");
|
|
1044
|
-
return;
|
|
692
|
+
debugLog(`[Node] DepositsClient initialized (contract=${payments.depositsAddress.slice(0, 10)}...)`);
|
|
1045
693
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
const existing = this._settlementTimers.get(buyerPeerId);
|
|
1054
|
-
if (existing) {
|
|
1055
|
-
clearTimeout(existing);
|
|
1056
|
-
}
|
|
1057
|
-
const idleMs = this._config.payments?.settlementIdleMs ?? 30_000;
|
|
1058
|
-
const timer = setTimeout(() => {
|
|
1059
|
-
void this._finalizeSession(buyerPeerId, "idle-timeout");
|
|
1060
|
-
}, idleMs);
|
|
1061
|
-
if (typeof timer.unref === "function") {
|
|
1062
|
-
timer.unref();
|
|
694
|
+
// Initialize ChannelsClient
|
|
695
|
+
if (payments.rpcUrl && payments.channelsAddress) {
|
|
696
|
+
this._channelsClient = new ChannelsClient({
|
|
697
|
+
rpcUrl: payments.rpcUrl,
|
|
698
|
+
contractAddress: payments.channelsAddress,
|
|
699
|
+
});
|
|
700
|
+
debugLog(`[Node] ChannelsClient initialized (contract=${payments.channelsAddress.slice(0, 10)}...)`);
|
|
1063
701
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
702
|
+
// Initialize StakingClient
|
|
703
|
+
if (payments.rpcUrl && payments.stakingAddress && payments.usdcAddress) {
|
|
704
|
+
this._stakingClient = new StakingClient({
|
|
705
|
+
rpcUrl: payments.rpcUrl,
|
|
706
|
+
contractAddress: payments.stakingAddress,
|
|
707
|
+
usdcAddress: payments.usdcAddress,
|
|
708
|
+
});
|
|
709
|
+
debugLog(`[Node] StakingClient initialized (contract=${payments.stakingAddress.slice(0, 10)}...)`);
|
|
1070
710
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
711
|
+
// Initialize IdentityClient (ERC-8004 IdentityRegistry)
|
|
712
|
+
if (payments.rpcUrl && payments.identityRegistryAddress) {
|
|
713
|
+
this._identityClient = new IdentityClient({
|
|
714
|
+
rpcUrl: payments.rpcUrl,
|
|
715
|
+
contractAddress: payments.identityRegistryAddress,
|
|
716
|
+
});
|
|
717
|
+
debugLog(`[Node] IdentityClient initialized (contract=${payments.identityRegistryAddress.slice(0, 10)}...)`);
|
|
1076
718
|
}
|
|
1077
|
-
//
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
const sessionIdHex = "0x" + bytesToHex(session.sessionIdBytes);
|
|
719
|
+
// Initialize ChannelStore for persistent payment channels (shared instance)
|
|
720
|
+
const paymentsDir = join(dataDir, "payments");
|
|
721
|
+
if (!this._channelStore) {
|
|
1081
722
|
try {
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
debugLog(`[Node] Ghost buyer — opening dispute with lastAckedTotal=${session.lastAckedTotal}`);
|
|
1085
|
-
await this._escrowClient.openDispute(sellerWallet, sessionIdHex, session.lastAckedTotal);
|
|
1086
|
-
}
|
|
1087
|
-
else if (session.runningTotal > 0n) {
|
|
1088
|
-
// No acks but work was done — open dispute with running total
|
|
1089
|
-
debugLog(`[Node] Ghost buyer — opening dispute with runningTotal=${session.runningTotal}`);
|
|
1090
|
-
await this._escrowClient.openDispute(sellerWallet, sessionIdHex, session.runningTotal);
|
|
1091
|
-
}
|
|
1092
|
-
else {
|
|
1093
|
-
// No work done — lock expires after 1 hour automatically
|
|
1094
|
-
debugLog(`[Node] Ghost buyer — no work done, lock will expire`);
|
|
1095
|
-
}
|
|
723
|
+
this._channelStore = new ChannelStore(paymentsDir);
|
|
724
|
+
debugLog("[Node] ChannelStore initialized");
|
|
1096
725
|
}
|
|
1097
726
|
catch (err) {
|
|
1098
|
-
debugWarn(`[Node]
|
|
727
|
+
debugWarn(`[Node] ChannelStore unavailable: ${err instanceof Error ? err.message : err}`);
|
|
1099
728
|
}
|
|
1100
|
-
this._sessions.delete(buyerPeerId);
|
|
1101
|
-
this.emit("session:finalized", {
|
|
1102
|
-
buyerPeerId,
|
|
1103
|
-
sessionId: session.sessionId,
|
|
1104
|
-
reason: "ghost-disconnect",
|
|
1105
|
-
});
|
|
1106
|
-
return;
|
|
1107
729
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
this.
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
void this._finalizeSession(buyerPeerId, "retry");
|
|
1141
|
-
}, 10_000);
|
|
1142
|
-
if (typeof retry.unref === "function") {
|
|
1143
|
-
retry.unref();
|
|
730
|
+
// Initialize SellerPaymentManager for seller role
|
|
731
|
+
if (this._config.role === 'seller' && this._identity && this._channelStore &&
|
|
732
|
+
payments.rpcUrl && payments.channelsAddress) {
|
|
733
|
+
const sellerConfig = {
|
|
734
|
+
rpcUrl: payments.rpcUrl,
|
|
735
|
+
channelsContractAddress: payments.channelsAddress,
|
|
736
|
+
chainId: payments.chainId ?? 8453,
|
|
737
|
+
dataDir: paymentsDir,
|
|
738
|
+
...(payments.minBudgetPerRequest ? { minBudgetPerRequest: payments.minBudgetPerRequest } : {}),
|
|
739
|
+
};
|
|
740
|
+
this._sellerPaymentManager = new SellerPaymentManager(this._identity, sellerConfig, this._channelStore);
|
|
741
|
+
debugLog(`[Node] SellerPaymentManager initialized`);
|
|
742
|
+
// Startup recovery: check for timed-out sessions
|
|
743
|
+
await this._sellerPaymentManager.checkTimeouts();
|
|
744
|
+
// Initialize CloseRequested polling cursor to current block
|
|
745
|
+
try {
|
|
746
|
+
this._closeRequestedFromBlock = await this._sellerPaymentManager.channelsClient.getBlockNumber();
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
this._closeRequestedFromBlock = 0;
|
|
750
|
+
}
|
|
751
|
+
// Start periodic timeout checker + CloseRequested poller (every 60s)
|
|
752
|
+
this._timeoutCheckerInterval = setInterval(() => {
|
|
753
|
+
void this._sellerPaymentManager?.checkTimeouts();
|
|
754
|
+
if (this._sellerPaymentManager) {
|
|
755
|
+
void this._sellerPaymentManager.pollCloseRequested(this._closeRequestedFromBlock).then((nextBlock) => {
|
|
756
|
+
this._closeRequestedFromBlock = nextBlock;
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
}, 60_000);
|
|
760
|
+
if (typeof this._timeoutCheckerInterval.unref === "function") {
|
|
761
|
+
this._timeoutCheckerInterval.unref();
|
|
1144
762
|
}
|
|
1145
|
-
this._settlementTimers.set(buyerPeerId, retry);
|
|
1146
763
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
if (this._sessions.size === 0)
|
|
764
|
+
if (!this._metering) {
|
|
765
|
+
debugWarn("[Node] Payments enabled but metering storage is unavailable; skipping balance manager wiring");
|
|
1150
766
|
return;
|
|
1151
|
-
|
|
1152
|
-
|
|
767
|
+
}
|
|
768
|
+
this._balanceManager = new BalanceManager();
|
|
769
|
+
await this._balanceManager.load(paymentsDir).catch((err) => {
|
|
770
|
+
debugWarn(`[Node] Failed to load payment balances: ${err instanceof Error ? err.message : err}`);
|
|
771
|
+
});
|
|
1153
772
|
}
|
|
1154
773
|
async _getOrCreateConnection(peer) {
|
|
1155
774
|
if (!this._connectionManager || !this._identity) {
|
|
@@ -1235,292 +854,6 @@ export class AntseedNode extends EventEmitter {
|
|
|
1235
854
|
this._muxes.set(peerId, mux);
|
|
1236
855
|
return mux;
|
|
1237
856
|
}
|
|
1238
|
-
// ── Seller-side bilateral payment handlers ─────────────────────
|
|
1239
|
-
/**
|
|
1240
|
-
* Handle SessionLockAuth from buyer (Task 2).
|
|
1241
|
-
* Recovers buyer address, commits lock on-chain, initializes bilateral state.
|
|
1242
|
-
*/
|
|
1243
|
-
async _handleSessionLockAuth(buyerPeerId, payload, paymentMux) {
|
|
1244
|
-
if (!this._identity || !this._escrowClient) {
|
|
1245
|
-
paymentMux.sendSessionLockReject({
|
|
1246
|
-
sessionId: payload.sessionId,
|
|
1247
|
-
reason: "Escrow client not configured",
|
|
1248
|
-
});
|
|
1249
|
-
return;
|
|
1250
|
-
}
|
|
1251
|
-
try {
|
|
1252
|
-
const sellerWallet = identityToEvmWallet(this._identity);
|
|
1253
|
-
const lockedAmount = BigInt(payload.lockedAmount);
|
|
1254
|
-
// Recover buyer address from ECDSA signature
|
|
1255
|
-
const lockMsgHash = buildLockMessageHash(payload.sessionId, sellerWallet.address, lockedAmount);
|
|
1256
|
-
const buyerEvmAddress = verifyMessage(getBytes(lockMsgHash), payload.buyerSig);
|
|
1257
|
-
// Submit commit_lock on-chain
|
|
1258
|
-
const txHash = await this._escrowClient.commitLock(sellerWallet, buyerEvmAddress, payload.sessionId, lockedAmount, payload.buyerSig);
|
|
1259
|
-
// Initialize or update bilateral session state
|
|
1260
|
-
let session = this._sessions.get(buyerPeerId);
|
|
1261
|
-
if (!session) {
|
|
1262
|
-
session = this._getOrCreateSellerSession(buyerPeerId, this._providers[0]?.name ?? "unknown");
|
|
1263
|
-
}
|
|
1264
|
-
if (session) {
|
|
1265
|
-
// Override sessionId with the one from the lock auth (buyer-chosen)
|
|
1266
|
-
session.sessionId = payload.sessionId;
|
|
1267
|
-
session.sessionIdBytes = hexToBytes(payload.sessionId.replace(/^0x/, ""));
|
|
1268
|
-
session.lockCommitted = true;
|
|
1269
|
-
session.lockedAmount = lockedAmount;
|
|
1270
|
-
session.runningTotal = 0n;
|
|
1271
|
-
session.ackedRequestCount = 0;
|
|
1272
|
-
session.lastAckedTotal = 0n;
|
|
1273
|
-
session.awaitingAck = false;
|
|
1274
|
-
session.buyerEvmAddress = buyerEvmAddress;
|
|
1275
|
-
}
|
|
1276
|
-
debugLog(`[Node] Lock committed for buyer ${buyerPeerId.slice(0, 12)}... amount=${lockedAmount} tx=${txHash.slice(0, 12)}...`);
|
|
1277
|
-
paymentMux.sendSessionLockConfirm({
|
|
1278
|
-
sessionId: payload.sessionId,
|
|
1279
|
-
txSignature: txHash,
|
|
1280
|
-
});
|
|
1281
|
-
}
|
|
1282
|
-
catch (err) {
|
|
1283
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
1284
|
-
debugWarn(`[Node] Failed to commit lock for ${buyerPeerId.slice(0, 12)}...: ${reason}`);
|
|
1285
|
-
paymentMux.sendSessionLockReject({
|
|
1286
|
-
sessionId: payload.sessionId,
|
|
1287
|
-
reason,
|
|
1288
|
-
});
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
/**
|
|
1292
|
-
* Generate and send a bilateral receipt after processing a request (Task 3).
|
|
1293
|
-
*/
|
|
1294
|
-
async _sendBilateralReceipt(_buyerPeerId, session, providerPricingUsdPerMillion, responseBody, paymentMux) {
|
|
1295
|
-
if (!this._identity)
|
|
1296
|
-
return;
|
|
1297
|
-
// Calculate incremental cost in USDC base units (6 decimals)
|
|
1298
|
-
// Estimate tokens from response body size
|
|
1299
|
-
const tokens = this._estimateTokens(0, responseBody.length);
|
|
1300
|
-
const costUSD = (tokens.inputTokens * providerPricingUsdPerMillion.inputUsdPerMillion +
|
|
1301
|
-
tokens.outputTokens * providerPricingUsdPerMillion.outputUsdPerMillion) /
|
|
1302
|
-
1_000_000;
|
|
1303
|
-
const costBaseUnits = BigInt(Math.round(costUSD * 1_000_000));
|
|
1304
|
-
// Update running total
|
|
1305
|
-
session.runningTotal += costBaseUnits;
|
|
1306
|
-
// SHA-256 hash of response body for proof of work
|
|
1307
|
-
const responseHash = createHash("sha256").update(responseBody).digest();
|
|
1308
|
-
// Build receipt message and sign with Ed25519
|
|
1309
|
-
const receiptMsg = buildReceiptMessage(session.sessionIdBytes, session.runningTotal, session.totalRequests, new Uint8Array(responseHash));
|
|
1310
|
-
const sellerSig = await signMessageEd25519(this._identity, receiptMsg);
|
|
1311
|
-
paymentMux.sendSellerReceipt({
|
|
1312
|
-
sessionId: session.sessionId,
|
|
1313
|
-
runningTotal: session.runningTotal.toString(),
|
|
1314
|
-
requestCount: session.totalRequests,
|
|
1315
|
-
responseHash: bytesToHex(new Uint8Array(responseHash)),
|
|
1316
|
-
sellerSig: bytesToHex(sellerSig),
|
|
1317
|
-
});
|
|
1318
|
-
session.awaitingAck = true;
|
|
1319
|
-
// Send TopUpRequest if running total > 80% of locked amount
|
|
1320
|
-
if (session.lockedAmount > 0n && session.runningTotal * 100n > session.lockedAmount * 80n) {
|
|
1321
|
-
const additionalAmount = session.lockedAmount; // Request same amount again
|
|
1322
|
-
paymentMux.sendTopUpRequest({
|
|
1323
|
-
sessionId: session.sessionId,
|
|
1324
|
-
additionalAmount: additionalAmount.toString(),
|
|
1325
|
-
currentRunningTotal: session.runningTotal.toString(),
|
|
1326
|
-
currentLockedAmount: session.lockedAmount.toString(),
|
|
1327
|
-
});
|
|
1328
|
-
debugLog(`[Node] TopUpRequest sent for session ${session.sessionId.slice(0, 8)}... (running=${session.runningTotal}, locked=${session.lockedAmount})`);
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
/**
|
|
1332
|
-
* Handle BuyerAck (Task 4).
|
|
1333
|
-
* Verifies buyer's Ed25519 ack signature and updates session state.
|
|
1334
|
-
*/
|
|
1335
|
-
async _handleBuyerAck(buyerPeerId, payload) {
|
|
1336
|
-
const session = this._sessions.get(buyerPeerId);
|
|
1337
|
-
if (!session || !session.lockCommitted) {
|
|
1338
|
-
debugWarn(`[Node] Received BuyerAck for unknown/uncommitted session from ${buyerPeerId.slice(0, 12)}...`);
|
|
1339
|
-
return;
|
|
1340
|
-
}
|
|
1341
|
-
try {
|
|
1342
|
-
// Verify buyer's Ed25519 ack signature
|
|
1343
|
-
const buyerPublicKey = hexToBytes(buyerPeerId);
|
|
1344
|
-
const ackMsg = buildAckMessage(session.sessionIdBytes, BigInt(payload.runningTotal), payload.requestCount);
|
|
1345
|
-
const sigBytes = hexToBytes(payload.buyerSig);
|
|
1346
|
-
const valid = await verifyMessageEd25519(buyerPublicKey, sigBytes, ackMsg);
|
|
1347
|
-
if (!valid) {
|
|
1348
|
-
debugWarn(`[Node] Invalid BuyerAck signature from ${buyerPeerId.slice(0, 12)}...`);
|
|
1349
|
-
return;
|
|
1350
|
-
}
|
|
1351
|
-
session.ackedRequestCount = payload.requestCount;
|
|
1352
|
-
session.lastAckedTotal = BigInt(payload.runningTotal);
|
|
1353
|
-
session.awaitingAck = false;
|
|
1354
|
-
debugLog(`[Node] BuyerAck received: requestCount=${payload.requestCount} runningTotal=${payload.runningTotal}`);
|
|
1355
|
-
}
|
|
1356
|
-
catch (err) {
|
|
1357
|
-
debugWarn(`[Node] Failed to process BuyerAck: ${err instanceof Error ? err.message : err}`);
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
/**
|
|
1361
|
-
* Handle SessionEnd from buyer (Task 5).
|
|
1362
|
-
* Submits settlement on-chain and cleans up.
|
|
1363
|
-
*/
|
|
1364
|
-
async _handleSessionEnd(buyerPeerId, payload) {
|
|
1365
|
-
const session = this._sessions.get(buyerPeerId);
|
|
1366
|
-
if (!session || !session.lockCommitted) {
|
|
1367
|
-
debugWarn(`[Node] Received SessionEnd for unknown/uncommitted session from ${buyerPeerId.slice(0, 12)}...`);
|
|
1368
|
-
return;
|
|
1369
|
-
}
|
|
1370
|
-
if (!this._identity || !this._escrowClient) {
|
|
1371
|
-
debugWarn(`[Node] Cannot process SessionEnd — escrow client not available`);
|
|
1372
|
-
return;
|
|
1373
|
-
}
|
|
1374
|
-
try {
|
|
1375
|
-
const sellerWallet = identityToEvmWallet(this._identity);
|
|
1376
|
-
const sessionIdHex = "0x" + bytesToHex(session.sessionIdBytes);
|
|
1377
|
-
// Submit settlement on-chain with buyer's ECDSA signature and score
|
|
1378
|
-
const txHash = await this._escrowClient.settle(sellerWallet, sessionIdHex, BigInt(payload.runningTotal), payload.score, payload.buyerSig);
|
|
1379
|
-
debugLog(`[Node] Session settled on-chain: ${session.sessionId.slice(0, 8)}... tx=${txHash.slice(0, 12)}... score=${payload.score}`);
|
|
1380
|
-
// Clean up session
|
|
1381
|
-
this._sessions.delete(buyerPeerId);
|
|
1382
|
-
const timer = this._settlementTimers.get(buyerPeerId);
|
|
1383
|
-
if (timer) {
|
|
1384
|
-
clearTimeout(timer);
|
|
1385
|
-
this._settlementTimers.delete(buyerPeerId);
|
|
1386
|
-
}
|
|
1387
|
-
this.emit("session:settled", {
|
|
1388
|
-
buyerPeerId,
|
|
1389
|
-
sessionId: session.sessionId,
|
|
1390
|
-
runningTotal: payload.runningTotal,
|
|
1391
|
-
score: payload.score,
|
|
1392
|
-
txHash,
|
|
1393
|
-
});
|
|
1394
|
-
}
|
|
1395
|
-
catch (err) {
|
|
1396
|
-
debugWarn(`[Node] Failed to settle session ${session.sessionId}: ${err instanceof Error ? err.message : err}`);
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
/**
|
|
1400
|
-
* Handle TopUpAuth from buyer (Task 6).
|
|
1401
|
-
* Calls extendLock on-chain and updates session.
|
|
1402
|
-
*/
|
|
1403
|
-
async _handleTopUpAuth(buyerPeerId, payload) {
|
|
1404
|
-
const session = this._sessions.get(buyerPeerId);
|
|
1405
|
-
if (!session || !session.lockCommitted) {
|
|
1406
|
-
debugWarn(`[Node] Received TopUpAuth for unknown/uncommitted session from ${buyerPeerId.slice(0, 12)}...`);
|
|
1407
|
-
return;
|
|
1408
|
-
}
|
|
1409
|
-
if (!this._identity || !this._escrowClient) {
|
|
1410
|
-
debugWarn(`[Node] Cannot process TopUpAuth — escrow client not available`);
|
|
1411
|
-
return;
|
|
1412
|
-
}
|
|
1413
|
-
try {
|
|
1414
|
-
const sellerWallet = identityToEvmWallet(this._identity);
|
|
1415
|
-
const sessionIdHex = "0x" + bytesToHex(session.sessionIdBytes);
|
|
1416
|
-
const additionalAmount = BigInt(payload.additionalAmount);
|
|
1417
|
-
const txHash = await this._escrowClient.extendLock(sellerWallet, sessionIdHex, additionalAmount, payload.buyerSig);
|
|
1418
|
-
session.lockedAmount += additionalAmount;
|
|
1419
|
-
debugLog(`[Node] TopUp committed: session=${session.sessionId.slice(0, 8)}... additional=${additionalAmount} newTotal=${session.lockedAmount} tx=${txHash.slice(0, 12)}...`);
|
|
1420
|
-
}
|
|
1421
|
-
catch (err) {
|
|
1422
|
-
debugWarn(`[Node] Failed to extend lock for session ${session.sessionId}: ${err instanceof Error ? err.message : err}`);
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
// ── Buyer-side payment helpers ─────────────────────────────────
|
|
1426
|
-
/**
|
|
1427
|
-
* Create a PaymentMux for a buyer-side outbound connection and register
|
|
1428
|
-
* buyer-side handlers (lock confirm, lock reject, seller receipt, top-up request).
|
|
1429
|
-
*/
|
|
1430
|
-
_getOrCreateBuyerPaymentMux(peerId, conn) {
|
|
1431
|
-
const existing = this._paymentMuxes.get(peerId);
|
|
1432
|
-
if (existing)
|
|
1433
|
-
return existing;
|
|
1434
|
-
const pmux = new PaymentMux(conn);
|
|
1435
|
-
this._paymentMuxes.set(peerId, pmux);
|
|
1436
|
-
const bpm = this._buyerPaymentManager;
|
|
1437
|
-
if (!bpm)
|
|
1438
|
-
return pmux;
|
|
1439
|
-
pmux.onSessionLockConfirm((payload) => {
|
|
1440
|
-
bpm.handleLockConfirm(peerId, payload);
|
|
1441
|
-
});
|
|
1442
|
-
pmux.onSessionLockReject((payload) => {
|
|
1443
|
-
bpm.handleLockReject(peerId, payload);
|
|
1444
|
-
});
|
|
1445
|
-
pmux.onSellerReceipt((receipt) => {
|
|
1446
|
-
void bpm.handleSellerReceipt(peerId, receipt, pmux);
|
|
1447
|
-
});
|
|
1448
|
-
pmux.onTopUpRequest((request) => {
|
|
1449
|
-
void bpm.handleTopUpRequest(peerId, request, pmux);
|
|
1450
|
-
});
|
|
1451
|
-
return pmux;
|
|
1452
|
-
}
|
|
1453
|
-
/**
|
|
1454
|
-
* Initiate a lock with a seller peer. Creates PaymentMux, signs lock auth,
|
|
1455
|
-
* and waits for confirmation before returning.
|
|
1456
|
-
*/
|
|
1457
|
-
async _initiateBuyerLock(peer, conn) {
|
|
1458
|
-
const bpm = this._buyerPaymentManager;
|
|
1459
|
-
if (!bpm)
|
|
1460
|
-
return;
|
|
1461
|
-
// Mark as locked so we don't re-initiate
|
|
1462
|
-
this._buyerLockedPeers.add(peer.peerId);
|
|
1463
|
-
const pmux = this._getOrCreateBuyerPaymentMux(peer.peerId, conn);
|
|
1464
|
-
// Determine seller EVM address — prefer from peer metadata
|
|
1465
|
-
const sellerEvmAddress = peer.evmAddress ?? "";
|
|
1466
|
-
if (!sellerEvmAddress) {
|
|
1467
|
-
debugWarn(`[Node] Seller ${peer.peerId.slice(0, 12)}... has no EVM address; skipping lock initiation`);
|
|
1468
|
-
return;
|
|
1469
|
-
}
|
|
1470
|
-
try {
|
|
1471
|
-
await bpm.initiateLock(peer.peerId, sellerEvmAddress, pmux);
|
|
1472
|
-
debugLog(`[Node] Lock initiated for seller ${peer.peerId.slice(0, 12)}..., waiting for confirmation...`);
|
|
1473
|
-
// Wait for lock confirmation (polls every 200ms, 30s timeout)
|
|
1474
|
-
await this._waitForLockConfirmation(peer.peerId);
|
|
1475
|
-
debugLog(`[Node] Lock confirmed for seller ${peer.peerId.slice(0, 12)}...`);
|
|
1476
|
-
}
|
|
1477
|
-
catch (err) {
|
|
1478
|
-
debugWarn(`[Node] Lock initiation/confirmation failed for ${peer.peerId.slice(0, 12)}...: ${err instanceof Error ? err.message : err}`);
|
|
1479
|
-
// Remove from locked set so next request can retry
|
|
1480
|
-
this._buyerLockedPeers.delete(peer.peerId);
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
/**
|
|
1484
|
-
* Poll until the lock for a seller is confirmed or rejected.
|
|
1485
|
-
* Polls every 200ms with a 30-second timeout.
|
|
1486
|
-
*/
|
|
1487
|
-
async _waitForLockConfirmation(sellerPeerId) {
|
|
1488
|
-
const bpm = this._buyerPaymentManager;
|
|
1489
|
-
if (!bpm)
|
|
1490
|
-
return;
|
|
1491
|
-
const pollIntervalMs = 200;
|
|
1492
|
-
const timeoutMs = 30_000;
|
|
1493
|
-
const deadline = Date.now() + timeoutMs;
|
|
1494
|
-
while (Date.now() < deadline) {
|
|
1495
|
-
if (bpm.isLockConfirmed(sellerPeerId)) {
|
|
1496
|
-
return;
|
|
1497
|
-
}
|
|
1498
|
-
if (bpm.isLockRejected(sellerPeerId)) {
|
|
1499
|
-
throw new Error(`Lock rejected by seller ${sellerPeerId.slice(0, 12)}...`);
|
|
1500
|
-
}
|
|
1501
|
-
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
1502
|
-
}
|
|
1503
|
-
throw new Error(`Lock confirmation timed out for seller ${sellerPeerId.slice(0, 12)}... (${timeoutMs}ms)`);
|
|
1504
|
-
}
|
|
1505
|
-
/**
|
|
1506
|
-
* End all active buyer payment sessions (called during shutdown).
|
|
1507
|
-
*/
|
|
1508
|
-
async _endAllBuyerSessions() {
|
|
1509
|
-
const bpm = this._buyerPaymentManager;
|
|
1510
|
-
if (!bpm)
|
|
1511
|
-
return;
|
|
1512
|
-
const sessions = bpm.getActiveSessions();
|
|
1513
|
-
if (sessions.length === 0)
|
|
1514
|
-
return;
|
|
1515
|
-
debugLog(`[Node] Ending ${sessions.length} buyer payment session(s)...`);
|
|
1516
|
-
await Promise.allSettled(sessions.map((session) => {
|
|
1517
|
-
const pmux = this._paymentMuxes.get(session.sellerPeerId);
|
|
1518
|
-
if (pmux) {
|
|
1519
|
-
return bpm.endSession(session.sellerPeerId, pmux, 80);
|
|
1520
|
-
}
|
|
1521
|
-
return Promise.resolve();
|
|
1522
|
-
}));
|
|
1523
|
-
}
|
|
1524
857
|
_resolvePublicAddress(result) {
|
|
1525
858
|
const metadataPublicAddress = result.metadata.publicAddress?.trim();
|
|
1526
859
|
if (metadataPublicAddress && parsePublicAddress(metadataPublicAddress) !== null) {
|
|
@@ -1576,59 +909,13 @@ export class AntseedNode extends EventEmitter {
|
|
|
1576
909
|
defaultOutputUsdPerMillion: firstProvider?.defaultPricing.outputUsdPerMillion,
|
|
1577
910
|
maxConcurrency: firstProvider?.maxConcurrency,
|
|
1578
911
|
currentLoad: firstProvider?.currentLoad,
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
onChainSessionCount: result.metadata.onChainSessionCount,
|
|
1582
|
-
onChainDisputeCount: result.metadata.onChainDisputeCount,
|
|
1583
|
-
trustScore: result.metadata.onChainReputation,
|
|
912
|
+
onChainChannelCount: result.metadata.onChainChannelCount,
|
|
913
|
+
onChainGhostCount: result.metadata.onChainGhostCount,
|
|
1584
914
|
};
|
|
1585
915
|
}
|
|
1586
|
-
_adjustProviderLoad(providerName, delta) {
|
|
1587
|
-
const nextLoad = Math.max(0, (this._providerLoadCounts.get(providerName) ?? 0) + delta);
|
|
1588
|
-
this._providerLoadCounts.set(providerName, nextLoad);
|
|
1589
|
-
if (!this._announcer)
|
|
1590
|
-
return;
|
|
1591
|
-
this._announcer.updateLoad(providerName, nextLoad);
|
|
1592
|
-
this._scheduleMetadataRefresh();
|
|
1593
|
-
}
|
|
1594
|
-
_scheduleMetadataRefresh() {
|
|
1595
|
-
if (!this._announcer || this._metadataRefreshTimer) {
|
|
1596
|
-
return;
|
|
1597
|
-
}
|
|
1598
|
-
const timer = setTimeout(() => {
|
|
1599
|
-
this._metadataRefreshTimer = null;
|
|
1600
|
-
const announcer = this._announcer;
|
|
1601
|
-
if (!announcer)
|
|
1602
|
-
return;
|
|
1603
|
-
void announcer.refreshMetadata().catch((err) => {
|
|
1604
|
-
debugWarn(`[Node] Failed to refresh metadata snapshot: ${err instanceof Error ? err.message : err}`);
|
|
1605
|
-
});
|
|
1606
|
-
}, AntseedNode._METADATA_REFRESH_DEBOUNCE_MS);
|
|
1607
|
-
this._metadataRefreshTimer = timer;
|
|
1608
|
-
this._unrefTimer(timer);
|
|
1609
|
-
}
|
|
1610
|
-
_unrefTimer(timer) {
|
|
1611
|
-
if (typeof timer.unref === "function") {
|
|
1612
|
-
timer.unref();
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
916
|
}
|
|
1616
917
|
function parsePeerAddress(address) {
|
|
1617
918
|
const parts = address.split(":");
|
|
1618
919
|
return { host: parts[0], port: parseInt(parts[1] ?? "6882", 10) };
|
|
1619
920
|
}
|
|
1620
|
-
function concatChunks(chunks) {
|
|
1621
|
-
if (chunks.length === 0)
|
|
1622
|
-
return new Uint8Array(0);
|
|
1623
|
-
if (chunks.length === 1)
|
|
1624
|
-
return chunks[0];
|
|
1625
|
-
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
1626
|
-
const output = new Uint8Array(totalLength);
|
|
1627
|
-
let offset = 0;
|
|
1628
|
-
for (const chunk of chunks) {
|
|
1629
|
-
output.set(chunk, offset);
|
|
1630
|
-
offset += chunk.length;
|
|
1631
|
-
}
|
|
1632
|
-
return output;
|
|
1633
|
-
}
|
|
1634
921
|
//# sourceMappingURL=node.js.map
|