blue-js-sdk 2.2.0 → 2.4.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/CHANGELOG.md +34 -0
- package/batch.js +6 -10
- package/chain/authz.js +1 -9
- package/chain/fee-grants.js +53 -2
- package/chain/index.js +30 -167
- package/chain/queries.js +98 -12
- package/chain/rpc.js +169 -0
- package/client/index.js +1 -3
- package/connection/discovery.js +11 -11
- package/cosmjs-setup.js +68 -521
- package/defaults.js +121 -1
- package/index.js +13 -0
- package/node-connect.js +23 -14
- package/package.json +1 -1
- package/pricing/index.js +3 -26
- package/session-manager.js +6 -4
package/cosmjs-setup.js
CHANGED
|
@@ -52,9 +52,42 @@ import {
|
|
|
52
52
|
} from './plan-operations.js';
|
|
53
53
|
import { GAS_PRICE, RPC_ENDPOINTS, LCD_ENDPOINTS, tryWithFallback } from './defaults.js';
|
|
54
54
|
import { ValidationError, NodeError, ChainError, ErrorCodes } from './errors.js';
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
// path, os, fs imports removed — settings persistence now in chain/queries.js
|
|
56
|
+
|
|
57
|
+
// RPC-first query layer — cosmjs-setup.js delegates query functions here
|
|
58
|
+
import {
|
|
59
|
+
findExistingSession as _findExistingSession,
|
|
60
|
+
fetchActiveNodes as _fetchActiveNodes,
|
|
61
|
+
getNetworkOverview as _getNetworkOverview,
|
|
62
|
+
queryNode as _queryNode,
|
|
63
|
+
resolveNodeUrl as _resolveNodeUrl,
|
|
64
|
+
querySubscriptions as _querySubscriptions,
|
|
65
|
+
querySessionAllocation as _querySessionAllocation,
|
|
66
|
+
querySessions as _querySessions,
|
|
67
|
+
flattenSession as _flattenSession,
|
|
68
|
+
querySubscription as _querySubscription,
|
|
69
|
+
hasActiveSubscription as _hasActiveSubscription,
|
|
70
|
+
queryPlanNodes as _queryPlanNodes,
|
|
71
|
+
discoverPlans as _discoverPlans,
|
|
72
|
+
discoverPlanIds as _discoverPlanIds,
|
|
73
|
+
getNodePrices as _getNodePrices,
|
|
74
|
+
getProviderByAddress as _getProviderByAddress,
|
|
75
|
+
queryPlanSubscribers as _queryPlanSubscribers,
|
|
76
|
+
getPlanStats as _getPlanStats,
|
|
77
|
+
querySubscriptionAllocations as _querySubscriptionAllocations,
|
|
78
|
+
queryAuthzGrants as _queryAuthzGrants,
|
|
79
|
+
loadVpnSettings as _loadVpnSettings,
|
|
80
|
+
saveVpnSettings as _saveVpnSettings,
|
|
81
|
+
} from './chain/queries.js';
|
|
82
|
+
import {
|
|
83
|
+
queryFeeGrants as _queryFeeGrants,
|
|
84
|
+
queryFeeGrantsIssued as _queryFeeGrantsIssued,
|
|
85
|
+
queryFeeGrant as _queryFeeGrant,
|
|
86
|
+
grantPlanSubscribers as _grantPlanSubscribers,
|
|
87
|
+
getExpiringGrants as _getExpiringGrants,
|
|
88
|
+
renewExpiringGrants as _renewExpiringGrants,
|
|
89
|
+
monitorFeeGrants as _monitorFeeGrants,
|
|
90
|
+
} from './chain/fee-grants.js';
|
|
58
91
|
|
|
59
92
|
// ─── Input Validation Helpers ────────────────────────────────────────────────
|
|
60
93
|
|
|
@@ -429,29 +462,11 @@ export function extractId(txResult, eventPattern, keyNames) {
|
|
|
429
462
|
}
|
|
430
463
|
|
|
431
464
|
// ─── LCD Query Helper ────────────────────────────────────────────────────────
|
|
465
|
+
// Canonical LCD functions live in chain/lcd.js. Re-export for backward compatibility.
|
|
432
466
|
|
|
433
467
|
import axios from 'axios';
|
|
434
|
-
import { publicEndpointAgent } from './tls-trust.js';
|
|
435
468
|
|
|
436
|
-
|
|
437
|
-
* Query a Sentinel LCD REST endpoint.
|
|
438
|
-
* Checks both HTTP status AND gRPC error codes in response body.
|
|
439
|
-
* Uses CA-validated HTTPS for LCD public infrastructure (valid CA certs).
|
|
440
|
-
*
|
|
441
|
-
* Usage:
|
|
442
|
-
* const data = await lcd('https://lcd.sentinel.co', '/sentinel/node/v3/nodes?status=1');
|
|
443
|
-
*/
|
|
444
|
-
export async function lcd(baseUrl, path) {
|
|
445
|
-
// Accept Endpoint objects ({ url, name }) or bare strings
|
|
446
|
-
const base = typeof baseUrl === 'object' ? baseUrl.url : baseUrl;
|
|
447
|
-
const url = `${base}${path}`;
|
|
448
|
-
const res = await axios.get(url, { httpsAgent: publicEndpointAgent, timeout: 15000 });
|
|
449
|
-
const data = res.data;
|
|
450
|
-
if (data?.code && data.code !== 0) {
|
|
451
|
-
throw new ChainError(ErrorCodes.LCD_ERROR, `LCD ${path}: code=${data.code} ${data.message || ''}`, { path, code: data.code, message: data.message });
|
|
452
|
-
}
|
|
453
|
-
return data;
|
|
454
|
-
}
|
|
469
|
+
export { lcd, lcdQuery, lcdQueryAll, lcdPaginatedSafe } from './chain/lcd.js';
|
|
455
470
|
|
|
456
471
|
// ─── Query Helpers ───────────────────────────────────────────────────────────
|
|
457
472
|
|
|
@@ -472,18 +487,7 @@ export async function getBalance(client, address) {
|
|
|
472
487
|
* Note: Sessions have a nested base_session object containing the actual data.
|
|
473
488
|
*/
|
|
474
489
|
export async function findExistingSession(lcdUrl, walletAddr, nodeAddr) {
|
|
475
|
-
|
|
476
|
-
for (const s of items) {
|
|
477
|
-
const bs = s.base_session || s;
|
|
478
|
-
if ((bs.node_address || bs.node) !== nodeAddr) continue;
|
|
479
|
-
if (bs.status && bs.status !== 'active') continue;
|
|
480
|
-
const acct = bs.acc_address || bs.address;
|
|
481
|
-
if (acct && acct !== walletAddr) continue;
|
|
482
|
-
const maxBytes = parseInt(bs.max_bytes || '0');
|
|
483
|
-
const used = parseInt(bs.download_bytes || '0') + parseInt(bs.upload_bytes || '0');
|
|
484
|
-
if (maxBytes === 0 || used < maxBytes) return BigInt(bs.id);
|
|
485
|
-
}
|
|
486
|
-
return null;
|
|
490
|
+
return _findExistingSession(lcdUrl, walletAddr, nodeAddr);
|
|
487
491
|
}
|
|
488
492
|
|
|
489
493
|
/**
|
|
@@ -493,13 +497,7 @@ export async function findExistingSession(lcdUrl, walletAddr, nodeAddr) {
|
|
|
493
497
|
* This handles both formats.
|
|
494
498
|
*/
|
|
495
499
|
export function resolveNodeUrl(node) {
|
|
496
|
-
|
|
497
|
-
if (node.remote_url && typeof node.remote_url === 'string') return node.remote_url;
|
|
498
|
-
// v3 LCD: remote_addrs is an array of "IP:PORT" strings
|
|
499
|
-
const addrs = node.remote_addrs || [];
|
|
500
|
-
const raw = addrs.find(a => a.includes(':')) || addrs[0];
|
|
501
|
-
if (!raw) throw new NodeError(ErrorCodes.NODE_NOT_FOUND, `Node ${node.address} has no remote_addrs`, { address: node.address });
|
|
502
|
-
return raw.startsWith('http') ? raw : `https://${raw}`;
|
|
500
|
+
return _resolveNodeUrl(node);
|
|
503
501
|
}
|
|
504
502
|
|
|
505
503
|
/**
|
|
@@ -507,11 +505,7 @@ export function resolveNodeUrl(node) {
|
|
|
507
505
|
* Returns array of node objects. Each node has `remote_url` resolved from `remote_addrs`.
|
|
508
506
|
*/
|
|
509
507
|
export async function fetchActiveNodes(lcdUrl, limit = 500, maxPages = 20) {
|
|
510
|
-
|
|
511
|
-
for (const n of items) {
|
|
512
|
-
try { n.remote_url = resolveNodeUrl(n); } catch { /* skip nodes with no address */ }
|
|
513
|
-
}
|
|
514
|
-
return items;
|
|
508
|
+
return _fetchActiveNodes(lcdUrl, limit, maxPages);
|
|
515
509
|
}
|
|
516
510
|
|
|
517
511
|
/**
|
|
@@ -527,55 +521,7 @@ export async function fetchActiveNodes(lcdUrl, limit = 500, maxPages = 20) {
|
|
|
527
521
|
* console.log(`Average: ${overview.averagePrice.gigabyteDvpn.toFixed(3)} P2P/GB`);
|
|
528
522
|
*/
|
|
529
523
|
export async function getNetworkOverview(lcdUrl) {
|
|
530
|
-
|
|
531
|
-
if (lcdUrl) {
|
|
532
|
-
nodes = await fetchActiveNodes(lcdUrl);
|
|
533
|
-
} else {
|
|
534
|
-
const result = await tryWithFallback(LCD_ENDPOINTS, fetchActiveNodes, 'getNetworkOverview');
|
|
535
|
-
nodes = result.result;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Filter to nodes that accept udvpn
|
|
539
|
-
const active = nodes.filter(n => n.remote_url && (n.gigabyte_prices || []).some(p => p.denom === 'udvpn'));
|
|
540
|
-
|
|
541
|
-
// Count by country (from LCD metadata, limited — enrichNodes gives better data)
|
|
542
|
-
const countryMap = {};
|
|
543
|
-
for (const n of active) {
|
|
544
|
-
const c = n.location?.country || n.country || 'Unknown';
|
|
545
|
-
countryMap[c] = (countryMap[c] || 0) + 1;
|
|
546
|
-
}
|
|
547
|
-
const byCountry = Object.entries(countryMap)
|
|
548
|
-
.map(([country, count]) => ({ country, count }))
|
|
549
|
-
.sort((a, b) => b.count - a.count);
|
|
550
|
-
|
|
551
|
-
// Count by type (type not in LCD — estimate from service_type field if present)
|
|
552
|
-
const byType = { wireguard: 0, v2ray: 0, unknown: 0 };
|
|
553
|
-
for (const n of active) {
|
|
554
|
-
const t = n.service_type || n.type;
|
|
555
|
-
if (t === 'wireguard' || t === 1) byType.wireguard++;
|
|
556
|
-
else if (t === 'v2ray' || t === 2) byType.v2ray++;
|
|
557
|
-
else byType.unknown++;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Average prices
|
|
561
|
-
let gbTotal = 0, gbCount = 0, hrTotal = 0, hrCount = 0;
|
|
562
|
-
for (const n of active) {
|
|
563
|
-
const gb = (n.gigabyte_prices || []).find(p => p.denom === 'udvpn');
|
|
564
|
-
if (gb?.quote_value) { gbTotal += parseInt(gb.quote_value, 10); gbCount++; }
|
|
565
|
-
const hr = (n.hourly_prices || []).find(p => p.denom === 'udvpn');
|
|
566
|
-
if (hr?.quote_value) { hrTotal += parseInt(hr.quote_value, 10); hrCount++; }
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
return {
|
|
570
|
-
totalNodes: active.length,
|
|
571
|
-
byCountry,
|
|
572
|
-
byType,
|
|
573
|
-
averagePrice: {
|
|
574
|
-
gigabyteDvpn: gbCount > 0 ? (gbTotal / gbCount) / 1_000_000 : 0,
|
|
575
|
-
hourlyDvpn: hrCount > 0 ? (hrTotal / hrCount) / 1_000_000 : 0,
|
|
576
|
-
},
|
|
577
|
-
nodes: active,
|
|
578
|
-
};
|
|
524
|
+
return _getNetworkOverview(lcdUrl);
|
|
579
525
|
}
|
|
580
526
|
|
|
581
527
|
/**
|
|
@@ -584,9 +530,7 @@ export async function getNetworkOverview(lcdUrl) {
|
|
|
584
530
|
* Returns sorted array of plan IDs that have at least 1 subscription.
|
|
585
531
|
*/
|
|
586
532
|
export async function discoverPlanIds(lcdUrl, maxId = 500) {
|
|
587
|
-
|
|
588
|
-
const plans = await discoverPlans(lcdUrl, { maxId });
|
|
589
|
-
return plans.map(p => p.id);
|
|
533
|
+
return _discoverPlanIds(lcdUrl, maxId);
|
|
590
534
|
}
|
|
591
535
|
|
|
592
536
|
/**
|
|
@@ -606,29 +550,7 @@ export async function discoverPlanIds(lcdUrl, maxId = 500) {
|
|
|
606
550
|
* // needed by encodeMsgStartSession's max_price field.
|
|
607
551
|
*/
|
|
608
552
|
export async function getNodePrices(nodeAddress, lcdUrl) {
|
|
609
|
-
|
|
610
|
-
throw new ValidationError(ErrorCodes.INVALID_NODE_ADDRESS, 'nodeAddress must be a valid sentnode1... bech32 address (46 characters)', { value: nodeAddress });
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Reuse queryNode() instead of duplicating pagination
|
|
614
|
-
const node = await queryNode(nodeAddress, { lcdUrl });
|
|
615
|
-
|
|
616
|
-
function extractPrice(priceArray) {
|
|
617
|
-
if (!Array.isArray(priceArray)) return { dvpn: 0, udvpn: 0, raw: null };
|
|
618
|
-
const entry = priceArray.find(p => p.denom === 'udvpn');
|
|
619
|
-
if (!entry) return { dvpn: 0, udvpn: 0, raw: null };
|
|
620
|
-
// Defensive fallback chain: quote_value (V3 current) → base_value → amount (legacy)
|
|
621
|
-
const rawVal = entry.quote_value || entry.base_value || entry.amount || '0';
|
|
622
|
-
const udvpn = parseInt(rawVal, 10) || 0;
|
|
623
|
-
return { dvpn: parseFloat((udvpn / 1_000_000).toFixed(6)), udvpn, raw: entry };
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
return {
|
|
627
|
-
gigabyte: extractPrice(node.gigabyte_prices),
|
|
628
|
-
hourly: extractPrice(node.hourly_prices),
|
|
629
|
-
denom: 'P2P',
|
|
630
|
-
nodeAddress,
|
|
631
|
-
};
|
|
553
|
+
return _getNodePrices(nodeAddress, lcdUrl);
|
|
632
554
|
}
|
|
633
555
|
|
|
634
556
|
// ─── Display & Serialization Helpers ────────────────────────────────────────
|
|
@@ -861,8 +783,7 @@ export function buildRevokeFeeGrantMsg(granter, grantee) {
|
|
|
861
783
|
* @returns {Promise<Array>} Array of allowance objects
|
|
862
784
|
*/
|
|
863
785
|
export async function queryFeeGrants(lcdUrl, grantee) {
|
|
864
|
-
|
|
865
|
-
return items;
|
|
786
|
+
return _queryFeeGrants(lcdUrl, grantee);
|
|
866
787
|
}
|
|
867
788
|
|
|
868
789
|
/**
|
|
@@ -872,8 +793,7 @@ export async function queryFeeGrants(lcdUrl, grantee) {
|
|
|
872
793
|
* @returns {Promise<Array>}
|
|
873
794
|
*/
|
|
874
795
|
export async function queryFeeGrantsIssued(lcdUrl, granter) {
|
|
875
|
-
|
|
876
|
-
return items;
|
|
796
|
+
return _queryFeeGrantsIssued(lcdUrl, granter);
|
|
877
797
|
}
|
|
878
798
|
|
|
879
799
|
/**
|
|
@@ -881,10 +801,7 @@ export async function queryFeeGrantsIssued(lcdUrl, granter) {
|
|
|
881
801
|
* @returns {Promise<object|null>} Allowance object or null
|
|
882
802
|
*/
|
|
883
803
|
export async function queryFeeGrant(lcdUrl, granter, grantee) {
|
|
884
|
-
|
|
885
|
-
const data = await lcd(lcdUrl, `/cosmos/feegrant/v1beta1/allowance/${granter}/${grantee}`);
|
|
886
|
-
return data.allowance || null;
|
|
887
|
-
} catch { return null; } // 404 = no grant
|
|
804
|
+
return _queryFeeGrant(lcdUrl, granter, grantee);
|
|
888
805
|
}
|
|
889
806
|
|
|
890
807
|
/**
|
|
@@ -995,94 +912,10 @@ export function encodeForExec(msgs) {
|
|
|
995
912
|
* @returns {Promise<Array>} Array of grant objects
|
|
996
913
|
*/
|
|
997
914
|
export async function queryAuthzGrants(lcdUrl, granter, grantee) {
|
|
998
|
-
|
|
999
|
-
return items;
|
|
915
|
+
return _queryAuthzGrants(lcdUrl, granter, grantee);
|
|
1000
916
|
}
|
|
1001
917
|
|
|
1002
|
-
//
|
|
1003
|
-
// General-purpose LCD query with timeout, retry, error wrapping, and pagination.
|
|
1004
|
-
|
|
1005
|
-
/**
|
|
1006
|
-
* Single LCD query with timeout, single retry on network error, and ChainError wrapping.
|
|
1007
|
-
* Uses the fallback endpoint list if no lcdUrl is provided.
|
|
1008
|
-
*
|
|
1009
|
-
* @param {string} path - LCD path (e.g. '/sentinel/node/v3/nodes?status=1')
|
|
1010
|
-
* @param {object} [opts]
|
|
1011
|
-
* @param {string} [opts.lcdUrl] - Specific LCD endpoint (or uses fallback chain)
|
|
1012
|
-
* @param {number} [opts.timeout] - Request timeout in ms (default: 15000)
|
|
1013
|
-
* @returns {Promise<any>} Parsed JSON response
|
|
1014
|
-
*/
|
|
1015
|
-
export async function lcdQuery(path, opts = {}) {
|
|
1016
|
-
const timeout = opts.timeout || 15000;
|
|
1017
|
-
const doQuery = async (baseUrl) => {
|
|
1018
|
-
try {
|
|
1019
|
-
return await lcd(baseUrl, path);
|
|
1020
|
-
} catch (err) {
|
|
1021
|
-
// Single retry on network error
|
|
1022
|
-
if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.message?.includes('timeout')) {
|
|
1023
|
-
await new Promise(r => setTimeout(r, 1000));
|
|
1024
|
-
return await lcd(baseUrl, path);
|
|
1025
|
-
}
|
|
1026
|
-
throw err;
|
|
1027
|
-
}
|
|
1028
|
-
};
|
|
1029
|
-
|
|
1030
|
-
if (opts.lcdUrl) {
|
|
1031
|
-
return doQuery(opts.lcdUrl);
|
|
1032
|
-
}
|
|
1033
|
-
const { result } = await tryWithFallback(LCD_ENDPOINTS, doQuery, `LCD ${path}`);
|
|
1034
|
-
return result;
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
/**
|
|
1038
|
-
* Auto-paginating LCD query. Fetches all pages via next_key, returns all results + chain total.
|
|
1039
|
-
*
|
|
1040
|
-
* @param {string} basePath - LCD path without pagination params (e.g. '/sentinel/node/v3/nodes?status=1')
|
|
1041
|
-
* @param {object} [opts]
|
|
1042
|
-
* @param {string} [opts.lcdUrl] - Specific LCD endpoint (or uses fallback chain)
|
|
1043
|
-
* @param {number} [opts.limit] - Page size (default: 200)
|
|
1044
|
-
* @param {number} [opts.timeout] - Per-page timeout (default: 15000)
|
|
1045
|
-
* @param {string} [opts.dataKey] - Key for the results array in response (default: auto-detect first array)
|
|
1046
|
-
* @returns {Promise<{ items: any[], total: number|null }>}
|
|
1047
|
-
*/
|
|
1048
|
-
export async function lcdQueryAll(basePath, opts = {}) {
|
|
1049
|
-
const limit = opts.limit || 200;
|
|
1050
|
-
const dataKey = opts.dataKey || null;
|
|
1051
|
-
|
|
1052
|
-
const fetchAll = async (baseUrl) => {
|
|
1053
|
-
let allItems = [];
|
|
1054
|
-
let nextKey = null;
|
|
1055
|
-
let chainTotal = null;
|
|
1056
|
-
let isFirst = true;
|
|
1057
|
-
do {
|
|
1058
|
-
const sep = basePath.includes('?') ? '&' : '?';
|
|
1059
|
-
let url = `${basePath}${sep}pagination.limit=${limit}`;
|
|
1060
|
-
if (isFirst) url += '&pagination.count_total=true';
|
|
1061
|
-
if (nextKey) url += `&pagination.key=${encodeURIComponent(nextKey)}`;
|
|
1062
|
-
const data = await lcd(baseUrl, url);
|
|
1063
|
-
if (isFirst && data.pagination?.total) {
|
|
1064
|
-
chainTotal = parseInt(data.pagination.total, 10);
|
|
1065
|
-
}
|
|
1066
|
-
// Auto-detect data key: first array property that isn't 'pagination'
|
|
1067
|
-
const key = dataKey || Object.keys(data).find(k => k !== 'pagination' && Array.isArray(data[k]));
|
|
1068
|
-
const pageItems = key ? (data[key] || []) : [];
|
|
1069
|
-
allItems = allItems.concat(pageItems);
|
|
1070
|
-
nextKey = data.pagination?.next_key || null;
|
|
1071
|
-
isFirst = false;
|
|
1072
|
-
} while (nextKey);
|
|
1073
|
-
|
|
1074
|
-
if (chainTotal && allItems.length !== chainTotal) {
|
|
1075
|
-
console.warn(`[lcdQueryAll] Pagination mismatch: got ${allItems.length}, chain reports ${chainTotal}`);
|
|
1076
|
-
}
|
|
1077
|
-
return { items: allItems, total: chainTotal };
|
|
1078
|
-
};
|
|
1079
|
-
|
|
1080
|
-
if (opts.lcdUrl) {
|
|
1081
|
-
return fetchAll(opts.lcdUrl);
|
|
1082
|
-
}
|
|
1083
|
-
const { result } = await tryWithFallback(LCD_ENDPOINTS, fetchAll, `LCD paginated ${basePath}`);
|
|
1084
|
-
return result;
|
|
1085
|
-
}
|
|
918
|
+
// LCD Query Helpers — canonical implementations in chain/lcd.js, re-exported above.
|
|
1086
919
|
|
|
1087
920
|
// ─── Plan Subscriber Helpers (v25b) ──────────────────────────────────────────
|
|
1088
921
|
|
|
@@ -1096,20 +929,7 @@ export async function lcdQueryAll(basePath, opts = {}) {
|
|
|
1096
929
|
* @returns {Promise<{ subscribers: Array<{ address: string, status: number, id: string }>, total: number|null }>}
|
|
1097
930
|
*/
|
|
1098
931
|
export async function queryPlanSubscribers(planId, opts = {}) {
|
|
1099
|
-
|
|
1100
|
-
`/sentinel/subscription/v3/plans/${planId}/subscriptions`,
|
|
1101
|
-
{ lcdUrl: opts.lcdUrl, dataKey: 'subscriptions' },
|
|
1102
|
-
);
|
|
1103
|
-
let subscribers = items.map(s => ({
|
|
1104
|
-
address: s.address || s.subscriber,
|
|
1105
|
-
status: s.status,
|
|
1106
|
-
id: s.id || s.base_id,
|
|
1107
|
-
...s,
|
|
1108
|
-
}));
|
|
1109
|
-
if (opts.excludeAddress) {
|
|
1110
|
-
subscribers = subscribers.filter(s => s.address !== opts.excludeAddress);
|
|
1111
|
-
}
|
|
1112
|
-
return { subscribers, total };
|
|
932
|
+
return _queryPlanSubscribers(planId, opts);
|
|
1113
933
|
}
|
|
1114
934
|
|
|
1115
935
|
/**
|
|
@@ -1122,14 +942,7 @@ export async function queryPlanSubscribers(planId, opts = {}) {
|
|
|
1122
942
|
* @returns {Promise<{ subscriberCount: number, totalOnChain: number, ownerSubscribed: boolean }>}
|
|
1123
943
|
*/
|
|
1124
944
|
export async function getPlanStats(planId, ownerAddress, opts = {}) {
|
|
1125
|
-
|
|
1126
|
-
const ownerSubscribed = subscribers.some(s => s.address === ownerAddress);
|
|
1127
|
-
const filtered = subscribers.filter(s => s.address !== ownerAddress);
|
|
1128
|
-
return {
|
|
1129
|
-
subscriberCount: filtered.length,
|
|
1130
|
-
totalOnChain: total,
|
|
1131
|
-
ownerSubscribed,
|
|
1132
|
-
};
|
|
945
|
+
return _getPlanStats(planId, ownerAddress, opts);
|
|
1133
946
|
}
|
|
1134
947
|
|
|
1135
948
|
// ─── Fee Grant Workflow Helpers (v25b) ────────────────────────────────────────
|
|
@@ -1146,39 +959,7 @@ export async function getPlanStats(planId, ownerAddress, opts = {}) {
|
|
|
1146
959
|
* @returns {Promise<{ msgs: Array, skipped: string[], newGrants: string[] }>} Messages ready for broadcast
|
|
1147
960
|
*/
|
|
1148
961
|
export async function grantPlanSubscribers(planId, opts = {}) {
|
|
1149
|
-
|
|
1150
|
-
if (!granterAddress) throw new ValidationError(ErrorCodes.INVALID_OPTIONS, 'granterAddress is required');
|
|
1151
|
-
|
|
1152
|
-
// Get subscribers
|
|
1153
|
-
const { subscribers } = await queryPlanSubscribers(planId, { lcdUrl });
|
|
1154
|
-
|
|
1155
|
-
// Get existing grants ISSUED BY granter (not grants received)
|
|
1156
|
-
const existingGrants = await queryFeeGrantsIssued(lcdUrl || LCD_ENDPOINTS[0].url, granterAddress);
|
|
1157
|
-
const alreadyGranted = new Set(existingGrants.map(g => g.grantee));
|
|
1158
|
-
|
|
1159
|
-
const msgs = [];
|
|
1160
|
-
const skipped = [];
|
|
1161
|
-
const newGrants = [];
|
|
1162
|
-
|
|
1163
|
-
const now = new Date();
|
|
1164
|
-
// Deduplicate by address and filter active+non-expired
|
|
1165
|
-
const seen = new Set();
|
|
1166
|
-
for (const sub of subscribers) {
|
|
1167
|
-
const addr = sub.acc_address || sub.address;
|
|
1168
|
-
if (!addr || seen.has(addr)) continue;
|
|
1169
|
-
seen.add(addr);
|
|
1170
|
-
// Skip self-grant (chain rejects granter === grantee)
|
|
1171
|
-
if (addr === granterAddress || isSameKey(addr, granterAddress)) { skipped.push(addr); continue; }
|
|
1172
|
-
// Skip inactive or expired
|
|
1173
|
-
if (sub.status && sub.status !== 'active') { skipped.push(addr); continue; }
|
|
1174
|
-
if (sub.inactive_at && new Date(sub.inactive_at) <= now) { skipped.push(addr); continue; }
|
|
1175
|
-
// Skip already granted
|
|
1176
|
-
if (alreadyGranted.has(addr)) { skipped.push(addr); continue; }
|
|
1177
|
-
msgs.push(buildFeeGrantMsg(granterAddress, addr, grantOpts));
|
|
1178
|
-
newGrants.push(addr);
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
return { msgs, skipped, newGrants };
|
|
962
|
+
return _grantPlanSubscribers(planId, opts);
|
|
1182
963
|
}
|
|
1183
964
|
|
|
1184
965
|
/**
|
|
@@ -1191,34 +972,7 @@ export async function grantPlanSubscribers(planId, opts = {}) {
|
|
|
1191
972
|
* @returns {Promise<Array<{ granter: string, grantee: string, expiresAt: Date|null, daysLeft: number|null }>>}
|
|
1192
973
|
*/
|
|
1193
974
|
export async function getExpiringGrants(lcdUrl, granteeOrGranter, withinDays = 7, role = 'grantee') {
|
|
1194
|
-
|
|
1195
|
-
? await queryFeeGrants(lcdUrl, granteeOrGranter)
|
|
1196
|
-
: await queryFeeGrantsIssued(lcdUrl, granteeOrGranter);
|
|
1197
|
-
|
|
1198
|
-
const now = Date.now();
|
|
1199
|
-
const cutoff = now + withinDays * 24 * 60 * 60_000;
|
|
1200
|
-
const expiring = [];
|
|
1201
|
-
|
|
1202
|
-
for (const g of grants) {
|
|
1203
|
-
// Fee grant allowances have complex nested @type structures:
|
|
1204
|
-
// BasicAllowance: { expiration }
|
|
1205
|
-
// PeriodicAllowance: { basic: { expiration } }
|
|
1206
|
-
// AllowedMsgAllowance: { allowance: { expiration } or allowance: { basic: { expiration } } }
|
|
1207
|
-
const a = g.allowance || {};
|
|
1208
|
-
const inner = a.allowance || a; // unwrap AllowedMsgAllowance
|
|
1209
|
-
const expStr = inner.expiration || inner.basic?.expiration || a.expiration || a.basic?.expiration;
|
|
1210
|
-
if (!expStr) continue; // no expiry set
|
|
1211
|
-
const expiresAt = new Date(expStr);
|
|
1212
|
-
if (expiresAt.getTime() <= cutoff) {
|
|
1213
|
-
expiring.push({
|
|
1214
|
-
granter: g.granter,
|
|
1215
|
-
grantee: g.grantee,
|
|
1216
|
-
expiresAt,
|
|
1217
|
-
daysLeft: Math.max(0, Math.round((expiresAt.getTime() - now) / (24 * 60 * 60_000))),
|
|
1218
|
-
});
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
return expiring;
|
|
975
|
+
return _getExpiringGrants(lcdUrl, granteeOrGranter, withinDays, role);
|
|
1222
976
|
}
|
|
1223
977
|
|
|
1224
978
|
/**
|
|
@@ -1231,18 +985,7 @@ export async function getExpiringGrants(lcdUrl, granteeOrGranter, withinDays = 7
|
|
|
1231
985
|
* @returns {Promise<{ msgs: Array, renewed: string[] }>} Messages ready for broadcast
|
|
1232
986
|
*/
|
|
1233
987
|
export async function renewExpiringGrants(lcdUrl, granterAddress, withinDays = 7, grantOpts = {}) {
|
|
1234
|
-
|
|
1235
|
-
const msgs = [];
|
|
1236
|
-
const renewed = [];
|
|
1237
|
-
|
|
1238
|
-
for (const g of expiring) {
|
|
1239
|
-
if (g.grantee === granterAddress) continue; // skip self
|
|
1240
|
-
msgs.push(buildRevokeFeeGrantMsg(granterAddress, g.grantee));
|
|
1241
|
-
msgs.push(buildFeeGrantMsg(granterAddress, g.grantee, grantOpts));
|
|
1242
|
-
renewed.push(g.grantee);
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
return { msgs, renewed };
|
|
988
|
+
return _renewExpiringGrants(lcdUrl, granterAddress, withinDays, grantOpts);
|
|
1246
989
|
}
|
|
1247
990
|
|
|
1248
991
|
// ─── Fee Grant Monitoring (v25b) ─────────────────────────────────────────────
|
|
@@ -1260,46 +1003,7 @@ export async function renewExpiringGrants(lcdUrl, granterAddress, withinDays = 7
|
|
|
1260
1003
|
* @returns {EventEmitter} Emits 'expiring' and 'expired' events. Call .stop() to stop monitoring.
|
|
1261
1004
|
*/
|
|
1262
1005
|
export function monitorFeeGrants(opts = {}) {
|
|
1263
|
-
|
|
1264
|
-
if (!lcdUrl || !address) throw new ValidationError(ErrorCodes.INVALID_OPTIONS, 'monitorFeeGrants requires lcdUrl and address');
|
|
1265
|
-
|
|
1266
|
-
const emitter = new EventEmitter();
|
|
1267
|
-
let timer = null;
|
|
1268
|
-
|
|
1269
|
-
const check = async () => {
|
|
1270
|
-
try {
|
|
1271
|
-
const expiring = await getExpiringGrants(lcdUrl, address, warnDays, 'granter');
|
|
1272
|
-
const now = Date.now();
|
|
1273
|
-
|
|
1274
|
-
for (const g of expiring) {
|
|
1275
|
-
if (g.expiresAt.getTime() <= now) {
|
|
1276
|
-
emitter.emit('expired', g);
|
|
1277
|
-
} else {
|
|
1278
|
-
emitter.emit('expiring', g);
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
if (autoRenew && expiring.length > 0) {
|
|
1283
|
-
const { msgs, renewed } = await renewExpiringGrants(lcdUrl, address, warnDays, grantOpts);
|
|
1284
|
-
if (msgs.length > 0) {
|
|
1285
|
-
emitter.emit('renew', { msgs, renewed });
|
|
1286
|
-
}
|
|
1287
|
-
}
|
|
1288
|
-
} catch (err) {
|
|
1289
|
-
emitter.emit('error', err);
|
|
1290
|
-
}
|
|
1291
|
-
};
|
|
1292
|
-
|
|
1293
|
-
// Start checking
|
|
1294
|
-
check();
|
|
1295
|
-
timer = setInterval(check, checkIntervalMs);
|
|
1296
|
-
if (timer.unref) timer.unref(); // Don't prevent process exit
|
|
1297
|
-
|
|
1298
|
-
emitter.stop = () => {
|
|
1299
|
-
if (timer) { clearInterval(timer); timer = null; }
|
|
1300
|
-
};
|
|
1301
|
-
|
|
1302
|
-
return emitter;
|
|
1006
|
+
return _monitorFeeGrants(opts);
|
|
1303
1007
|
}
|
|
1304
1008
|
|
|
1305
1009
|
// ─── Query Helpers (v25c) ────────────────────────────────────────────────────
|
|
@@ -1311,10 +1015,7 @@ export function monitorFeeGrants(opts = {}) {
|
|
|
1311
1015
|
* @returns {Promise<{ subscriptions: any[], total: number|null }>}
|
|
1312
1016
|
*/
|
|
1313
1017
|
export async function querySubscriptions(lcdUrl, walletAddr, opts = {}) {
|
|
1314
|
-
|
|
1315
|
-
let path = `/sentinel/subscription/v3/accounts/${walletAddr}/subscriptions`;
|
|
1316
|
-
if (opts.status) path += `?status=${opts.status === 'active' ? '1' : '2'}`;
|
|
1317
|
-
return lcdQueryAll(path, { lcdUrl, dataKey: 'subscriptions' });
|
|
1018
|
+
return _querySubscriptions(lcdUrl, walletAddr, opts);
|
|
1318
1019
|
}
|
|
1319
1020
|
|
|
1320
1021
|
/**
|
|
@@ -1324,20 +1025,7 @@ export async function querySubscriptions(lcdUrl, walletAddr, opts = {}) {
|
|
|
1324
1025
|
* @returns {Promise<{ maxBytes: number, usedBytes: number, remainingBytes: number, percentUsed: number }|null>}
|
|
1325
1026
|
*/
|
|
1326
1027
|
export async function querySessionAllocation(lcdUrl, sessionId) {
|
|
1327
|
-
|
|
1328
|
-
const data = await lcd(lcdUrl, `/sentinel/session/v3/sessions/${sessionId}`);
|
|
1329
|
-
const s = data.session?.base_session || data.session || {};
|
|
1330
|
-
const maxBytes = parseInt(s.max_bytes || '0', 10);
|
|
1331
|
-
const dl = parseInt(s.download_bytes || '0', 10);
|
|
1332
|
-
const ul = parseInt(s.upload_bytes || '0', 10);
|
|
1333
|
-
const usedBytes = dl + ul;
|
|
1334
|
-
return {
|
|
1335
|
-
maxBytes,
|
|
1336
|
-
usedBytes,
|
|
1337
|
-
remainingBytes: Math.max(0, maxBytes - usedBytes),
|
|
1338
|
-
percentUsed: maxBytes > 0 ? Math.round((usedBytes / maxBytes) * 100) : 0,
|
|
1339
|
-
};
|
|
1340
|
-
} catch { return null; }
|
|
1028
|
+
return _querySessionAllocation(lcdUrl, sessionId);
|
|
1341
1029
|
}
|
|
1342
1030
|
|
|
1343
1031
|
/**
|
|
@@ -1350,28 +1038,7 @@ export async function querySessionAllocation(lcdUrl, sessionId) {
|
|
|
1350
1038
|
* @returns {Promise<object>} Node object with remote_url resolved
|
|
1351
1039
|
*/
|
|
1352
1040
|
export async function queryNode(nodeAddress, opts = {}) {
|
|
1353
|
-
|
|
1354
|
-
throw new ValidationError(ErrorCodes.INVALID_NODE_ADDRESS, 'nodeAddress must be sentnode1...', { nodeAddress });
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
const fetchDirect = async (baseUrl) => {
|
|
1358
|
-
try {
|
|
1359
|
-
const data = await lcdQuery(`/sentinel/node/v3/nodes/${nodeAddress}`, { lcdUrl: baseUrl });
|
|
1360
|
-
if (data?.node) {
|
|
1361
|
-
data.node.remote_url = resolveNodeUrl(data.node);
|
|
1362
|
-
return data.node;
|
|
1363
|
-
}
|
|
1364
|
-
} catch { /* fall through to full list */ }
|
|
1365
|
-
const { items } = await lcdPaginatedSafe(baseUrl, '/sentinel/node/v3/nodes?status=1', 'nodes');
|
|
1366
|
-
const found = items.find(n => n.address === nodeAddress);
|
|
1367
|
-
if (!found) throw new NodeError(ErrorCodes.NODE_NOT_FOUND, `Node ${nodeAddress} not found on LCD (may be inactive)`, { nodeAddress });
|
|
1368
|
-
found.remote_url = resolveNodeUrl(found);
|
|
1369
|
-
return found;
|
|
1370
|
-
};
|
|
1371
|
-
|
|
1372
|
-
if (opts.lcdUrl) return fetchDirect(opts.lcdUrl);
|
|
1373
|
-
const { result } = await tryWithFallback(LCD_ENDPOINTS, fetchDirect, `LCD node lookup ${nodeAddress}`);
|
|
1374
|
-
return result;
|
|
1041
|
+
return _queryNode(nodeAddress, opts);
|
|
1375
1042
|
}
|
|
1376
1043
|
|
|
1377
1044
|
/**
|
|
@@ -1408,53 +1075,7 @@ export function buildEndSessionMsg(from, sessionId) {
|
|
|
1408
1075
|
};
|
|
1409
1076
|
}
|
|
1410
1077
|
|
|
1411
|
-
//
|
|
1412
|
-
|
|
1413
|
-
/**
|
|
1414
|
-
* Paginated LCD query that handles Sentinel's broken pagination.
|
|
1415
|
-
* Tries next_key first. If next_key is null but we got exactly `limit` results
|
|
1416
|
-
* (suggesting truncation), falls back to a single large request.
|
|
1417
|
-
*
|
|
1418
|
-
* @param {string} lcdUrl - LCD base URL
|
|
1419
|
-
* @param {string} path - Endpoint path (e.g. '/sentinel/node/v3/plans/36/nodes')
|
|
1420
|
-
* @param {string} itemsKey - Response array key ('nodes', 'subscriptions', 'sessions')
|
|
1421
|
-
* @param {object} [opts]
|
|
1422
|
-
* @param {number} [opts.limit=500] - Page size for paginated requests
|
|
1423
|
-
* @param {number} [opts.fallbackLimit=5000] - Single-request limit if pagination broken
|
|
1424
|
-
* @returns {Promise<{ items: any[], total: number }>}
|
|
1425
|
-
*/
|
|
1426
|
-
export async function lcdPaginatedSafe(lcdUrl, path, itemsKey, opts = {}) {
|
|
1427
|
-
const limit = opts.limit || 500;
|
|
1428
|
-
const fallbackLimit = opts.fallbackLimit || 5000;
|
|
1429
|
-
const baseLcd = lcdUrl || LCD_ENDPOINTS[0].url;
|
|
1430
|
-
const sep = path.includes('?') ? '&' : '?';
|
|
1431
|
-
|
|
1432
|
-
const firstPage = await lcd(baseLcd, `${path}${sep}pagination.limit=${limit}`);
|
|
1433
|
-
const firstItems = firstPage[itemsKey] || [];
|
|
1434
|
-
const nextKey = firstPage.pagination?.next_key;
|
|
1435
|
-
|
|
1436
|
-
// Fewer than limit = that's everything
|
|
1437
|
-
if (firstItems.length < limit) {
|
|
1438
|
-
return { items: firstItems, total: firstItems.length };
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
// next_key exists = pagination works, follow it
|
|
1442
|
-
if (nextKey) {
|
|
1443
|
-
let allItems = [...firstItems];
|
|
1444
|
-
let key = nextKey;
|
|
1445
|
-
while (key) {
|
|
1446
|
-
const page = await lcd(baseLcd, `${path}${sep}pagination.limit=${limit}&pagination.key=${encodeURIComponent(key)}`);
|
|
1447
|
-
allItems.push(...(page[itemsKey] || []));
|
|
1448
|
-
key = page.pagination?.next_key || null;
|
|
1449
|
-
}
|
|
1450
|
-
return { items: allItems, total: allItems.length };
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
// next_key null but hit limit = broken pagination. Single large request.
|
|
1454
|
-
const fullData = await lcd(baseLcd, `${path}${sep}pagination.limit=${fallbackLimit}`);
|
|
1455
|
-
const allItems = fullData[itemsKey] || [];
|
|
1456
|
-
return { items: allItems, total: allItems.length };
|
|
1457
|
-
}
|
|
1078
|
+
// lcdPaginatedSafe — canonical implementation in chain/lcd.js, re-exported above.
|
|
1458
1079
|
|
|
1459
1080
|
// ─── v26c: Session & Subscription Queries ────────────────────────────────────
|
|
1460
1081
|
|
|
@@ -1467,12 +1088,7 @@ export async function lcdPaginatedSafe(lcdUrl, path, itemsKey, opts = {}) {
|
|
|
1467
1088
|
* @returns {Promise<{ items: ChainSession[], total: number }>}
|
|
1468
1089
|
*/
|
|
1469
1090
|
export async function querySessions(address, lcdUrl, opts = {}) {
|
|
1470
|
-
|
|
1471
|
-
if (opts.status) path += `&status=${opts.status}`;
|
|
1472
|
-
const result = await lcdPaginatedSafe(lcdUrl, path, 'sessions');
|
|
1473
|
-
// Auto-flatten base_session nesting so devs don't hit session.id === undefined
|
|
1474
|
-
result.items = result.items.map(flattenSession);
|
|
1475
|
-
return result;
|
|
1091
|
+
return _querySessions(address, lcdUrl, opts);
|
|
1476
1092
|
}
|
|
1477
1093
|
|
|
1478
1094
|
/**
|
|
@@ -1484,27 +1100,7 @@ export async function querySessions(address, lcdUrl, opts = {}) {
|
|
|
1484
1100
|
* @returns {object} Flattened session with all fields at top level
|
|
1485
1101
|
*/
|
|
1486
1102
|
export function flattenSession(session) {
|
|
1487
|
-
|
|
1488
|
-
const bs = session.base_session || {};
|
|
1489
|
-
return {
|
|
1490
|
-
id: bs.id || session.id,
|
|
1491
|
-
acc_address: bs.acc_address || session.acc_address,
|
|
1492
|
-
node_address: bs.node_address || bs.node || session.node_address,
|
|
1493
|
-
download_bytes: bs.download_bytes || session.download_bytes || '0',
|
|
1494
|
-
upload_bytes: bs.upload_bytes || session.upload_bytes || '0',
|
|
1495
|
-
max_bytes: bs.max_bytes || session.max_bytes || '0',
|
|
1496
|
-
duration: bs.duration || session.duration,
|
|
1497
|
-
max_duration: bs.max_duration || session.max_duration,
|
|
1498
|
-
status: bs.status || session.status,
|
|
1499
|
-
start_at: bs.start_at || session.start_at,
|
|
1500
|
-
status_at: bs.status_at || session.status_at,
|
|
1501
|
-
inactive_at: bs.inactive_at || session.inactive_at,
|
|
1502
|
-
// Preserve type-specific fields
|
|
1503
|
-
price: session.price || undefined,
|
|
1504
|
-
subscription_id: session.subscription_id || undefined,
|
|
1505
|
-
'@type': session['@type'] || undefined,
|
|
1506
|
-
_raw: session, // original for advanced use
|
|
1507
|
-
};
|
|
1103
|
+
return _flattenSession(session);
|
|
1508
1104
|
}
|
|
1509
1105
|
|
|
1510
1106
|
/**
|
|
@@ -1514,10 +1110,7 @@ export function flattenSession(session) {
|
|
|
1514
1110
|
* @returns {Promise<Subscription|null>}
|
|
1515
1111
|
*/
|
|
1516
1112
|
export async function querySubscription(id, lcdUrl) {
|
|
1517
|
-
|
|
1518
|
-
const data = await lcdQuery(`/sentinel/subscription/v3/subscriptions/${id}`, { lcdUrl });
|
|
1519
|
-
return data.subscription || null;
|
|
1520
|
-
} catch { return null; }
|
|
1113
|
+
return _querySubscription(id, lcdUrl);
|
|
1521
1114
|
}
|
|
1522
1115
|
|
|
1523
1116
|
/**
|
|
@@ -1528,10 +1121,7 @@ export async function querySubscription(id, lcdUrl) {
|
|
|
1528
1121
|
* @returns {Promise<{ has: boolean, subscription?: object }>}
|
|
1529
1122
|
*/
|
|
1530
1123
|
export async function hasActiveSubscription(address, planId, lcdUrl) {
|
|
1531
|
-
|
|
1532
|
-
const match = items.find(s => String(s.plan_id) === String(planId));
|
|
1533
|
-
if (match) return { has: true, subscription: match };
|
|
1534
|
-
return { has: false };
|
|
1124
|
+
return _hasActiveSubscription(address, planId, lcdUrl);
|
|
1535
1125
|
}
|
|
1536
1126
|
|
|
1537
1127
|
// ─── v26c: Display Helpers ───────────────────────────────────────────────────
|
|
@@ -1572,15 +1162,7 @@ export function parseChainDuration(durationStr) {
|
|
|
1572
1162
|
* @returns {Promise<{ items: any[], total: number|null }>}
|
|
1573
1163
|
*/
|
|
1574
1164
|
export async function queryPlanNodes(planId, lcdUrl) {
|
|
1575
|
-
|
|
1576
|
-
// and next_key is always null. Single high-limit request gets all nodes.
|
|
1577
|
-
const doQuery = async (baseUrl) => {
|
|
1578
|
-
const data = await lcd(baseUrl, `/sentinel/node/v3/plans/${planId}/nodes?pagination.limit=5000`);
|
|
1579
|
-
return { items: data.nodes || [], total: (data.nodes || []).length };
|
|
1580
|
-
};
|
|
1581
|
-
if (lcdUrl) return doQuery(lcdUrl);
|
|
1582
|
-
const { result } = await tryWithFallback(LCD_ENDPOINTS, doQuery, `LCD plan ${planId} nodes`);
|
|
1583
|
-
return result;
|
|
1165
|
+
return _queryPlanNodes(planId, lcdUrl);
|
|
1584
1166
|
}
|
|
1585
1167
|
|
|
1586
1168
|
/**
|
|
@@ -1595,33 +1177,7 @@ export async function queryPlanNodes(planId, lcdUrl) {
|
|
|
1595
1177
|
* @returns {Promise<Array<{ id: number, subscribers: number, nodeCount: number, price: object|null, hasNodes: boolean }>>}
|
|
1596
1178
|
*/
|
|
1597
1179
|
export async function discoverPlans(lcdUrl, opts = {}) {
|
|
1598
|
-
|
|
1599
|
-
const batchSize = opts.batchSize || 15;
|
|
1600
|
-
const includeEmpty = opts.includeEmpty || false;
|
|
1601
|
-
const baseLcd = lcdUrl || LCD_ENDPOINTS[0].url;
|
|
1602
|
-
const plans = [];
|
|
1603
|
-
|
|
1604
|
-
for (let batch = 0; batch < Math.ceil(maxId / batchSize); batch++) {
|
|
1605
|
-
const probes = [];
|
|
1606
|
-
for (let i = batch * batchSize + 1; i <= Math.min((batch + 1) * batchSize, maxId); i++) {
|
|
1607
|
-
probes.push((async (id) => {
|
|
1608
|
-
try {
|
|
1609
|
-
const subData = await lcd(baseLcd, `/sentinel/subscription/v3/plans/${id}/subscriptions?pagination.limit=1&pagination.count_total=true`);
|
|
1610
|
-
const subCount = parseInt(subData.pagination?.total || '0', 10);
|
|
1611
|
-
if (subCount === 0 && !includeEmpty) return null;
|
|
1612
|
-
// Plan nodes endpoint has broken pagination (count_total wrong, next_key null).
|
|
1613
|
-
// Use limit=5000 single request and count the actual array.
|
|
1614
|
-
const nodeData = await lcd(baseLcd, `/sentinel/node/v3/plans/${id}/nodes?pagination.limit=5000`);
|
|
1615
|
-
const nodeCount = (nodeData.nodes || []).length;
|
|
1616
|
-
const price = subData.subscriptions?.[0]?.price || null;
|
|
1617
|
-
return { id, subscribers: subCount, nodeCount, price, hasNodes: nodeCount > 0 };
|
|
1618
|
-
} catch { return null; }
|
|
1619
|
-
})(i));
|
|
1620
|
-
}
|
|
1621
|
-
const results = await Promise.all(probes);
|
|
1622
|
-
for (const r of results) if (r) plans.push(r);
|
|
1623
|
-
}
|
|
1624
|
-
return plans.sort((a, b) => a.id - b.id);
|
|
1180
|
+
return _discoverPlans(lcdUrl, opts);
|
|
1625
1181
|
}
|
|
1626
1182
|
|
|
1627
1183
|
/**
|
|
@@ -1709,10 +1265,7 @@ export async function subscribeToPlan(client, fromAddress, planId, denom = 'udvp
|
|
|
1709
1265
|
* @returns {Promise<object|null>}
|
|
1710
1266
|
*/
|
|
1711
1267
|
export async function getProviderByAddress(provAddress, opts = {}) {
|
|
1712
|
-
|
|
1713
|
-
const data = await lcdQuery(`/sentinel/provider/v2/providers/${provAddress}`, opts);
|
|
1714
|
-
return data.provider || null;
|
|
1715
|
-
} catch { return null; }
|
|
1268
|
+
return _getProviderByAddress(provAddress, opts);
|
|
1716
1269
|
}
|
|
1717
1270
|
|
|
1718
1271
|
/**
|
|
@@ -1904,7 +1457,6 @@ export const MSG_TYPES = {
|
|
|
1904
1457
|
// v27: Persistent user settings (backported from C# VpnSettings.cs).
|
|
1905
1458
|
// Stores preferences in ~/.sentinel-sdk/settings.json with restrictive permissions.
|
|
1906
1459
|
|
|
1907
|
-
const SETTINGS_FILE = path.join(os.homedir(), '.sentinel-sdk', 'settings.json');
|
|
1908
1460
|
|
|
1909
1461
|
/**
|
|
1910
1462
|
* Load persisted VPN settings from disk.
|
|
@@ -1912,10 +1464,7 @@ const SETTINGS_FILE = path.join(os.homedir(), '.sentinel-sdk', 'settings.json');
|
|
|
1912
1464
|
* @returns {Record<string, any>}
|
|
1913
1465
|
*/
|
|
1914
1466
|
export function loadVpnSettings() {
|
|
1915
|
-
|
|
1916
|
-
if (!existsSync(SETTINGS_FILE)) return {};
|
|
1917
|
-
return JSON.parse(readFileSync(SETTINGS_FILE, 'utf-8'));
|
|
1918
|
-
} catch { return {}; }
|
|
1467
|
+
return _loadVpnSettings();
|
|
1919
1468
|
}
|
|
1920
1469
|
|
|
1921
1470
|
/**
|
|
@@ -1923,7 +1472,5 @@ export function loadVpnSettings() {
|
|
|
1923
1472
|
* @param {Record<string, any>} settings
|
|
1924
1473
|
*/
|
|
1925
1474
|
export function saveVpnSettings(settings) {
|
|
1926
|
-
|
|
1927
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
1928
|
-
writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), { mode: 0o600 });
|
|
1475
|
+
return _saveVpnSettings(settings);
|
|
1929
1476
|
}
|