@antseed/cli 0.1.95 → 0.1.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/buyer/start.d.ts.map +1 -1
- package/dist/cli/commands/buyer/start.js +22 -2
- package/dist/cli/commands/buyer/start.js.map +1 -1
- package/dist/cli/commands/network/browse.d.ts +1 -2
- package/dist/cli/commands/network/browse.d.ts.map +1 -1
- package/dist/cli/commands/network/browse.js +528 -94
- package/dist/cli/commands/network/browse.js.map +1 -1
- package/dist/cli/commands/network/chain-config-helper.d.ts +35 -0
- package/dist/cli/commands/network/chain-config-helper.d.ts.map +1 -0
- package/dist/cli/commands/network/chain-config-helper.js +45 -0
- package/dist/cli/commands/network/chain-config-helper.js.map +1 -0
- package/dist/cli/commands/network/index.d.ts.map +1 -1
- package/dist/cli/commands/network/index.js +2 -0
- package/dist/cli/commands/network/index.js.map +1 -1
- package/dist/cli/commands/network/peer.d.ts +6 -0
- package/dist/cli/commands/network/peer.d.ts.map +1 -0
- package/dist/cli/commands/network/peer.js +297 -0
- package/dist/cli/commands/network/peer.js.map +1 -0
- package/dist/cli/commands/network/pricing-format.d.ts +25 -0
- package/dist/cli/commands/network/pricing-format.d.ts.map +1 -0
- package/dist/cli/commands/network/pricing-format.js +38 -0
- package/dist/cli/commands/network/pricing-format.js.map +1 -0
- package/dist/cli/commands/network/tag-filter.d.ts +30 -0
- package/dist/cli/commands/network/tag-filter.d.ts.map +1 -0
- package/dist/cli/commands/network/tag-filter.js +75 -0
- package/dist/cli/commands/network/tag-filter.js.map +1 -0
- package/dist/proxy/buyer-proxy.d.ts +5 -4
- package/dist/proxy/buyer-proxy.d.ts.map +1 -1
- package/dist/proxy/buyer-proxy.js +130 -206
- package/dist/proxy/buyer-proxy.js.map +1 -1
- package/dist/proxy/routing.d.ts +0 -1
- package/dist/proxy/routing.d.ts.map +1 -1
- package/dist/proxy/routing.js +0 -7
- package/dist/proxy/routing.js.map +1 -1
- package/package.json +6 -6
|
@@ -5,9 +5,12 @@ import { readFile } from 'node:fs/promises';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { getGlobalOptions } from '../types.js';
|
|
7
7
|
import { loadConfig } from '../../../config/loader.js';
|
|
8
|
-
import { AntseedNode } from '@antseed/node';
|
|
8
|
+
import { AntseedNode, } from '@antseed/node';
|
|
9
9
|
import { parseBootstrapList, toBootstrapConfig } from '@antseed/node/discovery';
|
|
10
10
|
import { parsePersistedPeers } from '../../../proxy/buyer-proxy.js';
|
|
11
|
+
import { buildPaymentsConfig } from './chain-config-helper.js';
|
|
12
|
+
import { collectServiceTags, parseTagFilter, peerMatchesTagFilter, serviceMatchesTagFilter, } from './tag-filter.js';
|
|
13
|
+
import { formatUsdPerMillion } from './pricing-format.js';
|
|
11
14
|
function isProcessAlive(pid) {
|
|
12
15
|
try {
|
|
13
16
|
process.kill(pid, 0);
|
|
@@ -18,12 +21,10 @@ function isProcessAlive(pid) {
|
|
|
18
21
|
}
|
|
19
22
|
}
|
|
20
23
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* its PID is still alive, and the peer list is non-empty. This avoids surfacing
|
|
24
|
-
* stale peer data from a daemon that exited without clearing the file.
|
|
24
|
+
* Load a snapshot from a live buyer daemon. Returns `null` unless the daemon is
|
|
25
|
+
* connected, its PID is alive, and the persisted peer list is non-empty.
|
|
25
26
|
*/
|
|
26
|
-
async function
|
|
27
|
+
async function loadSnapshotFromBuyerState(dataDir) {
|
|
27
28
|
try {
|
|
28
29
|
const raw = await readFile(join(dataDir, 'buyer.state.json'), 'utf-8');
|
|
29
30
|
const parsed = JSON.parse(raw);
|
|
@@ -32,126 +33,559 @@ async function loadPeersFromBuyerState(dataDir) {
|
|
|
32
33
|
if (typeof parsed.pid !== 'number' || !isProcessAlive(parsed.pid))
|
|
33
34
|
return null;
|
|
34
35
|
const peers = parsePersistedPeers(parsed);
|
|
35
|
-
|
|
36
|
+
if (peers.length === 0)
|
|
37
|
+
return null;
|
|
38
|
+
const refreshedAt = typeof parsed.onChainStatsRefreshedAt === 'number'
|
|
39
|
+
&& Number.isFinite(parsed.onChainStatsRefreshedAt)
|
|
40
|
+
? parsed.onChainStatsRefreshedAt
|
|
41
|
+
: null;
|
|
42
|
+
return {
|
|
43
|
+
peers,
|
|
44
|
+
onChainStatsRefreshedAt: refreshedAt,
|
|
45
|
+
sourceLabel: `buyer daemon (pid ${parsed.pid})`,
|
|
46
|
+
};
|
|
36
47
|
}
|
|
37
48
|
catch {
|
|
38
49
|
return null;
|
|
39
50
|
}
|
|
40
51
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
52
|
+
/**
|
|
53
|
+
* A pricing entry counts as "free" when both input and output prices are
|
|
54
|
+
* finite and zero. We intentionally don't treat undefined/negative values as
|
|
55
|
+
* free — those are "unknown" and handled separately by the formatter.
|
|
56
|
+
*/
|
|
57
|
+
function isFreePricing(pricing) {
|
|
58
|
+
return (Number.isFinite(pricing.inputUsdPerMillion)
|
|
59
|
+
&& Number.isFinite(pricing.outputUsdPerMillion)
|
|
60
|
+
&& pricing.inputUsdPerMillion === 0
|
|
61
|
+
&& pricing.outputUsdPerMillion === 0);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Derive the set of service names this peer offers, flattened across all its
|
|
65
|
+
* providers, in stable (sorted) order.
|
|
66
|
+
*/
|
|
67
|
+
function collectServiceNames(peer) {
|
|
68
|
+
const names = new Set();
|
|
69
|
+
const pricing = peer.providerPricing;
|
|
70
|
+
if (pricing) {
|
|
71
|
+
for (const entry of Object.values(pricing)) {
|
|
72
|
+
const services = entry.services;
|
|
73
|
+
if (services) {
|
|
74
|
+
for (const name of Object.keys(services)) {
|
|
75
|
+
const trimmed = name.trim();
|
|
76
|
+
if (trimmed.length > 0)
|
|
77
|
+
names.add(trimmed);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
44
81
|
}
|
|
45
|
-
|
|
46
|
-
|
|
82
|
+
return Array.from(names).sort((a, b) => a.localeCompare(b));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Collect the names of free (both input and output = $0/1M) services a peer
|
|
86
|
+
* offers. Used to populate the optional "Free" column.
|
|
87
|
+
*/
|
|
88
|
+
function collectFreeServiceNames(peer) {
|
|
89
|
+
const names = new Set();
|
|
90
|
+
const pricing = peer.providerPricing;
|
|
91
|
+
if (pricing) {
|
|
92
|
+
for (const entry of Object.values(pricing)) {
|
|
93
|
+
if (entry.services) {
|
|
94
|
+
for (const [serviceName, servicePricing] of Object.entries(entry.services)) {
|
|
95
|
+
if (isFreePricing(servicePricing))
|
|
96
|
+
names.add(serviceName);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// If the provider's defaults are free and no per-service override lifts
|
|
100
|
+
// them, consider "(default)" a free offering worth surfacing.
|
|
101
|
+
if (entry.defaults && isFreePricing(entry.defaults) && (!entry.services || Object.keys(entry.services).length === 0)) {
|
|
102
|
+
names.add('(default)');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
47
105
|
}
|
|
48
|
-
return
|
|
106
|
+
return Array.from(names).sort((a, b) => a.localeCompare(b));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Check whether the peer matches a `--service` filter. Matches on provider
|
|
110
|
+
* name OR any announced service name (case-insensitive).
|
|
111
|
+
*/
|
|
112
|
+
function peerMatchesServiceFilter(peer, filter) {
|
|
113
|
+
const needle = filter.trim().toLowerCase();
|
|
114
|
+
if (needle.length === 0)
|
|
115
|
+
return true;
|
|
116
|
+
if (peer.providers.some((p) => p.toLowerCase() === needle))
|
|
117
|
+
return true;
|
|
118
|
+
return collectServiceNames(peer).some((name) => name.toLowerCase() === needle);
|
|
49
119
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Determine the cheapest *paid* input/output price pair across all services.
|
|
122
|
+
* Free services (input=output=0) are deliberately excluded from these
|
|
123
|
+
* columns — they are surfaced in a dedicated "Free" column when present —
|
|
124
|
+
* so the In/Out $/1M columns always reflect the cheapest paid option a peer
|
|
125
|
+
* offers. If every candidate is free or unknown, the result is `null`.
|
|
126
|
+
*
|
|
127
|
+
* CAVEAT: the returned input and output are picked independently across
|
|
128
|
+
* services. A peer with serviceA=$1/$2 and serviceB=$2/$1 is rendered as
|
|
129
|
+
* $1/$1 — a lower-bound approximation, not a real single-service offer.
|
|
130
|
+
* Use `antseed network browse --services` or `antseed network peer <id>`
|
|
131
|
+
* for per-service pricing when exact comparison matters.
|
|
132
|
+
*/
|
|
133
|
+
function resolveBestPaidPricing(peer) {
|
|
134
|
+
let bestInput = null;
|
|
135
|
+
let bestOutput = null;
|
|
136
|
+
const pricing = peer.providerPricing;
|
|
137
|
+
if (pricing) {
|
|
138
|
+
for (const entry of Object.values(pricing)) {
|
|
139
|
+
const candidates = [];
|
|
140
|
+
if (entry.defaults)
|
|
141
|
+
candidates.push(entry.defaults);
|
|
142
|
+
if (entry.services)
|
|
143
|
+
candidates.push(...Object.values(entry.services));
|
|
144
|
+
for (const c of candidates) {
|
|
145
|
+
if (isFreePricing(c))
|
|
146
|
+
continue;
|
|
147
|
+
if (Number.isFinite(c.inputUsdPerMillion) && c.inputUsdPerMillion > 0) {
|
|
148
|
+
if (bestInput === null || c.inputUsdPerMillion < bestInput)
|
|
149
|
+
bestInput = c.inputUsdPerMillion;
|
|
150
|
+
}
|
|
151
|
+
if (Number.isFinite(c.outputUsdPerMillion) && c.outputUsdPerMillion > 0) {
|
|
152
|
+
if (bestOutput === null || c.outputUsdPerMillion < bestOutput)
|
|
153
|
+
bestOutput = c.outputUsdPerMillion;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Top-level defaults are only used when we have nothing else — and only if
|
|
159
|
+
// they are positive (non-free).
|
|
160
|
+
if (bestInput === null && Number.isFinite(peer.defaultInputUsdPerMillion) && (peer.defaultInputUsdPerMillion ?? 0) > 0) {
|
|
161
|
+
bestInput = peer.defaultInputUsdPerMillion ?? null;
|
|
162
|
+
}
|
|
163
|
+
if (bestOutput === null && Number.isFinite(peer.defaultOutputUsdPerMillion) && (peer.defaultOutputUsdPerMillion ?? 0) > 0) {
|
|
164
|
+
bestOutput = peer.defaultOutputUsdPerMillion ?? null;
|
|
165
|
+
}
|
|
166
|
+
return { input: bestInput, output: bestOutput };
|
|
167
|
+
}
|
|
168
|
+
function formatUsdcVolume(micros) {
|
|
169
|
+
if (typeof micros !== 'number' || !Number.isFinite(micros) || micros < 0) {
|
|
170
|
+
return chalk.dim('—');
|
|
171
|
+
}
|
|
172
|
+
const usd = micros / 1_000_000;
|
|
173
|
+
if (usd >= 1000)
|
|
174
|
+
return chalk.green(`$${usd.toFixed(0)}`);
|
|
175
|
+
if (usd >= 1)
|
|
176
|
+
return chalk.green(`$${usd.toFixed(2)}`);
|
|
177
|
+
if (usd > 0)
|
|
178
|
+
return `$${usd.toFixed(4)}`;
|
|
179
|
+
return chalk.dim('$0');
|
|
180
|
+
}
|
|
181
|
+
function formatAge(sec) {
|
|
182
|
+
if (typeof sec !== 'number' || !Number.isFinite(sec) || sec <= 0) {
|
|
183
|
+
return chalk.dim('never');
|
|
184
|
+
}
|
|
185
|
+
const nowMs = Date.now();
|
|
186
|
+
const ageMs = nowMs - sec * 1000;
|
|
187
|
+
if (ageMs < 0)
|
|
188
|
+
return chalk.dim('just now');
|
|
189
|
+
const mins = Math.floor(ageMs / 60_000);
|
|
190
|
+
if (mins < 1)
|
|
191
|
+
return 'just now';
|
|
192
|
+
if (mins < 60)
|
|
193
|
+
return `${mins}m ago`;
|
|
194
|
+
const hours = Math.floor(mins / 60);
|
|
195
|
+
if (hours < 24)
|
|
196
|
+
return `${hours}h ago`;
|
|
197
|
+
const days = Math.floor(hours / 24);
|
|
198
|
+
if (days < 30)
|
|
199
|
+
return `${days}d ago`;
|
|
200
|
+
const months = Math.floor(days / 30);
|
|
201
|
+
return `${months}mo ago`;
|
|
202
|
+
}
|
|
203
|
+
function formatHumanAgeMs(ms) {
|
|
204
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
205
|
+
return 'just now';
|
|
206
|
+
if (ms < 60_000)
|
|
207
|
+
return `${Math.floor(ms / 1000)}s ago`;
|
|
208
|
+
const mins = Math.floor(ms / 60_000);
|
|
209
|
+
if (mins < 60)
|
|
210
|
+
return `${mins}m ago`;
|
|
211
|
+
const hours = Math.floor(mins / 60);
|
|
212
|
+
if (hours < 24)
|
|
213
|
+
return `${hours}h ago`;
|
|
214
|
+
const days = Math.floor(hours / 24);
|
|
215
|
+
return `${days}d ago`;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Mark a peer as "vouched" when it has positive on-chain channel count and no
|
|
219
|
+
* ghosts. Rendered as a ✓ next to its peer id in the table.
|
|
220
|
+
*/
|
|
221
|
+
function isPeerVouched(peer) {
|
|
222
|
+
const channels = peer.onChainChannelCount ?? 0;
|
|
223
|
+
const ghosts = peer.onChainGhostCount ?? 0;
|
|
224
|
+
return channels > 0 && ghosts === 0;
|
|
225
|
+
}
|
|
226
|
+
function sortPeers(peers, sortKey) {
|
|
227
|
+
const copy = [...peers];
|
|
228
|
+
copy.sort((a, b) => {
|
|
229
|
+
switch (sortKey) {
|
|
230
|
+
case 'volume': {
|
|
231
|
+
const va = a.onChainTotalVolumeUsdcMicros ?? -1;
|
|
232
|
+
const vb = b.onChainTotalVolumeUsdcMicros ?? -1;
|
|
233
|
+
if (va !== vb)
|
|
234
|
+
return vb - va;
|
|
235
|
+
return (b.onChainChannelCount ?? 0) - (a.onChainChannelCount ?? 0);
|
|
236
|
+
}
|
|
237
|
+
case 'sessions': {
|
|
238
|
+
const ca = a.onChainChannelCount ?? -1;
|
|
239
|
+
const cb = b.onChainChannelCount ?? -1;
|
|
240
|
+
if (ca !== cb)
|
|
241
|
+
return cb - ca;
|
|
242
|
+
return (b.onChainTotalVolumeUsdcMicros ?? 0) - (a.onChainTotalVolumeUsdcMicros ?? 0);
|
|
243
|
+
}
|
|
244
|
+
case 'price': {
|
|
245
|
+
const pa = resolveBestPaidPricing(a).input ?? Number.POSITIVE_INFINITY;
|
|
246
|
+
const pb = resolveBestPaidPricing(b).input ?? Number.POSITIVE_INFINITY;
|
|
247
|
+
if (pa !== pb)
|
|
248
|
+
return pa - pb;
|
|
249
|
+
return (b.onChainTotalVolumeUsdcMicros ?? 0) - (a.onChainTotalVolumeUsdcMicros ?? 0);
|
|
250
|
+
}
|
|
251
|
+
case 'recent': {
|
|
252
|
+
const ra = a.onChainLastSettledAtSec ?? 0;
|
|
253
|
+
const rb = b.onChainLastSettledAtSec ?? 0;
|
|
254
|
+
if (ra !== rb)
|
|
255
|
+
return rb - ra;
|
|
256
|
+
return (b.onChainTotalVolumeUsdcMicros ?? 0) - (a.onChainTotalVolumeUsdcMicros ?? 0);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
62
259
|
});
|
|
260
|
+
return copy;
|
|
261
|
+
}
|
|
262
|
+
function parseSortKey(raw) {
|
|
263
|
+
const normalized = (raw ?? 'volume').trim().toLowerCase();
|
|
264
|
+
if (normalized === 'sessions' || normalized === 'price' || normalized === 'recent' || normalized === 'volume') {
|
|
265
|
+
return normalized;
|
|
266
|
+
}
|
|
267
|
+
return 'volume';
|
|
268
|
+
}
|
|
269
|
+
function parseTopLimit(raw) {
|
|
270
|
+
const parsed = raw === undefined ? 20 : parseInt(raw, 10);
|
|
271
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
272
|
+
return 20;
|
|
273
|
+
return Math.min(parsed, 500);
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Render the default compact table: one row per peer with aggregate metrics.
|
|
277
|
+
* Full peer ids are printed so the output is directly copy-pasteable into
|
|
278
|
+
* `antseed buyer connection set --peer <id>` without any truncation.
|
|
279
|
+
* The "Free" column is only emitted when at least one displayed peer has
|
|
280
|
+
* free services; otherwise it's omitted so the common-case table stays
|
|
281
|
+
* compact.
|
|
282
|
+
*/
|
|
283
|
+
function renderCompactTable(peers, hasChainData) {
|
|
284
|
+
const freeNamesByPeer = new Map(peers.map((peer) => [peer.peerId, collectFreeServiceNames(peer)]));
|
|
285
|
+
const anyFreeService = Array.from(freeNamesByPeer.values()).some((names) => names.length > 0);
|
|
286
|
+
const head = [
|
|
287
|
+
chalk.bold('Peer'),
|
|
288
|
+
chalk.bold('Name'),
|
|
289
|
+
chalk.bold('Providers'),
|
|
290
|
+
chalk.bold('Services'),
|
|
291
|
+
chalk.bold('Min In $/1M'),
|
|
292
|
+
chalk.bold('Min Out $/1M'),
|
|
293
|
+
];
|
|
294
|
+
if (anyFreeService)
|
|
295
|
+
head.push(chalk.bold('Free'));
|
|
296
|
+
head.push(chalk.bold('Sessions'), chalk.bold('Ghosts'), chalk.bold('Volume'), chalk.bold('Last settled'), chalk.bold('Load'));
|
|
297
|
+
const table = new Table({ head, wordWrap: true });
|
|
63
298
|
for (const peer of peers) {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
299
|
+
const services = collectServiceNames(peer);
|
|
300
|
+
const servicesCell = services.length === 0
|
|
301
|
+
? chalk.dim('—')
|
|
302
|
+
: services.length <= 2
|
|
303
|
+
? services.join(', ')
|
|
304
|
+
: `${services.slice(0, 2).join(', ')} ${chalk.dim(`+${services.length - 2}`)}`;
|
|
305
|
+
const pricing = resolveBestPaidPricing(peer);
|
|
306
|
+
const ghostCount = peer.onChainGhostCount;
|
|
307
|
+
const ghostCell = typeof ghostCount === 'number'
|
|
308
|
+
? (ghostCount === 0 ? chalk.dim('0') : chalk.red(String(ghostCount)))
|
|
309
|
+
: chalk.dim('—');
|
|
310
|
+
const sessionsCell = typeof peer.onChainChannelCount === 'number'
|
|
311
|
+
? (peer.onChainChannelCount > 0 ? chalk.cyan(String(peer.onChainChannelCount)) : chalk.dim('0'))
|
|
312
|
+
: chalk.dim('—');
|
|
67
313
|
const load = peer.currentLoad !== undefined && peer.maxConcurrency !== undefined
|
|
68
314
|
? `${peer.currentLoad}/${peer.maxConcurrency}`
|
|
69
|
-
: chalk.dim('
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
315
|
+
: chalk.dim('—');
|
|
316
|
+
const badge = isPeerVouched(peer) ? chalk.green(' ✓') : '';
|
|
317
|
+
const peerCell = peer.peerId + badge;
|
|
318
|
+
const freeNames = freeNamesByPeer.get(peer.peerId) ?? [];
|
|
319
|
+
const freeCell = freeNames.length === 0
|
|
320
|
+
? chalk.dim('—')
|
|
321
|
+
: freeNames.length <= 2
|
|
322
|
+
? chalk.green(freeNames.join(', '))
|
|
323
|
+
: chalk.green(`${freeNames.slice(0, 2).join(', ')}`) + chalk.dim(` +${freeNames.length - 2}`);
|
|
324
|
+
const row = [
|
|
325
|
+
peerCell,
|
|
326
|
+
peer.displayName ?? chalk.dim('—'),
|
|
327
|
+
peer.providers.join(', ') || chalk.dim('—'),
|
|
328
|
+
servicesCell,
|
|
329
|
+
formatUsdPerMillion(pricing.input),
|
|
330
|
+
formatUsdPerMillion(pricing.output),
|
|
331
|
+
];
|
|
332
|
+
if (anyFreeService)
|
|
333
|
+
row.push(freeCell);
|
|
334
|
+
row.push(sessionsCell, ghostCell, formatUsdcVolume(peer.onChainTotalVolumeUsdcMicros ?? null), formatAge(peer.onChainLastSettledAtSec ?? null), load);
|
|
335
|
+
table.push(row);
|
|
83
336
|
}
|
|
84
337
|
console.log('');
|
|
85
338
|
console.log(table.toString());
|
|
339
|
+
if (!hasChainData) {
|
|
340
|
+
console.log(chalk.dim(' Sessions / Ghosts / Volume / Last settled are dim — configure chain RPC to enable on-chain verification.'));
|
|
341
|
+
}
|
|
342
|
+
if (anyFreeService) {
|
|
343
|
+
console.log(chalk.dim(' Free column lists services a peer offers at $0 in/out. "Min In/Out $/1M" always reflects the cheapest PAID option.'));
|
|
344
|
+
}
|
|
86
345
|
console.log('');
|
|
87
346
|
}
|
|
88
347
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
348
|
+
* Render the expanded "one row per (peer, provider, service)" table. Adds a
|
|
349
|
+
* `Tags` column when at least one displayed (peer, service) pair has tags —
|
|
350
|
+
* handy both for exploring categories and for verifying a `--tag` filter
|
|
351
|
+
* actually matched the services you expected.
|
|
352
|
+
*
|
|
353
|
+
* When `requestedTags` is non-empty:
|
|
354
|
+
* - Services that don't match any requested tag are skipped.
|
|
355
|
+
* - Tagless fallback rows (peers with no `providerPricing` entries; or a
|
|
356
|
+
* provider with no services, rendered as a synthetic `(default)` row)
|
|
357
|
+
* are skipped entirely — they can't match an opt-in tag constraint,
|
|
358
|
+
* and surfacing them would contradict the `peer` detail command and
|
|
359
|
+
* mislead callers auditing a filtered view.
|
|
360
|
+
* - Matching tags inside the `Tags` cell are highlighted green.
|
|
361
|
+
*/
|
|
362
|
+
function renderExpandedTable(peers, requestedTags) {
|
|
363
|
+
const rows = [];
|
|
364
|
+
const hasTagFilter = requestedTags.size > 0;
|
|
365
|
+
for (const peer of peers) {
|
|
366
|
+
const pricing = peer.providerPricing;
|
|
367
|
+
if (!pricing || Object.keys(pricing).length === 0) {
|
|
368
|
+
// Peer with no pricing info — render one row per provider as a fallback
|
|
369
|
+
// shape so the table is never empty for a known peer. These rows have
|
|
370
|
+
// no tags and can never match `--tag`, so suppress them under a filter.
|
|
371
|
+
if (hasTagFilter)
|
|
372
|
+
continue;
|
|
373
|
+
for (const provider of peer.providers) {
|
|
374
|
+
rows.push({
|
|
375
|
+
peerId: peer.peerId,
|
|
376
|
+
provider,
|
|
377
|
+
service: '—',
|
|
378
|
+
input: formatUsdPerMillion(peer.defaultInputUsdPerMillion ?? null),
|
|
379
|
+
output: formatUsdPerMillion(peer.defaultOutputUsdPerMillion ?? null),
|
|
380
|
+
sessions: typeof peer.onChainChannelCount === 'number' ? String(peer.onChainChannelCount) : '—',
|
|
381
|
+
volume: formatUsdcVolume(peer.onChainTotalVolumeUsdcMicros ?? null),
|
|
382
|
+
tags: [],
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
for (const [providerName, providerEntry] of Object.entries(pricing)) {
|
|
388
|
+
const services = providerEntry.services ?? {};
|
|
389
|
+
const serviceEntries = Object.entries(services);
|
|
390
|
+
if (serviceEntries.length === 0) {
|
|
391
|
+
// Synthetic `(default)` row — defaults aren't tagged so they can't
|
|
392
|
+
// match a tag filter; keep the row only when no filter is active.
|
|
393
|
+
if (hasTagFilter)
|
|
394
|
+
continue;
|
|
395
|
+
rows.push({
|
|
396
|
+
peerId: peer.peerId,
|
|
397
|
+
provider: providerName,
|
|
398
|
+
service: '(default)',
|
|
399
|
+
input: formatUsdPerMillion(providerEntry.defaults?.inputUsdPerMillion ?? null),
|
|
400
|
+
output: formatUsdPerMillion(providerEntry.defaults?.outputUsdPerMillion ?? null),
|
|
401
|
+
sessions: typeof peer.onChainChannelCount === 'number' ? String(peer.onChainChannelCount) : '—',
|
|
402
|
+
volume: formatUsdcVolume(peer.onChainTotalVolumeUsdcMicros ?? null),
|
|
403
|
+
tags: [],
|
|
404
|
+
});
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
for (const [serviceName, servicePricing] of serviceEntries.sort(([a], [b]) => a.localeCompare(b))) {
|
|
408
|
+
if (hasTagFilter && !serviceMatchesTagFilter(peer, providerName, serviceName, requestedTags)) {
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
rows.push({
|
|
412
|
+
peerId: peer.peerId,
|
|
413
|
+
provider: providerName,
|
|
414
|
+
service: serviceName,
|
|
415
|
+
input: formatUsdPerMillion(servicePricing.inputUsdPerMillion),
|
|
416
|
+
output: formatUsdPerMillion(servicePricing.outputUsdPerMillion),
|
|
417
|
+
sessions: typeof peer.onChainChannelCount === 'number' ? String(peer.onChainChannelCount) : '—',
|
|
418
|
+
volume: formatUsdcVolume(peer.onChainTotalVolumeUsdcMicros ?? null),
|
|
419
|
+
tags: collectServiceTags(peer, providerName, serviceName),
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const anyTags = rows.some((r) => r.tags.length > 0);
|
|
425
|
+
const head = [
|
|
426
|
+
chalk.bold('Peer'),
|
|
427
|
+
chalk.bold('Provider'),
|
|
428
|
+
chalk.bold('Service'),
|
|
429
|
+
chalk.bold('In $/1M'),
|
|
430
|
+
chalk.bold('Out $/1M'),
|
|
431
|
+
chalk.bold('Sessions'),
|
|
432
|
+
chalk.bold('Volume'),
|
|
433
|
+
];
|
|
434
|
+
if (anyTags)
|
|
435
|
+
head.push(chalk.bold('Tags'));
|
|
436
|
+
const table = new Table({ head, wordWrap: true });
|
|
437
|
+
for (const r of rows) {
|
|
438
|
+
const row = [
|
|
439
|
+
r.peerId,
|
|
440
|
+
r.provider,
|
|
441
|
+
r.service === '—' || r.service === '(default)' ? chalk.dim(r.service) : r.service,
|
|
442
|
+
r.input,
|
|
443
|
+
r.output,
|
|
444
|
+
r.sessions === '—' ? chalk.dim('—') : r.sessions,
|
|
445
|
+
r.volume,
|
|
446
|
+
];
|
|
447
|
+
if (anyTags) {
|
|
448
|
+
row.push(r.tags.length === 0
|
|
449
|
+
? chalk.dim('—')
|
|
450
|
+
: r.tags
|
|
451
|
+
.map((t) => (requestedTags.has(t) ? chalk.green(t) : t))
|
|
452
|
+
.join(', '));
|
|
453
|
+
}
|
|
454
|
+
table.push(row);
|
|
455
|
+
}
|
|
456
|
+
console.log('');
|
|
457
|
+
console.log(table.toString());
|
|
458
|
+
if (anyTags && requestedTags.size > 0) {
|
|
459
|
+
console.log(chalk.dim(` Matching tags highlighted green: ${Array.from(requestedTags).sort().join(', ')}`));
|
|
460
|
+
}
|
|
461
|
+
console.log('');
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Register the `antseed network browse` command.
|
|
91
465
|
*/
|
|
92
466
|
export function registerNetworkBrowseCommand(networkCmd) {
|
|
93
467
|
networkCmd
|
|
94
468
|
.command('browse')
|
|
95
|
-
.description('Browse
|
|
96
|
-
.option('-s, --service <service>', 'filter by service name')
|
|
469
|
+
.description('Browse peers on the network — prices, services, and on-chain settlements')
|
|
470
|
+
.option('-s, --service <service>', 'filter by service or provider name')
|
|
471
|
+
.option('-t, --tag <tags>', 'filter by service category tag(s); comma-separated for OR match '
|
|
472
|
+
+ '(e.g. --tag tee,privacy). Well-known tags: privacy, legal, uncensored, coding, finance, tee')
|
|
473
|
+
.option('--services', 'expand to one row per (peer, provider, service) with per-service pricing', false)
|
|
474
|
+
.option('--sort <key>', 'sort by volume | sessions | price | recent (default: volume)')
|
|
475
|
+
.option('--top <n>', 'show only the top N peers (default: 20)')
|
|
97
476
|
.option('--json', 'output as JSON', false)
|
|
98
|
-
.action(async (
|
|
477
|
+
.action(async (rawOptions) => {
|
|
99
478
|
const globalOpts = getGlobalOptions(networkCmd);
|
|
100
479
|
const config = await loadConfig(globalOpts.config);
|
|
101
|
-
const serviceFilter =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
const
|
|
108
|
-
?
|
|
109
|
-
:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
480
|
+
const serviceFilter = rawOptions.service?.trim();
|
|
481
|
+
const tagFilter = parseTagFilter(rawOptions.tag);
|
|
482
|
+
const sortKey = parseSortKey(rawOptions.sort);
|
|
483
|
+
const topLimit = parseTopLimit(rawOptions.top);
|
|
484
|
+
let snapshot = await loadSnapshotFromBuyerState(globalOpts.dataDir);
|
|
485
|
+
if (!snapshot) {
|
|
486
|
+
const bootstrapNodes = config.network.bootstrapNodes.length > 0
|
|
487
|
+
? toBootstrapConfig(parseBootstrapList(config.network.bootstrapNodes))
|
|
488
|
+
: undefined;
|
|
489
|
+
const spinner = ora('Discovering peers on the network...').start();
|
|
490
|
+
const paymentsConfig = buildPaymentsConfig(config.payments?.crypto);
|
|
491
|
+
const node = new AntseedNode({
|
|
492
|
+
role: 'buyer',
|
|
493
|
+
...(bootstrapNodes ? { bootstrapNodes } : {}),
|
|
494
|
+
dhtOperationTimeoutMs: 30_000,
|
|
495
|
+
...(paymentsConfig ? { payments: paymentsConfig } : {}),
|
|
496
|
+
});
|
|
497
|
+
try {
|
|
498
|
+
await node.start();
|
|
118
499
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (peers.length === 0) {
|
|
140
|
-
console.log(chalk.dim('No peers found. Try again later or check your bootstrap nodes.'));
|
|
141
|
-
await node.stop();
|
|
142
|
-
return;
|
|
500
|
+
catch (err) {
|
|
501
|
+
spinner.fail(chalk.red(`Failed to connect to network: ${err.message}`));
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
const peers = await node.discoverPeers(serviceFilter);
|
|
506
|
+
spinner.succeed(chalk.green(`Found ${peers.length} peer(s)`));
|
|
507
|
+
// Only stamp `onChainStatsRefreshedAt` when the enrichment loop
|
|
508
|
+
// could actually have run — otherwise we'd display a misleading
|
|
509
|
+
// "On-chain stats as of just now" header on top of a table that
|
|
510
|
+
// has "—" in every on-chain column. Derive it from the per-peer
|
|
511
|
+
// timestamps so we reflect only real RPC reads.
|
|
512
|
+
const latestChainStamp = peers
|
|
513
|
+
.map((p) => p.onChainStatsFetchedAt ?? 0)
|
|
514
|
+
.reduce((max, v) => (v > max ? v : max), 0);
|
|
515
|
+
snapshot = {
|
|
516
|
+
peers,
|
|
517
|
+
onChainStatsRefreshedAt: latestChainStamp > 0 ? latestChainStamp : null,
|
|
518
|
+
sourceLabel: 'live DHT discovery',
|
|
519
|
+
};
|
|
143
520
|
}
|
|
144
|
-
|
|
145
|
-
|
|
521
|
+
catch (err) {
|
|
522
|
+
spinner.fail(chalk.red(`Discovery failed: ${err.message}`));
|
|
146
523
|
await node.stop();
|
|
147
524
|
return;
|
|
148
525
|
}
|
|
149
|
-
|
|
526
|
+
await node.stop();
|
|
527
|
+
}
|
|
528
|
+
let peers = snapshot.peers;
|
|
529
|
+
if (serviceFilter) {
|
|
530
|
+
peers = peers.filter((peer) => peerMatchesServiceFilter(peer, serviceFilter));
|
|
531
|
+
}
|
|
532
|
+
if (tagFilter.size > 0) {
|
|
533
|
+
peers = peers.filter((peer) => peerMatchesTagFilter(peer, tagFilter));
|
|
534
|
+
}
|
|
535
|
+
if (peers.length === 0) {
|
|
536
|
+
const filters = [
|
|
537
|
+
serviceFilter ? '--service' : null,
|
|
538
|
+
tagFilter.size > 0 ? '--tag' : null,
|
|
539
|
+
].filter((x) => x !== null);
|
|
540
|
+
const hint = filters.length > 0
|
|
541
|
+
? `Try adjusting ${filters.join(' / ')} or widening your search.`
|
|
542
|
+
: 'Try again later once more peers announce.';
|
|
543
|
+
console.log(chalk.dim(`No peers found. ${hint}`));
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
peers = sortPeers(peers, sortKey);
|
|
547
|
+
const truncated = peers.length > topLimit;
|
|
548
|
+
const displayed = truncated ? peers.slice(0, topLimit) : peers;
|
|
549
|
+
if (rawOptions.json) {
|
|
550
|
+
console.log(JSON.stringify({
|
|
551
|
+
source: snapshot.sourceLabel,
|
|
552
|
+
onChainStatsRefreshedAt: snapshot.onChainStatsRefreshedAt,
|
|
553
|
+
sort: sortKey,
|
|
554
|
+
filters: {
|
|
555
|
+
service: serviceFilter ?? null,
|
|
556
|
+
tags: tagFilter.size > 0 ? Array.from(tagFilter).sort() : null,
|
|
557
|
+
},
|
|
558
|
+
total: peers.length,
|
|
559
|
+
peers: displayed,
|
|
560
|
+
}, null, 2));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const filterBits = [];
|
|
564
|
+
if (serviceFilter)
|
|
565
|
+
filterBits.push(`service="${serviceFilter}"`);
|
|
566
|
+
if (tagFilter.size > 0)
|
|
567
|
+
filterBits.push(`tags=[${Array.from(tagFilter).sort().join(',')}]`);
|
|
568
|
+
const filterSuffix = filterBits.length > 0 ? ` • filter: ${filterBits.join(' ')}` : '';
|
|
569
|
+
console.log(chalk.dim(`Source: ${snapshot.sourceLabel} • ${peers.length} peer(s)${truncated ? ` (showing top ${topLimit})` : ''} • sort: ${sortKey}${filterSuffix}`));
|
|
570
|
+
if (snapshot.onChainStatsRefreshedAt) {
|
|
571
|
+
const ageMs = Date.now() - snapshot.onChainStatsRefreshedAt;
|
|
572
|
+
console.log(chalk.dim(`On-chain stats as of ${formatHumanAgeMs(ageMs)}`));
|
|
573
|
+
}
|
|
574
|
+
const hasChainData = displayed.some((peer) => typeof peer.onChainChannelCount === 'number');
|
|
575
|
+
if (rawOptions.services) {
|
|
576
|
+
renderExpandedTable(displayed, tagFilter);
|
|
577
|
+
}
|
|
578
|
+
else {
|
|
579
|
+
renderCompactTable(displayed, hasChainData);
|
|
150
580
|
}
|
|
151
|
-
|
|
152
|
-
|
|
581
|
+
const topPeer = displayed[0];
|
|
582
|
+
if (topPeer) {
|
|
583
|
+
console.log(chalk.bold('Pin a peer:'));
|
|
584
|
+
console.log(` antseed buyer connection set --peer ${topPeer.peerId}`);
|
|
585
|
+
console.log(chalk.dim(` or per-request: curl -H "x-antseed-pin-peer: ${topPeer.peerId}" ...`));
|
|
586
|
+
console.log(chalk.dim(` full details: antseed network peer ${topPeer.peerId}`));
|
|
587
|
+
console.log('');
|
|
153
588
|
}
|
|
154
|
-
await node.stop();
|
|
155
589
|
});
|
|
156
590
|
}
|
|
157
591
|
//# sourceMappingURL=browse.js.map
|