blue-js-sdk 2.1.1 → 2.3.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/ai-path/DECISION-TREE.md +85 -41
- package/ai-path/E2E-FLOW.md +153 -1
- package/ai-path/FAILURES.md +7 -0
- package/ai-path/GUIDE.md +98 -0
- package/ai-path/README.md +41 -0
- package/ai-path/connect.js +112 -0
- package/ai-path/pricing.js +5 -4
- package/batch.js +4 -8
- package/chain/fee-grants.js +51 -0
- package/chain/queries.js +327 -61
- package/chain/rpc.js +508 -7
- package/connection/resilience.js +10 -2
- package/cosmjs-setup.js +64 -370
- package/defaults.js +121 -1
- package/index.js +18 -0
- package/node-connect.js +63 -18
- package/package.json +1 -1
- 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
|
|
|
@@ -472,18 +505,7 @@ export async function getBalance(client, address) {
|
|
|
472
505
|
* Note: Sessions have a nested base_session object containing the actual data.
|
|
473
506
|
*/
|
|
474
507
|
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;
|
|
508
|
+
return _findExistingSession(lcdUrl, walletAddr, nodeAddr);
|
|
487
509
|
}
|
|
488
510
|
|
|
489
511
|
/**
|
|
@@ -493,13 +515,7 @@ export async function findExistingSession(lcdUrl, walletAddr, nodeAddr) {
|
|
|
493
515
|
* This handles both formats.
|
|
494
516
|
*/
|
|
495
517
|
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}`;
|
|
518
|
+
return _resolveNodeUrl(node);
|
|
503
519
|
}
|
|
504
520
|
|
|
505
521
|
/**
|
|
@@ -507,11 +523,7 @@ export function resolveNodeUrl(node) {
|
|
|
507
523
|
* Returns array of node objects. Each node has `remote_url` resolved from `remote_addrs`.
|
|
508
524
|
*/
|
|
509
525
|
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;
|
|
526
|
+
return _fetchActiveNodes(lcdUrl, limit, maxPages);
|
|
515
527
|
}
|
|
516
528
|
|
|
517
529
|
/**
|
|
@@ -527,55 +539,7 @@ export async function fetchActiveNodes(lcdUrl, limit = 500, maxPages = 20) {
|
|
|
527
539
|
* console.log(`Average: ${overview.averagePrice.gigabyteDvpn.toFixed(3)} P2P/GB`);
|
|
528
540
|
*/
|
|
529
541
|
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
|
-
};
|
|
542
|
+
return _getNetworkOverview(lcdUrl);
|
|
579
543
|
}
|
|
580
544
|
|
|
581
545
|
/**
|
|
@@ -584,9 +548,7 @@ export async function getNetworkOverview(lcdUrl) {
|
|
|
584
548
|
* Returns sorted array of plan IDs that have at least 1 subscription.
|
|
585
549
|
*/
|
|
586
550
|
export async function discoverPlanIds(lcdUrl, maxId = 500) {
|
|
587
|
-
|
|
588
|
-
const plans = await discoverPlans(lcdUrl, { maxId });
|
|
589
|
-
return plans.map(p => p.id);
|
|
551
|
+
return _discoverPlanIds(lcdUrl, maxId);
|
|
590
552
|
}
|
|
591
553
|
|
|
592
554
|
/**
|
|
@@ -606,29 +568,7 @@ export async function discoverPlanIds(lcdUrl, maxId = 500) {
|
|
|
606
568
|
* // needed by encodeMsgStartSession's max_price field.
|
|
607
569
|
*/
|
|
608
570
|
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
|
-
};
|
|
571
|
+
return _getNodePrices(nodeAddress, lcdUrl);
|
|
632
572
|
}
|
|
633
573
|
|
|
634
574
|
// ─── Display & Serialization Helpers ────────────────────────────────────────
|
|
@@ -861,8 +801,7 @@ export function buildRevokeFeeGrantMsg(granter, grantee) {
|
|
|
861
801
|
* @returns {Promise<Array>} Array of allowance objects
|
|
862
802
|
*/
|
|
863
803
|
export async function queryFeeGrants(lcdUrl, grantee) {
|
|
864
|
-
|
|
865
|
-
return items;
|
|
804
|
+
return _queryFeeGrants(lcdUrl, grantee);
|
|
866
805
|
}
|
|
867
806
|
|
|
868
807
|
/**
|
|
@@ -872,8 +811,7 @@ export async function queryFeeGrants(lcdUrl, grantee) {
|
|
|
872
811
|
* @returns {Promise<Array>}
|
|
873
812
|
*/
|
|
874
813
|
export async function queryFeeGrantsIssued(lcdUrl, granter) {
|
|
875
|
-
|
|
876
|
-
return items;
|
|
814
|
+
return _queryFeeGrantsIssued(lcdUrl, granter);
|
|
877
815
|
}
|
|
878
816
|
|
|
879
817
|
/**
|
|
@@ -881,10 +819,7 @@ export async function queryFeeGrantsIssued(lcdUrl, granter) {
|
|
|
881
819
|
* @returns {Promise<object|null>} Allowance object or null
|
|
882
820
|
*/
|
|
883
821
|
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
|
|
822
|
+
return _queryFeeGrant(lcdUrl, granter, grantee);
|
|
888
823
|
}
|
|
889
824
|
|
|
890
825
|
/**
|
|
@@ -995,8 +930,7 @@ export function encodeForExec(msgs) {
|
|
|
995
930
|
* @returns {Promise<Array>} Array of grant objects
|
|
996
931
|
*/
|
|
997
932
|
export async function queryAuthzGrants(lcdUrl, granter, grantee) {
|
|
998
|
-
|
|
999
|
-
return items;
|
|
933
|
+
return _queryAuthzGrants(lcdUrl, granter, grantee);
|
|
1000
934
|
}
|
|
1001
935
|
|
|
1002
936
|
// ─── LCD Query Helpers (v25b) ────────────────────────────────────────────────
|
|
@@ -1096,20 +1030,7 @@ export async function lcdQueryAll(basePath, opts = {}) {
|
|
|
1096
1030
|
* @returns {Promise<{ subscribers: Array<{ address: string, status: number, id: string }>, total: number|null }>}
|
|
1097
1031
|
*/
|
|
1098
1032
|
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 };
|
|
1033
|
+
return _queryPlanSubscribers(planId, opts);
|
|
1113
1034
|
}
|
|
1114
1035
|
|
|
1115
1036
|
/**
|
|
@@ -1122,14 +1043,7 @@ export async function queryPlanSubscribers(planId, opts = {}) {
|
|
|
1122
1043
|
* @returns {Promise<{ subscriberCount: number, totalOnChain: number, ownerSubscribed: boolean }>}
|
|
1123
1044
|
*/
|
|
1124
1045
|
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
|
-
};
|
|
1046
|
+
return _getPlanStats(planId, ownerAddress, opts);
|
|
1133
1047
|
}
|
|
1134
1048
|
|
|
1135
1049
|
// ─── Fee Grant Workflow Helpers (v25b) ────────────────────────────────────────
|
|
@@ -1146,39 +1060,7 @@ export async function getPlanStats(planId, ownerAddress, opts = {}) {
|
|
|
1146
1060
|
* @returns {Promise<{ msgs: Array, skipped: string[], newGrants: string[] }>} Messages ready for broadcast
|
|
1147
1061
|
*/
|
|
1148
1062
|
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 };
|
|
1063
|
+
return _grantPlanSubscribers(planId, opts);
|
|
1182
1064
|
}
|
|
1183
1065
|
|
|
1184
1066
|
/**
|
|
@@ -1191,34 +1073,7 @@ export async function grantPlanSubscribers(planId, opts = {}) {
|
|
|
1191
1073
|
* @returns {Promise<Array<{ granter: string, grantee: string, expiresAt: Date|null, daysLeft: number|null }>>}
|
|
1192
1074
|
*/
|
|
1193
1075
|
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;
|
|
1076
|
+
return _getExpiringGrants(lcdUrl, granteeOrGranter, withinDays, role);
|
|
1222
1077
|
}
|
|
1223
1078
|
|
|
1224
1079
|
/**
|
|
@@ -1231,18 +1086,7 @@ export async function getExpiringGrants(lcdUrl, granteeOrGranter, withinDays = 7
|
|
|
1231
1086
|
* @returns {Promise<{ msgs: Array, renewed: string[] }>} Messages ready for broadcast
|
|
1232
1087
|
*/
|
|
1233
1088
|
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 };
|
|
1089
|
+
return _renewExpiringGrants(lcdUrl, granterAddress, withinDays, grantOpts);
|
|
1246
1090
|
}
|
|
1247
1091
|
|
|
1248
1092
|
// ─── Fee Grant Monitoring (v25b) ─────────────────────────────────────────────
|
|
@@ -1260,46 +1104,7 @@ export async function renewExpiringGrants(lcdUrl, granterAddress, withinDays = 7
|
|
|
1260
1104
|
* @returns {EventEmitter} Emits 'expiring' and 'expired' events. Call .stop() to stop monitoring.
|
|
1261
1105
|
*/
|
|
1262
1106
|
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;
|
|
1107
|
+
return _monitorFeeGrants(opts);
|
|
1303
1108
|
}
|
|
1304
1109
|
|
|
1305
1110
|
// ─── Query Helpers (v25c) ────────────────────────────────────────────────────
|
|
@@ -1311,10 +1116,7 @@ export function monitorFeeGrants(opts = {}) {
|
|
|
1311
1116
|
* @returns {Promise<{ subscriptions: any[], total: number|null }>}
|
|
1312
1117
|
*/
|
|
1313
1118
|
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' });
|
|
1119
|
+
return _querySubscriptions(lcdUrl, walletAddr, opts);
|
|
1318
1120
|
}
|
|
1319
1121
|
|
|
1320
1122
|
/**
|
|
@@ -1324,20 +1126,7 @@ export async function querySubscriptions(lcdUrl, walletAddr, opts = {}) {
|
|
|
1324
1126
|
* @returns {Promise<{ maxBytes: number, usedBytes: number, remainingBytes: number, percentUsed: number }|null>}
|
|
1325
1127
|
*/
|
|
1326
1128
|
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; }
|
|
1129
|
+
return _querySessionAllocation(lcdUrl, sessionId);
|
|
1341
1130
|
}
|
|
1342
1131
|
|
|
1343
1132
|
/**
|
|
@@ -1350,28 +1139,7 @@ export async function querySessionAllocation(lcdUrl, sessionId) {
|
|
|
1350
1139
|
* @returns {Promise<object>} Node object with remote_url resolved
|
|
1351
1140
|
*/
|
|
1352
1141
|
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;
|
|
1142
|
+
return _queryNode(nodeAddress, opts);
|
|
1375
1143
|
}
|
|
1376
1144
|
|
|
1377
1145
|
/**
|
|
@@ -1467,12 +1235,7 @@ export async function lcdPaginatedSafe(lcdUrl, path, itemsKey, opts = {}) {
|
|
|
1467
1235
|
* @returns {Promise<{ items: ChainSession[], total: number }>}
|
|
1468
1236
|
*/
|
|
1469
1237
|
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;
|
|
1238
|
+
return _querySessions(address, lcdUrl, opts);
|
|
1476
1239
|
}
|
|
1477
1240
|
|
|
1478
1241
|
/**
|
|
@@ -1484,27 +1247,7 @@ export async function querySessions(address, lcdUrl, opts = {}) {
|
|
|
1484
1247
|
* @returns {object} Flattened session with all fields at top level
|
|
1485
1248
|
*/
|
|
1486
1249
|
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
|
-
};
|
|
1250
|
+
return _flattenSession(session);
|
|
1508
1251
|
}
|
|
1509
1252
|
|
|
1510
1253
|
/**
|
|
@@ -1514,10 +1257,7 @@ export function flattenSession(session) {
|
|
|
1514
1257
|
* @returns {Promise<Subscription|null>}
|
|
1515
1258
|
*/
|
|
1516
1259
|
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; }
|
|
1260
|
+
return _querySubscription(id, lcdUrl);
|
|
1521
1261
|
}
|
|
1522
1262
|
|
|
1523
1263
|
/**
|
|
@@ -1528,10 +1268,7 @@ export async function querySubscription(id, lcdUrl) {
|
|
|
1528
1268
|
* @returns {Promise<{ has: boolean, subscription?: object }>}
|
|
1529
1269
|
*/
|
|
1530
1270
|
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 };
|
|
1271
|
+
return _hasActiveSubscription(address, planId, lcdUrl);
|
|
1535
1272
|
}
|
|
1536
1273
|
|
|
1537
1274
|
// ─── v26c: Display Helpers ───────────────────────────────────────────────────
|
|
@@ -1572,15 +1309,7 @@ export function parseChainDuration(durationStr) {
|
|
|
1572
1309
|
* @returns {Promise<{ items: any[], total: number|null }>}
|
|
1573
1310
|
*/
|
|
1574
1311
|
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;
|
|
1312
|
+
return _queryPlanNodes(planId, lcdUrl);
|
|
1584
1313
|
}
|
|
1585
1314
|
|
|
1586
1315
|
/**
|
|
@@ -1595,33 +1324,7 @@ export async function queryPlanNodes(planId, lcdUrl) {
|
|
|
1595
1324
|
* @returns {Promise<Array<{ id: number, subscribers: number, nodeCount: number, price: object|null, hasNodes: boolean }>>}
|
|
1596
1325
|
*/
|
|
1597
1326
|
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);
|
|
1327
|
+
return _discoverPlans(lcdUrl, opts);
|
|
1625
1328
|
}
|
|
1626
1329
|
|
|
1627
1330
|
/**
|
|
@@ -1709,10 +1412,7 @@ export async function subscribeToPlan(client, fromAddress, planId, denom = 'udvp
|
|
|
1709
1412
|
* @returns {Promise<object|null>}
|
|
1710
1413
|
*/
|
|
1711
1414
|
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; }
|
|
1415
|
+
return _getProviderByAddress(provAddress, opts);
|
|
1716
1416
|
}
|
|
1717
1417
|
|
|
1718
1418
|
/**
|
|
@@ -1904,7 +1604,6 @@ export const MSG_TYPES = {
|
|
|
1904
1604
|
// v27: Persistent user settings (backported from C# VpnSettings.cs).
|
|
1905
1605
|
// Stores preferences in ~/.sentinel-sdk/settings.json with restrictive permissions.
|
|
1906
1606
|
|
|
1907
|
-
const SETTINGS_FILE = path.join(os.homedir(), '.sentinel-sdk', 'settings.json');
|
|
1908
1607
|
|
|
1909
1608
|
/**
|
|
1910
1609
|
* Load persisted VPN settings from disk.
|
|
@@ -1912,10 +1611,7 @@ const SETTINGS_FILE = path.join(os.homedir(), '.sentinel-sdk', 'settings.json');
|
|
|
1912
1611
|
* @returns {Record<string, any>}
|
|
1913
1612
|
*/
|
|
1914
1613
|
export function loadVpnSettings() {
|
|
1915
|
-
|
|
1916
|
-
if (!existsSync(SETTINGS_FILE)) return {};
|
|
1917
|
-
return JSON.parse(readFileSync(SETTINGS_FILE, 'utf-8'));
|
|
1918
|
-
} catch { return {}; }
|
|
1614
|
+
return _loadVpnSettings();
|
|
1919
1615
|
}
|
|
1920
1616
|
|
|
1921
1617
|
/**
|
|
@@ -1923,7 +1619,5 @@ export function loadVpnSettings() {
|
|
|
1923
1619
|
* @param {Record<string, any>} settings
|
|
1924
1620
|
*/
|
|
1925
1621
|
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 });
|
|
1622
|
+
return _saveVpnSettings(settings);
|
|
1929
1623
|
}
|