blue-js-sdk 2.3.0 → 2.6.0
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 +3 -3
- package/batch.js +2 -2
- package/chain/authz.js +1 -9
- package/chain/fee-grants.js +199 -3
- package/chain/index.js +36 -167
- package/chain/queries.js +118 -7
- package/chain/rpc.js +58 -2
- package/client/index.js +1 -3
- package/client.js +17 -1
- package/connection/connect.js +17 -5
- package/connection/disconnect.js +86 -10
- package/connection/discovery.js +11 -11
- package/connection/index.js +2 -0
- package/cosmjs-setup.js +30 -153
- package/defaults.js +1 -1
- package/index.js +5 -1
- package/node-connect.js +118 -25
- package/operator.js +2 -0
- package/package.json +2 -5
- package/pricing/index.js +3 -26
- package/types/index.d.ts +2 -2
- package/ai-path/ADMIN-ELEVATION.md +0 -116
- package/ai-path/AI-MANIFESTO.md +0 -185
- package/ai-path/BREAKING.md +0 -74
- package/ai-path/CHECKLIST.md +0 -619
- package/ai-path/CONNECTION-STEPS.md +0 -724
- package/ai-path/DECISION-TREE.md +0 -422
- package/ai-path/DEPENDENCIES.md +0 -459
- package/ai-path/E2E-FLOW.md +0 -1707
- package/ai-path/FAILURES.md +0 -410
- package/ai-path/GUIDE.md +0 -1315
- package/ai-path/README.md +0 -599
- package/ai-path/SPLIT-TUNNEL.md +0 -266
- package/ai-path/cli.js +0 -548
- package/ai-path/connect.js +0 -1028
- package/ai-path/discover.js +0 -178
- package/ai-path/environment.js +0 -266
- package/ai-path/errors.js +0 -86
- package/ai-path/examples/autonomous-agent.mjs +0 -220
- package/ai-path/examples/multi-region.mjs +0 -174
- package/ai-path/examples/one-shot.mjs +0 -31
- package/ai-path/index.js +0 -79
- package/ai-path/pricing.js +0 -137
- package/ai-path/recommend.js +0 -413
- package/ai-path/run-admin.vbs +0 -25
- package/ai-path/setup.js +0 -291
- package/ai-path/wallet.js +0 -137
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
JavaScript/TypeScript SDK for the [Sentinel](https://sentinel.co) decentralized VPN network. WireGuard + V2Ray tunnels, Cosmos blockchain, 900+ nodes. RPC queries, typed events, CosmJS compatible.
|
|
4
4
|
|
|
5
|
-
**Also available:** [Blue C# SDK](https://github.com/Sentinel-Autonomybuilder/blue-csharp-sdk) | [Blue
|
|
5
|
+
**Also available:** [Blue C# SDK](https://github.com/Sentinel-Autonomybuilder/blue-csharp-sdk) | [Blue Agent Connect](https://github.com/Sentinel-Autonomybuilder/blue-agent-connect) (zero-config wrapper for AI agents)
|
|
6
6
|
|
|
7
7
|
## Platform Support
|
|
8
8
|
|
|
@@ -18,7 +18,7 @@ JavaScript/TypeScript SDK for the [Sentinel](https://sentinel.co) decentralized
|
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
-
> **For AI agents:** If you just want `connect()` with one function call, use [`blue-
|
|
21
|
+
> **For AI agents:** If you just want `connect()` with one function call, use [`blue-agent-connect`](https://www.npmjs.com/package/blue-agent-connect) instead.
|
|
22
22
|
|
|
23
23
|
## Install
|
|
24
24
|
|
|
@@ -46,7 +46,7 @@ await disconnect();
|
|
|
46
46
|
|
|
47
47
|
## For AI Agents
|
|
48
48
|
|
|
49
|
-
Use [
|
|
49
|
+
Use [blue-agent-connect](https://www.npmjs.com/package/blue-agent-connect) — a zero-config wrapper with one function call from zero to encrypted tunnel.
|
|
50
50
|
|
|
51
51
|
## Features
|
|
52
52
|
|
package/batch.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { ChainError, ErrorCodes } from './errors.js';
|
|
28
|
-
import { sleep } from './defaults.js';
|
|
28
|
+
import { sleep, DEFAULT_LCD } from './defaults.js';
|
|
29
29
|
import {
|
|
30
30
|
broadcast,
|
|
31
31
|
extractAllSessionIds,
|
|
@@ -246,7 +246,7 @@ async function _processBatch(client, account, batch, gigabytes, denom, ctx) {
|
|
|
246
246
|
export async function waitForBatchSessions(nodeAddrs, walletAddr, lcdUrl, options = {}) {
|
|
247
247
|
const maxWaitMs = options.maxWaitMs ?? DEFAULT_POLL_TIMEOUT;
|
|
248
248
|
const pollIntervalMs = options.pollIntervalMs ?? 2000;
|
|
249
|
-
const baseLcd = lcdUrl ||
|
|
249
|
+
const baseLcd = lcdUrl || DEFAULT_LCD;
|
|
250
250
|
|
|
251
251
|
if (nodeAddrs.length === 0) return { confirmed: [], pending: [] };
|
|
252
252
|
|
package/chain/authz.js
CHANGED
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
|
|
12
12
|
import { protoString, protoEmbedded, protoInt64 } from '../v3protocol.js';
|
|
13
13
|
import { ChainError, ErrorCodes } from '../errors.js';
|
|
14
|
-
import { lcd, lcdPaginatedSafe } from './lcd.js';
|
|
15
14
|
import { buildRegistry } from './client.js';
|
|
16
15
|
|
|
17
16
|
// ─── Protobuf Helpers ───────────────────────────────────────────────────────
|
|
@@ -99,11 +98,4 @@ export function encodeForExec(msgs) {
|
|
|
99
98
|
});
|
|
100
99
|
}
|
|
101
100
|
|
|
102
|
-
|
|
103
|
-
* Query authz grants between granter and grantee.
|
|
104
|
-
* @returns {Promise<Array>} Array of grant objects
|
|
105
|
-
*/
|
|
106
|
-
export async function queryAuthzGrants(lcdUrl, granter, grantee) {
|
|
107
|
-
const { items } = await lcdPaginatedSafe(lcdUrl, `/cosmos/authz/v1beta1/grants?granter=${granter}&grantee=${grantee}`, 'grants');
|
|
108
|
-
return items;
|
|
109
|
-
}
|
|
101
|
+
// queryAuthzGrants removed — use RPC-first version from chain/queries.js
|
package/chain/fee-grants.js
CHANGED
|
@@ -13,7 +13,7 @@ import { EventEmitter } from 'events';
|
|
|
13
13
|
import { protoString, protoInt64, protoEmbedded } from '../v3protocol.js';
|
|
14
14
|
import { LCD_ENDPOINTS } from '../defaults.js';
|
|
15
15
|
import { ValidationError, ErrorCodes } from '../errors.js';
|
|
16
|
-
import {
|
|
16
|
+
import { lcdQuery, lcdPaginatedSafe } from './lcd.js';
|
|
17
17
|
import { isSameKey } from './wallet.js';
|
|
18
18
|
import { queryPlanSubscribers } from './queries.js';
|
|
19
19
|
import {
|
|
@@ -178,9 +178,9 @@ export async function queryFeeGrant(lcdUrl, granter, grantee) {
|
|
|
178
178
|
}
|
|
179
179
|
} catch { /* fall through to LCD */ }
|
|
180
180
|
|
|
181
|
-
// LCD fallback
|
|
181
|
+
// LCD fallback (with endpoint failover via lcdQuery)
|
|
182
182
|
try {
|
|
183
|
-
const data = await
|
|
183
|
+
const data = await lcdQuery(`/cosmos/feegrant/v1beta1/allowance/${granter}/${grantee}`, { lcdUrl });
|
|
184
184
|
return data.allowance || null;
|
|
185
185
|
} catch { return null; } // 404 = no grant
|
|
186
186
|
}
|
|
@@ -354,3 +354,199 @@ export function monitorFeeGrants(opts = {}) {
|
|
|
354
354
|
|
|
355
355
|
return emitter;
|
|
356
356
|
}
|
|
357
|
+
|
|
358
|
+
// ─── Streaming Batch Grant (for SSE / progress UIs) ──────────────────────────
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Stream progress as we grant fee allowances to all plan subscribers in batches.
|
|
362
|
+
*
|
|
363
|
+
* Async generator. Yields events with `{ type, ...payload }`:
|
|
364
|
+
* - status { msg } — human-readable status line
|
|
365
|
+
* - batch_start { batch, total, count, addresses } — about to broadcast a batch
|
|
366
|
+
* - batch_ok { batch, total, granted, totalGranted, txHash, elapsed }
|
|
367
|
+
* - batch_error { batch, total, error, elapsed }
|
|
368
|
+
* - done { granted, skipped, total, errors? }
|
|
369
|
+
* - error { msg }
|
|
370
|
+
*
|
|
371
|
+
* The caller passes a `broadcast(msgs, memo)` function — any safe-broadcaster
|
|
372
|
+
* with the Plan Manager's mutex + sequence-retry semantics works. Consumer
|
|
373
|
+
* routes layer SSE (`res.write('data: ...\n\n')`) on top of these events.
|
|
374
|
+
*
|
|
375
|
+
* @param {number|string} planId
|
|
376
|
+
* @param {object} opts
|
|
377
|
+
* @param {string} opts.granterAddress - Plan owner paying fees
|
|
378
|
+
* @param {string} opts.lcdUrl - LCD endpoint
|
|
379
|
+
* @param {(msgs: Array, memo: string) => Promise<{code:number, rawLog?:string, transactionHash?:string}>} opts.broadcast
|
|
380
|
+
* @param {object} [opts.grantOpts] - { spendLimit, expiration } for BasicAllowance
|
|
381
|
+
* @param {number} [opts.batchSize=5] - Msgs per TX
|
|
382
|
+
* @param {() => boolean} [opts.isCancelled] - Return true to abort between batches
|
|
383
|
+
* @yields {{type: string, [key: string]: any}}
|
|
384
|
+
*/
|
|
385
|
+
export async function* streamGrantPlanSubscribers(planId, opts = {}) {
|
|
386
|
+
const {
|
|
387
|
+
granterAddress,
|
|
388
|
+
lcdUrl,
|
|
389
|
+
broadcast,
|
|
390
|
+
grantOpts = {},
|
|
391
|
+
batchSize = 5,
|
|
392
|
+
isCancelled = () => false,
|
|
393
|
+
} = opts;
|
|
394
|
+
|
|
395
|
+
if (!granterAddress) throw new ValidationError(ErrorCodes.INVALID_OPTIONS, 'granterAddress is required');
|
|
396
|
+
if (typeof broadcast !== 'function') throw new ValidationError(ErrorCodes.INVALID_OPTIONS, 'broadcast function is required');
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
yield { type: 'status', msg: 'Fetching plan subscribers...' };
|
|
400
|
+
const { subscribers } = await queryPlanSubscribers(planId, { lcdUrl });
|
|
401
|
+
|
|
402
|
+
const now = new Date();
|
|
403
|
+
const activeSubs = subscribers.filter(s => {
|
|
404
|
+
if (s.status && s.status !== 'active') return false;
|
|
405
|
+
if (s.inactive_at && new Date(s.inactive_at) <= now) return false;
|
|
406
|
+
return true;
|
|
407
|
+
});
|
|
408
|
+
const uniqueAddrs = [...new Set(activeSubs.map(s => s.acc_address || s.address))]
|
|
409
|
+
.filter(a => a && a !== granterAddress && !isSameKey(a, granterAddress));
|
|
410
|
+
|
|
411
|
+
yield { type: 'status', msg: `Found ${activeSubs.length} active subscribers (${uniqueAddrs.length} unique, excl. self)` };
|
|
412
|
+
|
|
413
|
+
if (uniqueAddrs.length === 0) {
|
|
414
|
+
yield { type: 'done', granted: 0, skipped: 0, total: 0, msg: 'No active subscribers (excluding self)' };
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
yield { type: 'status', msg: 'Checking existing grants...' };
|
|
419
|
+
const existing = await queryFeeGrantsIssued(lcdUrl || LCD_ENDPOINTS[0].url, granterAddress);
|
|
420
|
+
const existingGrantees = new Set(existing.map(g => g.grantee));
|
|
421
|
+
const needGrant = uniqueAddrs.filter(a => !existingGrantees.has(a));
|
|
422
|
+
const skipped = uniqueAddrs.length - needGrant.length;
|
|
423
|
+
|
|
424
|
+
yield { type: 'status', msg: `${existingGrantees.size} existing grants found. ${needGrant.length} need granting, ${skipped} already covered.` };
|
|
425
|
+
|
|
426
|
+
if (needGrant.length === 0) {
|
|
427
|
+
yield { type: 'done', granted: 0, skipped, total: 0, msg: 'All subscribers already have grants' };
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const totalBatches = Math.ceil(needGrant.length / batchSize);
|
|
432
|
+
let granted = 0;
|
|
433
|
+
const errors = [];
|
|
434
|
+
|
|
435
|
+
for (let i = 0; i < needGrant.length; i += batchSize) {
|
|
436
|
+
if (isCancelled()) break;
|
|
437
|
+
|
|
438
|
+
const batchNum = Math.floor(i / batchSize) + 1;
|
|
439
|
+
const batch = needGrant.slice(i, i + batchSize);
|
|
440
|
+
const shortAddrs = batch.map(a => a.slice(0, 12) + '...' + a.slice(-6)).join(', ');
|
|
441
|
+
|
|
442
|
+
yield { type: 'batch_start', batch: batchNum, total: totalBatches, count: batch.length, addresses: shortAddrs };
|
|
443
|
+
|
|
444
|
+
const msgs = batch.map(grantee => buildFeeGrantMsg(granterAddress, grantee, grantOpts));
|
|
445
|
+
|
|
446
|
+
const t0 = Date.now();
|
|
447
|
+
try {
|
|
448
|
+
const result = await broadcast(msgs, `Fee grant batch ${batchNum}/${totalBatches}`);
|
|
449
|
+
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
|
450
|
+
|
|
451
|
+
if (result.code !== 0) {
|
|
452
|
+
const errMsg = result.rawLog || `TX failed code=${result.code}`;
|
|
453
|
+
yield { type: 'batch_error', batch: batchNum, total: totalBatches, error: errMsg, elapsed };
|
|
454
|
+
errors.push(`Batch ${batchNum}: ${errMsg}`);
|
|
455
|
+
} else {
|
|
456
|
+
granted += batch.length;
|
|
457
|
+
yield {
|
|
458
|
+
type: 'batch_ok',
|
|
459
|
+
batch: batchNum,
|
|
460
|
+
total: totalBatches,
|
|
461
|
+
granted: batch.length,
|
|
462
|
+
totalGranted: granted,
|
|
463
|
+
txHash: result.transactionHash,
|
|
464
|
+
elapsed,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
} catch (e) {
|
|
468
|
+
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
|
469
|
+
yield { type: 'batch_error', batch: batchNum, total: totalBatches, error: e.message, elapsed };
|
|
470
|
+
errors.push(`Batch ${batchNum}: ${e.message}`);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
yield {
|
|
475
|
+
type: 'done',
|
|
476
|
+
granted,
|
|
477
|
+
skipped,
|
|
478
|
+
total: needGrant.length,
|
|
479
|
+
errors: errors.length ? errors : undefined,
|
|
480
|
+
};
|
|
481
|
+
} catch (e) {
|
|
482
|
+
yield { type: 'error', msg: e.message };
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ─── Gas Cost Analytics ──────────────────────────────────────────────────────
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Compute how many udvpn the granter has spent on fee-granted transactions
|
|
490
|
+
* for a plan's subscribers. Iterates each subscriber, pulls their outgoing
|
|
491
|
+
* TXs via LCD, and sums fees where `fee.granter === granterAddress`.
|
|
492
|
+
*
|
|
493
|
+
* @param {number|string} planId
|
|
494
|
+
* @param {object} opts
|
|
495
|
+
* @param {string} opts.granterAddress - Address that paid the fees (plan owner)
|
|
496
|
+
* @param {string} opts.lcdUrl - LCD endpoint
|
|
497
|
+
* @param {number} [opts.txLimit=100] - Max TXs to inspect per subscriber
|
|
498
|
+
* @param {(info: {processed:number, total:number, address:string}) => void} [opts.onProgress]
|
|
499
|
+
* @returns {Promise<{ totalUdvpn: number, txCount: number, byAddress: Record<string, {udvpn:number, txCount:number}>, subscriberCount: number }>}
|
|
500
|
+
*/
|
|
501
|
+
export async function computeFeeGrantGasCosts(planId, opts = {}) {
|
|
502
|
+
const { granterAddress, lcdUrl, txLimit = 100, onProgress } = opts;
|
|
503
|
+
if (!granterAddress) throw new ValidationError(ErrorCodes.INVALID_OPTIONS, 'granterAddress is required');
|
|
504
|
+
|
|
505
|
+
const { subscribers } = await queryPlanSubscribers(planId, { lcdUrl });
|
|
506
|
+
const subscriberAddrs = [...new Set(subscribers.map(s => s.acc_address || s.address))]
|
|
507
|
+
.filter(a => a && a !== granterAddress);
|
|
508
|
+
|
|
509
|
+
if (subscriberAddrs.length === 0) {
|
|
510
|
+
return { totalUdvpn: 0, txCount: 0, byAddress: {}, subscriberCount: 0 };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
let totalUdvpn = 0;
|
|
514
|
+
let txCount = 0;
|
|
515
|
+
const byAddress = {};
|
|
516
|
+
|
|
517
|
+
const base = lcdUrl || LCD_ENDPOINTS[0].url;
|
|
518
|
+
for (let idx = 0; idx < subscriberAddrs.length; idx++) {
|
|
519
|
+
const addr = subscriberAddrs[idx];
|
|
520
|
+
try {
|
|
521
|
+
const path =
|
|
522
|
+
`/cosmos/tx/v1beta1/txs?events=${encodeURIComponent("message.sender='" + addr + "'")}` +
|
|
523
|
+
`&pagination.limit=${txLimit}&order_by=2`;
|
|
524
|
+
const txData = await lcdQuery(path, { lcdUrl: base });
|
|
525
|
+
const rawTxs = txData.txs || [];
|
|
526
|
+
|
|
527
|
+
let addrGas = 0;
|
|
528
|
+
let addrTxCount = 0;
|
|
529
|
+
|
|
530
|
+
for (const tx of rawTxs) {
|
|
531
|
+
const fee = tx?.auth_info?.fee;
|
|
532
|
+
if (fee?.granter === granterAddress) {
|
|
533
|
+
const udvpnFee = (fee.amount || []).find(f => f.denom === 'udvpn');
|
|
534
|
+
if (udvpnFee) {
|
|
535
|
+
addrGas += parseInt(udvpnFee.amount, 10);
|
|
536
|
+
addrTxCount++;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (addrTxCount > 0) {
|
|
542
|
+
byAddress[addr] = { udvpn: addrGas, txCount: addrTxCount };
|
|
543
|
+
totalUdvpn += addrGas;
|
|
544
|
+
txCount += addrTxCount;
|
|
545
|
+
}
|
|
546
|
+
} catch { /* skip this subscriber on LCD failure */ }
|
|
547
|
+
|
|
548
|
+
if (onProgress) onProgress({ processed: idx + 1, total: subscriberAddrs.length, address: addr });
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return { totalUdvpn, txCount, byAddress, subscriberCount: subscriberAddrs.length };
|
|
552
|
+
}
|
package/chain/index.js
CHANGED
|
@@ -50,6 +50,19 @@ import { publicEndpointAgent } from '../security/index.js';
|
|
|
50
50
|
// Wallet — validation helpers (used by chain functions that validate addresses)
|
|
51
51
|
import { validateMnemonic, validateAddress } from '../wallet/index.js';
|
|
52
52
|
|
|
53
|
+
// RPC-first query modules — delegates LCD-only functions to these
|
|
54
|
+
import {
|
|
55
|
+
findExistingSession as _rpcFindExistingSession,
|
|
56
|
+
fetchActiveNodes as _rpcFetchActiveNodes,
|
|
57
|
+
getNetworkOverview as _rpcGetNetworkOverview,
|
|
58
|
+
getNodePrices as _rpcGetNodePrices,
|
|
59
|
+
discoverPlanIds as _rpcDiscoverPlanIds,
|
|
60
|
+
} from './queries.js';
|
|
61
|
+
import {
|
|
62
|
+
queryFeeGrants as _rpcQueryFeeGrants,
|
|
63
|
+
queryFeeGrant as _rpcQueryFeeGrant,
|
|
64
|
+
} from './fee-grants.js';
|
|
65
|
+
|
|
53
66
|
// ─── All Type URL Constants ──────────────────────────────────────────────────
|
|
54
67
|
|
|
55
68
|
export const MSG_TYPES = {
|
|
@@ -345,19 +358,16 @@ export function txResponse(result) {
|
|
|
345
358
|
/**
|
|
346
359
|
* Find an existing active session for a wallet+node pair.
|
|
347
360
|
* Returns session ID (BigInt) or null. Use this to avoid double-paying.
|
|
361
|
+
* Delegates to chain/queries.js RPC-first implementation.
|
|
348
362
|
*
|
|
349
|
-
*
|
|
363
|
+
* @param {string} lcdUrl - LCD endpoint URL
|
|
364
|
+
* @param {string} walletAddr - sent1... wallet address
|
|
365
|
+
* @param {string} nodeAddr - sentnode1... node address
|
|
366
|
+
* @param {object} [opts] - Optional. Pass `{ onStaleDuplicate: (BigInt) => void }` to
|
|
367
|
+
* receive fire-and-forget cancellation callbacks for stale duplicate sessions.
|
|
350
368
|
*/
|
|
351
|
-
export async function findExistingSession(lcdUrl, walletAddr, nodeAddr) {
|
|
352
|
-
|
|
353
|
-
for (const s of (data.sessions || [])) {
|
|
354
|
-
const bs = s.base_session || s; // session data is nested in base_session
|
|
355
|
-
if (bs.node_address !== nodeAddr) continue;
|
|
356
|
-
const maxBytes = parseInt(bs.max_bytes || '0');
|
|
357
|
-
const used = parseInt(bs.download_bytes || '0') + parseInt(bs.upload_bytes || '0');
|
|
358
|
-
if (maxBytes === 0 || used < maxBytes) return BigInt(bs.id);
|
|
359
|
-
}
|
|
360
|
-
return null;
|
|
369
|
+
export async function findExistingSession(lcdUrl, walletAddr, nodeAddr, opts) {
|
|
370
|
+
return _rpcFindExistingSession(lcdUrl, walletAddr, nodeAddr, opts);
|
|
361
371
|
}
|
|
362
372
|
|
|
363
373
|
/**
|
|
@@ -377,176 +387,44 @@ export function resolveNodeUrl(node) {
|
|
|
377
387
|
}
|
|
378
388
|
|
|
379
389
|
/**
|
|
380
|
-
* Fetch all active nodes
|
|
390
|
+
* Fetch all active nodes with RPC-first + LCD fallback.
|
|
381
391
|
* Returns array of node objects. Each node has `remote_url` resolved from `remote_addrs`.
|
|
392
|
+
* Delegates to chain/queries.js RPC-first implementation.
|
|
382
393
|
*/
|
|
383
394
|
export async function fetchActiveNodes(lcdUrl, limit = 500, maxPages = 20) {
|
|
384
|
-
|
|
385
|
-
let nextKey = null;
|
|
386
|
-
let page = 0;
|
|
387
|
-
do {
|
|
388
|
-
const keyParam = nextKey ? `&pagination.key=${encodeURIComponent(nextKey)}` : '';
|
|
389
|
-
const data = await lcd(lcdUrl, `/sentinel/node/v3/nodes?status=1&pagination.limit=${limit}${keyParam}`);
|
|
390
|
-
nodes.push(...(data.nodes || []));
|
|
391
|
-
nextKey = data.pagination?.next_key || null;
|
|
392
|
-
page++;
|
|
393
|
-
} while (nextKey && page < maxPages);
|
|
394
|
-
// Add computed remote_url for backward compatibility
|
|
395
|
-
for (const n of nodes) {
|
|
396
|
-
try { n.remote_url = resolveNodeUrl(n); } catch { /* skip nodes with no address */ }
|
|
397
|
-
}
|
|
398
|
-
return nodes;
|
|
395
|
+
return _rpcFetchActiveNodes(lcdUrl, limit, maxPages);
|
|
399
396
|
}
|
|
400
397
|
|
|
401
398
|
/**
|
|
402
399
|
* Get a quick network overview — total nodes, counts by country and service type, average prices.
|
|
403
|
-
*
|
|
400
|
+
* Delegates to chain/queries.js RPC-first implementation.
|
|
404
401
|
*
|
|
405
402
|
* @param {string} [lcdUrl] - LCD endpoint (default: cascading fallback)
|
|
406
403
|
* @returns {Promise<{ totalNodes: number, byCountry: Array<{country: string, count: number}>, byType: {wireguard: number, v2ray: number, unknown: number}, averagePrice: {gigabyteDvpn: number, hourlyDvpn: number}, nodes: Array }>}
|
|
407
|
-
*
|
|
408
|
-
* @example
|
|
409
|
-
* const overview = await getNetworkOverview();
|
|
410
|
-
* console.log(`${overview.totalNodes} nodes across ${overview.byCountry.length} countries`);
|
|
411
|
-
* console.log(`Average: ${overview.averagePrice.gigabyteDvpn.toFixed(3)} P2P/GB`);
|
|
412
404
|
*/
|
|
413
405
|
export async function getNetworkOverview(lcdUrl) {
|
|
414
|
-
|
|
415
|
-
let nodes;
|
|
416
|
-
if (lcdUrl) {
|
|
417
|
-
nodes = await fetchFn(lcdUrl);
|
|
418
|
-
} else {
|
|
419
|
-
const result = await tryWithFallback(LCD_ENDPOINTS, fetchFn, 'getNetworkOverview');
|
|
420
|
-
nodes = result.result;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Filter to nodes that accept udvpn
|
|
424
|
-
const active = nodes.filter(n => n.remote_url && (n.gigabyte_prices || []).some(p => p.denom === 'udvpn'));
|
|
425
|
-
|
|
426
|
-
// Count by country (from LCD metadata, limited — enrichNodes gives better data)
|
|
427
|
-
const countryMap = {};
|
|
428
|
-
for (const n of active) {
|
|
429
|
-
const c = n.location?.country || n.country || 'Unknown';
|
|
430
|
-
countryMap[c] = (countryMap[c] || 0) + 1;
|
|
431
|
-
}
|
|
432
|
-
const byCountry = Object.entries(countryMap)
|
|
433
|
-
.map(([country, count]) => ({ country, count }))
|
|
434
|
-
.sort((a, b) => b.count - a.count);
|
|
435
|
-
|
|
436
|
-
// Count by type (type not in LCD — estimate from service_type field if present)
|
|
437
|
-
const byType = { wireguard: 0, v2ray: 0, unknown: 0 };
|
|
438
|
-
for (const n of active) {
|
|
439
|
-
const t = n.service_type || n.type;
|
|
440
|
-
if (t === 'wireguard' || t === 1) byType.wireguard++;
|
|
441
|
-
else if (t === 'v2ray' || t === 2) byType.v2ray++;
|
|
442
|
-
else byType.unknown++;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// Average prices
|
|
446
|
-
let gbTotal = 0, gbCount = 0, hrTotal = 0, hrCount = 0;
|
|
447
|
-
for (const n of active) {
|
|
448
|
-
const gb = (n.gigabyte_prices || []).find(p => p.denom === 'udvpn');
|
|
449
|
-
if (gb?.quote_value) { gbTotal += parseInt(gb.quote_value, 10); gbCount++; }
|
|
450
|
-
const hr = (n.hourly_prices || []).find(p => p.denom === 'udvpn');
|
|
451
|
-
if (hr?.quote_value) { hrTotal += parseInt(hr.quote_value, 10); hrCount++; }
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
return {
|
|
455
|
-
totalNodes: active.length,
|
|
456
|
-
byCountry,
|
|
457
|
-
byType,
|
|
458
|
-
averagePrice: {
|
|
459
|
-
gigabyteDvpn: gbCount > 0 ? (gbTotal / gbCount) / 1_000_000 : 0,
|
|
460
|
-
hourlyDvpn: hrCount > 0 ? (hrTotal / hrCount) / 1_000_000 : 0,
|
|
461
|
-
},
|
|
462
|
-
nodes: active,
|
|
463
|
-
};
|
|
406
|
+
return _rpcGetNetworkOverview(lcdUrl);
|
|
464
407
|
}
|
|
465
408
|
|
|
466
409
|
/**
|
|
467
410
|
* Discover plan IDs by probing subscription endpoints.
|
|
468
|
-
*
|
|
411
|
+
* Delegates to chain/queries.js RPC-first implementation.
|
|
469
412
|
* Returns sorted array of plan IDs that have at least 1 subscription.
|
|
470
413
|
*/
|
|
471
414
|
export async function discoverPlanIds(lcdUrl, maxId = 100) {
|
|
472
|
-
|
|
473
|
-
const batchSize = 10;
|
|
474
|
-
for (let batch = 0; batch < maxId / batchSize; batch++) {
|
|
475
|
-
const checks = [];
|
|
476
|
-
for (let i = batch * batchSize + 1; i <= (batch + 1) * batchSize; i++) {
|
|
477
|
-
checks.push(
|
|
478
|
-
lcd(lcdUrl, `/sentinel/subscription/v3/plans/${i}/subscriptions?pagination.limit=1&pagination.count_total=true`)
|
|
479
|
-
.then(d => { if (parseInt(d.pagination?.total || '0') > 0) ids.push(i); })
|
|
480
|
-
.catch(() => {})
|
|
481
|
-
);
|
|
482
|
-
}
|
|
483
|
-
await Promise.all(checks);
|
|
484
|
-
}
|
|
485
|
-
return ids.sort((a, b) => a - b);
|
|
415
|
+
return _rpcDiscoverPlanIds(lcdUrl, maxId);
|
|
486
416
|
}
|
|
487
417
|
|
|
488
418
|
/**
|
|
489
|
-
* Get standardized prices for a node — abstracts V3
|
|
490
|
-
*
|
|
491
|
-
* Solves the common "NaN / GB" problem by defensively extracting quote_value,
|
|
492
|
-
* base_value, or amount from the nested LCD response structure.
|
|
419
|
+
* Get standardized prices for a node — abstracts V3 price parsing entirely.
|
|
420
|
+
* Delegates to chain/queries.js RPC-first implementation (direct node lookup, not full-list scan).
|
|
493
421
|
*
|
|
494
422
|
* @param {string} nodeAddress - sentnode1... address
|
|
495
423
|
* @param {string} [lcdUrl] - LCD endpoint URL (default: cascading fallback across all endpoints)
|
|
496
424
|
* @returns {Promise<{ gigabyte: { dvpn: number, udvpn: number, raw: object|null }, hourly: { dvpn: number, udvpn: number, raw: object|null }, denom: string, nodeAddress: string }>}
|
|
497
|
-
*
|
|
498
|
-
* @example
|
|
499
|
-
* const prices = await getNodePrices('sentnode1abc...');
|
|
500
|
-
* console.log(`${prices.gigabyte.dvpn} P2P/GB, ${prices.hourly.dvpn} P2P/hr`);
|
|
501
|
-
* // Use prices.gigabyte.raw for the full { denom, base_value, quote_value } object
|
|
502
|
-
* // needed by encodeMsgStartSession's max_price field.
|
|
503
425
|
*/
|
|
504
426
|
export async function getNodePrices(nodeAddress, lcdUrl) {
|
|
505
|
-
|
|
506
|
-
throw new ValidationError(ErrorCodes.INVALID_NODE_ADDRESS, 'nodeAddress must be a valid sentnode1... bech32 address (46 characters)', { value: nodeAddress });
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const fetchNode = async (baseUrl) => {
|
|
510
|
-
let nextKey = null;
|
|
511
|
-
let pages = 0;
|
|
512
|
-
do {
|
|
513
|
-
const keyParam = nextKey ? `&pagination.key=${encodeURIComponent(nextKey)}` : '';
|
|
514
|
-
const data = await lcd(baseUrl, `/sentinel/node/v3/nodes?status=1&pagination.limit=500${keyParam}`);
|
|
515
|
-
const nodes = data.nodes || [];
|
|
516
|
-
const found = nodes.find(n => n.address === nodeAddress);
|
|
517
|
-
if (found) return found;
|
|
518
|
-
nextKey = data.pagination?.next_key || null;
|
|
519
|
-
pages++;
|
|
520
|
-
} while (nextKey && pages < 20);
|
|
521
|
-
return null;
|
|
522
|
-
};
|
|
523
|
-
|
|
524
|
-
let node;
|
|
525
|
-
if (lcdUrl) {
|
|
526
|
-
node = await fetchNode(lcdUrl);
|
|
527
|
-
} else {
|
|
528
|
-
const result = await tryWithFallback(LCD_ENDPOINTS, fetchNode, 'getNodePrices');
|
|
529
|
-
node = result.result;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
if (!node) throw new NodeError(ErrorCodes.NODE_NOT_FOUND, `Node ${nodeAddress} not found on LCD (may be inactive or deregistered)`, { nodeAddress });
|
|
533
|
-
|
|
534
|
-
function extractPrice(priceArray) {
|
|
535
|
-
if (!Array.isArray(priceArray)) return { dvpn: 0, udvpn: 0, raw: null };
|
|
536
|
-
const entry = priceArray.find(p => p.denom === 'udvpn');
|
|
537
|
-
if (!entry) return { dvpn: 0, udvpn: 0, raw: null };
|
|
538
|
-
// Defensive fallback chain: quote_value (V3 current) -> base_value -> amount (legacy)
|
|
539
|
-
const rawVal = entry.quote_value || entry.base_value || entry.amount || '0';
|
|
540
|
-
const udvpn = parseInt(rawVal, 10) || 0;
|
|
541
|
-
return { dvpn: parseFloat((udvpn / 1_000_000).toFixed(6)), udvpn, raw: entry };
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
return {
|
|
545
|
-
gigabyte: extractPrice(node.gigabyte_prices),
|
|
546
|
-
hourly: extractPrice(node.hourly_prices),
|
|
547
|
-
denom: 'P2P',
|
|
548
|
-
nodeAddress,
|
|
549
|
-
};
|
|
427
|
+
return _rpcGetNodePrices(nodeAddress, lcdUrl);
|
|
550
428
|
}
|
|
551
429
|
|
|
552
430
|
// ─── Display & Serialization Helpers ────────────────────────────────────────
|
|
@@ -761,22 +639,20 @@ export function buildRevokeFeeGrantMsg(granter, grantee) {
|
|
|
761
639
|
|
|
762
640
|
/**
|
|
763
641
|
* Query fee grants given to a grantee.
|
|
642
|
+
* Delegates to chain/fee-grants.js RPC-first implementation.
|
|
764
643
|
* @returns {Promise<Array>} Array of allowance objects
|
|
765
644
|
*/
|
|
766
645
|
export async function queryFeeGrants(lcdUrl, grantee) {
|
|
767
|
-
|
|
768
|
-
return data.allowances || [];
|
|
646
|
+
return _rpcQueryFeeGrants(lcdUrl, grantee);
|
|
769
647
|
}
|
|
770
648
|
|
|
771
649
|
/**
|
|
772
650
|
* Query a specific fee grant between granter and grantee.
|
|
651
|
+
* Delegates to chain/fee-grants.js RPC-first implementation.
|
|
773
652
|
* @returns {Promise<object|null>} Allowance object or null
|
|
774
653
|
*/
|
|
775
654
|
export async function queryFeeGrant(lcdUrl, granter, grantee) {
|
|
776
|
-
|
|
777
|
-
const data = await lcd(lcdUrl, `/cosmos/feegrant/v1beta1/allowance/${granter}/${grantee}`);
|
|
778
|
-
return data.allowance || null;
|
|
779
|
-
} catch { return null; } // 404 = no grant
|
|
655
|
+
return _rpcQueryFeeGrant(lcdUrl, granter, grantee);
|
|
780
656
|
}
|
|
781
657
|
|
|
782
658
|
/**
|
|
@@ -878,14 +754,7 @@ export function encodeForExec(msgs) {
|
|
|
878
754
|
});
|
|
879
755
|
}
|
|
880
756
|
|
|
881
|
-
|
|
882
|
-
* Query authz grants between granter and grantee.
|
|
883
|
-
* @returns {Promise<Array>} Array of grant objects
|
|
884
|
-
*/
|
|
885
|
-
export async function queryAuthzGrants(lcdUrl, granter, grantee) {
|
|
886
|
-
const data = await lcd(lcdUrl, `/cosmos/authz/v1beta1/grants?granter=${granter}&grantee=${grantee}`);
|
|
887
|
-
return data.grants || [];
|
|
888
|
-
}
|
|
757
|
+
// queryAuthzGrants removed — use RPC-first version from chain/queries.js
|
|
889
758
|
|
|
890
759
|
// Re-export extractSessionId for convenience (from protocol module)
|
|
891
760
|
export { extractSessionId };
|