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/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
Every fix made during SDK creation, why it matters, and what happens if you use upstream Sentinel code directly without these fixes.
|
|
4
4
|
|
|
5
|
+
## v2.3.0 — RPC-First Migration (2026-04-14)
|
|
6
|
+
|
|
7
|
+
**100% of chain queries now use RPC-first with LCD fallback.** Protobuf/ABCI queries via Tendermint37Client are ~912x faster than LCD REST. If RPC fails, every query automatically falls back to LCD.
|
|
8
|
+
|
|
9
|
+
### JS SDK Changes
|
|
10
|
+
- **chain/rpc.js**: Added 4 new RPC functions — `rpcQueryFeeGrants`, `rpcQueryFeeGrantsIssued`, `rpcQueryAuthzGrants`, `rpcQueryProvider`
|
|
11
|
+
- **chain/queries.js**: All 22 query functions are RPC-first with LCD fallback
|
|
12
|
+
- **chain/fee-grants.js**: All 7 functions are RPC-first with LCD fallback
|
|
13
|
+
- **cosmjs-setup.js**: All 28 query bodies replaced with thin wrappers delegating to RPC-first modules
|
|
14
|
+
- **session-manager.js**: `buildSessionMap()` now uses RPC-first `querySessions()`
|
|
15
|
+
- **batch.js**: `waitForBatchSessions()` now uses RPC-first `querySessions()`
|
|
16
|
+
- **defaults.js**: Added runtime endpoint management — `addRpcEndpoint`, `removeRpcEndpoint`, `setEndpoints`, `getEndpoints`, `checkRpcEndpointHealth`, `optimizeEndpoints`
|
|
17
|
+
- **index.js**: 16 RPC query exports + 8 endpoint management exports
|
|
18
|
+
- **SDK_VERSION**: Bumped to 2.3.0
|
|
19
|
+
|
|
20
|
+
### C# SDK Changes
|
|
21
|
+
- **RpcClient.cs**: Wired into ChainClient. 17 typed query methods (sessions, subscriptions, nodes, balance, provider, fee grants, authz, allocations)
|
|
22
|
+
- **ProtobufReader.cs**: Added `DecodeSession`, `DecodeSubscription`, `DecodeProvider` decoders
|
|
23
|
+
- **ChainClient.Queries.cs**: 13 methods upgraded to RPC-first with LCD fallback
|
|
24
|
+
- **ChainClient.FeeGrants.cs**: 2 methods upgraded to RPC-first with LCD fallback
|
|
25
|
+
- **Total**: 15 direct + 9 transitive = 24 query methods are RPC-first
|
|
26
|
+
|
|
27
|
+
### Coverage
|
|
28
|
+
| Module | RPC-First | Total |
|
|
29
|
+
|--------|-----------|-------|
|
|
30
|
+
| JS chain/queries.js | 22/22 | 100% |
|
|
31
|
+
| JS chain/fee-grants.js | 7/7 | 100% |
|
|
32
|
+
| JS session-manager.js | 1/1 | 100% |
|
|
33
|
+
| JS batch.js | 1/1 | 100% |
|
|
34
|
+
| C# ChainClient.Queries | 13/13 | 100% |
|
|
35
|
+
| C# ChainClient.FeeGrants | 2/2 | 100% |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
5
39
|
## Documentation Versions
|
|
6
40
|
|
|
7
41
|
| Version | Date | Changes |
|
package/batch.js
CHANGED
|
@@ -25,14 +25,13 @@
|
|
|
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,
|
|
32
|
-
findExistingSession,
|
|
33
|
-
lcdPaginatedSafe,
|
|
34
32
|
MSG_TYPES,
|
|
35
33
|
} from './cosmjs-setup.js';
|
|
34
|
+
import { findExistingSession, querySessions } from './chain/queries.js';
|
|
36
35
|
|
|
37
36
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
38
37
|
|
|
@@ -247,7 +246,7 @@ async function _processBatch(client, account, batch, gigabytes, denom, ctx) {
|
|
|
247
246
|
export async function waitForBatchSessions(nodeAddrs, walletAddr, lcdUrl, options = {}) {
|
|
248
247
|
const maxWaitMs = options.maxWaitMs ?? DEFAULT_POLL_TIMEOUT;
|
|
249
248
|
const pollIntervalMs = options.pollIntervalMs ?? 2000;
|
|
250
|
-
const baseLcd = lcdUrl ||
|
|
249
|
+
const baseLcd = lcdUrl || DEFAULT_LCD;
|
|
251
250
|
|
|
252
251
|
if (nodeAddrs.length === 0) return { confirmed: [], pending: [] };
|
|
253
252
|
|
|
@@ -257,18 +256,15 @@ export async function waitForBatchSessions(nodeAddrs, walletAddr, lcdUrl, option
|
|
|
257
256
|
while (pending.size > 0 && Date.now() < deadline) {
|
|
258
257
|
await sleep(pollIntervalMs);
|
|
259
258
|
try {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
`/sentinel/session/v3/sessions?address=${walletAddr}&status=1`,
|
|
263
|
-
'sessions',
|
|
264
|
-
);
|
|
259
|
+
// RPC-first via chain/queries.js — returns flattened sessions
|
|
260
|
+
const result = await querySessions(walletAddr, baseLcd, { status: '1' });
|
|
265
261
|
for (const s of (result.items || [])) {
|
|
266
262
|
const bs = s.base_session || s;
|
|
267
263
|
const n = bs.node_address || bs.node;
|
|
268
264
|
if (pending.has(n)) pending.delete(n);
|
|
269
265
|
}
|
|
270
266
|
} catch {
|
|
271
|
-
// Transient LCD error — will retry on next poll
|
|
267
|
+
// Transient RPC/LCD error — will retry on next poll
|
|
272
268
|
}
|
|
273
269
|
}
|
|
274
270
|
|
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,9 +13,15 @@ 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
|
+
import {
|
|
20
|
+
createRpcQueryClientWithFallback,
|
|
21
|
+
rpcQueryFeeGrant as _rpcQueryFeeGrant,
|
|
22
|
+
rpcQueryFeeGrants as _rpcQueryFeeGrants,
|
|
23
|
+
rpcQueryFeeGrantsIssued as _rpcQueryFeeGrantsIssued,
|
|
24
|
+
} from './rpc.js';
|
|
19
25
|
|
|
20
26
|
// ─── Protobuf Helpers for FeeGrant ──────────────────────────────────────────
|
|
21
27
|
// Uses the same manual protobuf encoding as Sentinel types — no codegen needed.
|
|
@@ -60,6 +66,21 @@ function encodeAny(typeUrl, valueBytes) {
|
|
|
60
66
|
]);
|
|
61
67
|
}
|
|
62
68
|
|
|
69
|
+
// ─── RPC Client Helper ─────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
let _rpcClient = null;
|
|
72
|
+
let _rpcClientPromise = null;
|
|
73
|
+
|
|
74
|
+
async function getRpcClient() {
|
|
75
|
+
if (_rpcClient) return _rpcClient;
|
|
76
|
+
if (_rpcClientPromise) return _rpcClientPromise;
|
|
77
|
+
_rpcClientPromise = createRpcQueryClientWithFallback()
|
|
78
|
+
.then(client => { _rpcClient = client; return client; })
|
|
79
|
+
.catch(() => { _rpcClient = null; return null; })
|
|
80
|
+
.finally(() => { _rpcClientPromise = null; });
|
|
81
|
+
return _rpcClientPromise;
|
|
82
|
+
}
|
|
83
|
+
|
|
63
84
|
// ─── FeeGrant (cosmos.feegrant.v1beta1) ─────────────────────────────────────
|
|
64
85
|
|
|
65
86
|
/**
|
|
@@ -105,31 +126,61 @@ export function buildRevokeFeeGrantMsg(granter, grantee) {
|
|
|
105
126
|
|
|
106
127
|
/**
|
|
107
128
|
* Query fee grants given to a grantee.
|
|
129
|
+
* RPC-first with LCD fallback.
|
|
108
130
|
* @returns {Promise<Array>} Array of allowance objects
|
|
109
131
|
*/
|
|
110
132
|
export async function queryFeeGrants(lcdUrl, grantee) {
|
|
133
|
+
// RPC-first
|
|
134
|
+
try {
|
|
135
|
+
const rpc = await getRpcClient();
|
|
136
|
+
if (rpc) {
|
|
137
|
+
return await _rpcQueryFeeGrants(rpc, grantee);
|
|
138
|
+
}
|
|
139
|
+
} catch { /* fall through to LCD */ }
|
|
140
|
+
|
|
141
|
+
// LCD fallback
|
|
111
142
|
const { items } = await lcdPaginatedSafe(lcdUrl, `/cosmos/feegrant/v1beta1/allowances/${grantee}`, 'allowances');
|
|
112
143
|
return items;
|
|
113
144
|
}
|
|
114
145
|
|
|
115
146
|
/**
|
|
116
147
|
* Query fee grants issued BY an address (where addr is the granter).
|
|
148
|
+
* RPC-first with LCD fallback.
|
|
117
149
|
* @param {string} lcdUrl
|
|
118
150
|
* @param {string} granter - Address that issued the grants
|
|
119
151
|
* @returns {Promise<Array>}
|
|
120
152
|
*/
|
|
121
153
|
export async function queryFeeGrantsIssued(lcdUrl, granter) {
|
|
154
|
+
// RPC-first
|
|
155
|
+
try {
|
|
156
|
+
const rpc = await getRpcClient();
|
|
157
|
+
if (rpc) {
|
|
158
|
+
return await _rpcQueryFeeGrantsIssued(rpc, granter);
|
|
159
|
+
}
|
|
160
|
+
} catch { /* fall through to LCD */ }
|
|
161
|
+
|
|
162
|
+
// LCD fallback
|
|
122
163
|
const { items } = await lcdPaginatedSafe(lcdUrl, `/cosmos/feegrant/v1beta1/issued/${granter}`, 'allowances');
|
|
123
164
|
return items;
|
|
124
165
|
}
|
|
125
166
|
|
|
126
167
|
/**
|
|
127
168
|
* Query a specific fee grant between granter and grantee.
|
|
169
|
+
* RPC-first with LCD fallback.
|
|
128
170
|
* @returns {Promise<object|null>} Allowance object or null
|
|
129
171
|
*/
|
|
130
172
|
export async function queryFeeGrant(lcdUrl, granter, grantee) {
|
|
173
|
+
// RPC-first
|
|
174
|
+
try {
|
|
175
|
+
const rpc = await getRpcClient();
|
|
176
|
+
if (rpc) {
|
|
177
|
+
return await _rpcQueryFeeGrant(rpc, granter, grantee);
|
|
178
|
+
}
|
|
179
|
+
} catch { /* fall through to LCD */ }
|
|
180
|
+
|
|
181
|
+
// LCD fallback (with endpoint failover via lcdQuery)
|
|
131
182
|
try {
|
|
132
|
-
const data = await
|
|
183
|
+
const data = await lcdQuery(`/cosmos/feegrant/v1beta1/allowance/${granter}/${grantee}`, { lcdUrl });
|
|
133
184
|
return data.allowance || null;
|
|
134
185
|
} catch { return null; } // 404 = no grant
|
|
135
186
|
}
|
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,10 @@ 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.
|
|
348
|
-
*
|
|
349
|
-
* Note: Sessions have a nested base_session object containing the actual data.
|
|
361
|
+
* Delegates to chain/queries.js RPC-first implementation.
|
|
350
362
|
*/
|
|
351
363
|
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;
|
|
364
|
+
return _rpcFindExistingSession(lcdUrl, walletAddr, nodeAddr);
|
|
361
365
|
}
|
|
362
366
|
|
|
363
367
|
/**
|
|
@@ -377,176 +381,44 @@ export function resolveNodeUrl(node) {
|
|
|
377
381
|
}
|
|
378
382
|
|
|
379
383
|
/**
|
|
380
|
-
* Fetch all active nodes
|
|
384
|
+
* Fetch all active nodes with RPC-first + LCD fallback.
|
|
381
385
|
* Returns array of node objects. Each node has `remote_url` resolved from `remote_addrs`.
|
|
386
|
+
* Delegates to chain/queries.js RPC-first implementation.
|
|
382
387
|
*/
|
|
383
388
|
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;
|
|
389
|
+
return _rpcFetchActiveNodes(lcdUrl, limit, maxPages);
|
|
399
390
|
}
|
|
400
391
|
|
|
401
392
|
/**
|
|
402
393
|
* Get a quick network overview — total nodes, counts by country and service type, average prices.
|
|
403
|
-
*
|
|
394
|
+
* Delegates to chain/queries.js RPC-first implementation.
|
|
404
395
|
*
|
|
405
396
|
* @param {string} [lcdUrl] - LCD endpoint (default: cascading fallback)
|
|
406
397
|
* @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
398
|
*/
|
|
413
399
|
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
|
-
};
|
|
400
|
+
return _rpcGetNetworkOverview(lcdUrl);
|
|
464
401
|
}
|
|
465
402
|
|
|
466
403
|
/**
|
|
467
404
|
* Discover plan IDs by probing subscription endpoints.
|
|
468
|
-
*
|
|
405
|
+
* Delegates to chain/queries.js RPC-first implementation.
|
|
469
406
|
* Returns sorted array of plan IDs that have at least 1 subscription.
|
|
470
407
|
*/
|
|
471
408
|
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);
|
|
409
|
+
return _rpcDiscoverPlanIds(lcdUrl, maxId);
|
|
486
410
|
}
|
|
487
411
|
|
|
488
412
|
/**
|
|
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.
|
|
413
|
+
* Get standardized prices for a node — abstracts V3 price parsing entirely.
|
|
414
|
+
* Delegates to chain/queries.js RPC-first implementation (direct node lookup, not full-list scan).
|
|
493
415
|
*
|
|
494
416
|
* @param {string} nodeAddress - sentnode1... address
|
|
495
417
|
* @param {string} [lcdUrl] - LCD endpoint URL (default: cascading fallback across all endpoints)
|
|
496
418
|
* @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
419
|
*/
|
|
504
420
|
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
|
-
};
|
|
421
|
+
return _rpcGetNodePrices(nodeAddress, lcdUrl);
|
|
550
422
|
}
|
|
551
423
|
|
|
552
424
|
// ─── Display & Serialization Helpers ────────────────────────────────────────
|
|
@@ -761,22 +633,20 @@ export function buildRevokeFeeGrantMsg(granter, grantee) {
|
|
|
761
633
|
|
|
762
634
|
/**
|
|
763
635
|
* Query fee grants given to a grantee.
|
|
636
|
+
* Delegates to chain/fee-grants.js RPC-first implementation.
|
|
764
637
|
* @returns {Promise<Array>} Array of allowance objects
|
|
765
638
|
*/
|
|
766
639
|
export async function queryFeeGrants(lcdUrl, grantee) {
|
|
767
|
-
|
|
768
|
-
return data.allowances || [];
|
|
640
|
+
return _rpcQueryFeeGrants(lcdUrl, grantee);
|
|
769
641
|
}
|
|
770
642
|
|
|
771
643
|
/**
|
|
772
644
|
* Query a specific fee grant between granter and grantee.
|
|
645
|
+
* Delegates to chain/fee-grants.js RPC-first implementation.
|
|
773
646
|
* @returns {Promise<object|null>} Allowance object or null
|
|
774
647
|
*/
|
|
775
648
|
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
|
|
649
|
+
return _rpcQueryFeeGrant(lcdUrl, granter, grantee);
|
|
780
650
|
}
|
|
781
651
|
|
|
782
652
|
/**
|
|
@@ -878,14 +748,7 @@ export function encodeForExec(msgs) {
|
|
|
878
748
|
});
|
|
879
749
|
}
|
|
880
750
|
|
|
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
|
-
}
|
|
751
|
+
// queryAuthzGrants removed — use RPC-first version from chain/queries.js
|
|
889
752
|
|
|
890
753
|
// Re-export extractSessionId for convenience (from protocol module)
|
|
891
754
|
export { extractSessionId };
|
package/chain/queries.js
CHANGED
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
rpcQuerySubscriptionAllocations as rpcQuerySubAllocations,
|
|
31
31
|
rpcQueryPlan,
|
|
32
32
|
rpcQueryBalance,
|
|
33
|
+
rpcQueryProvider as _rpcQueryProvider,
|
|
34
|
+
rpcQueryAuthzGrants as _rpcQueryAuthzGrants,
|
|
33
35
|
} from './rpc.js';
|
|
34
36
|
|
|
35
37
|
// Re-export for convenience
|
|
@@ -54,6 +56,15 @@ async function getRpcClient() {
|
|
|
54
56
|
return _rpcClientPromise;
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Clear the cached RPC query client. Called during process cleanup
|
|
61
|
+
* to ensure WebSocket connections are properly closed.
|
|
62
|
+
*/
|
|
63
|
+
export function resetQueryRpcCache() {
|
|
64
|
+
_rpcClient = null;
|
|
65
|
+
_rpcClientPromise = null;
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
// ─── Query Helpers ───────────────────────────────────────────────────────────
|
|
58
69
|
|
|
59
70
|
/**
|
|
@@ -342,9 +353,9 @@ export async function querySessionById(lcdUrl, sessionId) {
|
|
|
342
353
|
}
|
|
343
354
|
} catch { /* fall through to LCD */ }
|
|
344
355
|
|
|
345
|
-
// LCD fallback
|
|
356
|
+
// LCD fallback (with endpoint failover via lcdQuery)
|
|
346
357
|
try {
|
|
347
|
-
const data = await
|
|
358
|
+
const data = await lcdQuery(`/sentinel/session/v3/sessions/${sessionId}`, { lcdUrl });
|
|
348
359
|
const raw = data?.session;
|
|
349
360
|
if (!raw) return null;
|
|
350
361
|
return flattenSession(raw);
|
|
@@ -370,9 +381,9 @@ export async function querySessionAllocation(lcdUrl, sessionId) {
|
|
|
370
381
|
} catch { /* fall through */ }
|
|
371
382
|
|
|
372
383
|
if (!s) {
|
|
373
|
-
// LCD fallback
|
|
384
|
+
// LCD fallback (with endpoint failover via lcdQuery)
|
|
374
385
|
try {
|
|
375
|
-
const data = await
|
|
386
|
+
const data = await lcdQuery(`/sentinel/session/v3/sessions/${sessionId}`, { lcdUrl });
|
|
376
387
|
s = data.session?.base_session || data.session || null;
|
|
377
388
|
} catch { return null; }
|
|
378
389
|
}
|
|
@@ -674,7 +685,7 @@ export async function queryPlanNodes(planId, lcdUrl) {
|
|
|
674
685
|
|
|
675
686
|
/**
|
|
676
687
|
* Discover all available plans with metadata (subscriber count, node count, price).
|
|
677
|
-
*
|
|
688
|
+
* RPC-first: probes plan IDs via rpcQueryPlan, falls back to LCD.
|
|
678
689
|
*
|
|
679
690
|
* @param {string} [lcdUrl]
|
|
680
691
|
* @param {object} [opts]
|
|
@@ -690,19 +701,61 @@ export async function discoverPlans(lcdUrl, opts = {}) {
|
|
|
690
701
|
const baseLcd = lcdUrl || LCD_ENDPOINTS[0].url;
|
|
691
702
|
const plans = [];
|
|
692
703
|
|
|
704
|
+
// Try to get RPC client for plan queries
|
|
705
|
+
let rpc = null;
|
|
706
|
+
try { rpc = await getRpcClient(); } catch { /* LCD only */ }
|
|
707
|
+
|
|
693
708
|
for (let batch = 0; batch < Math.ceil(maxId / batchSize); batch++) {
|
|
694
709
|
const probes = [];
|
|
695
710
|
for (let i = batch * batchSize + 1; i <= Math.min((batch + 1) * batchSize, maxId); i++) {
|
|
696
711
|
probes.push((async (id) => {
|
|
697
712
|
try {
|
|
698
|
-
|
|
699
|
-
|
|
713
|
+
// RPC-first: query plan existence via RPC
|
|
714
|
+
let plan = null;
|
|
715
|
+
if (rpc) {
|
|
716
|
+
try { plan = await rpcQueryPlan(rpc, id); } catch { /* fall through */ }
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Get subscriber count — RPC for plan subs, LCD as fallback
|
|
720
|
+
let subCount = 0;
|
|
721
|
+
let price = null;
|
|
722
|
+
if (rpc) {
|
|
723
|
+
try {
|
|
724
|
+
const subs = await rpcQuerySubscriptionsForPlan(rpc, id, { limit: 1 });
|
|
725
|
+
subCount = subs.length; // Quick check — at least 1
|
|
726
|
+
price = subs[0]?.price || null;
|
|
727
|
+
// If RPC returned subs, plan exists even if rpcQueryPlan returned null
|
|
728
|
+
if (subCount > 0 && !plan) plan = { id };
|
|
729
|
+
} catch { /* fall through */ }
|
|
730
|
+
}
|
|
731
|
+
if (!plan && subCount === 0) {
|
|
732
|
+
// LCD fallback for subscriber count
|
|
733
|
+
try {
|
|
734
|
+
const subData = await lcd(baseLcd, `/sentinel/subscription/v3/plans/${id}/subscriptions?pagination.limit=1&pagination.count_total=true`);
|
|
735
|
+
subCount = parseInt(subData.pagination?.total || '0', 10);
|
|
736
|
+
price = subData.subscriptions?.[0]?.price || null;
|
|
737
|
+
if (subCount > 0) plan = { id };
|
|
738
|
+
} catch { /* plan doesn't exist */ }
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (!plan && !includeEmpty) return null;
|
|
700
742
|
if (subCount === 0 && !includeEmpty) return null;
|
|
701
|
-
|
|
702
|
-
//
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
743
|
+
|
|
744
|
+
// Get node count — RPC-first
|
|
745
|
+
let nodeCount = 0;
|
|
746
|
+
if (rpc) {
|
|
747
|
+
try {
|
|
748
|
+
const nodes = await rpcQueryNodesForPlan(rpc, id, { status: 1, limit: 5000 });
|
|
749
|
+
nodeCount = nodes.length;
|
|
750
|
+
} catch { /* fall through to LCD */ }
|
|
751
|
+
}
|
|
752
|
+
if (nodeCount === 0) {
|
|
753
|
+
try {
|
|
754
|
+
const nodeData = await lcd(baseLcd, `/sentinel/node/v3/plans/${id}/nodes?pagination.limit=5000`);
|
|
755
|
+
nodeCount = (nodeData.nodes || []).length;
|
|
756
|
+
} catch { /* no nodes */ }
|
|
757
|
+
}
|
|
758
|
+
|
|
706
759
|
return { id, subscribers: subCount, nodeCount, price, hasNodes: nodeCount > 0 };
|
|
707
760
|
} catch { return null; }
|
|
708
761
|
})(i));
|
|
@@ -716,18 +769,51 @@ export async function discoverPlans(lcdUrl, opts = {}) {
|
|
|
716
769
|
|
|
717
770
|
/**
|
|
718
771
|
* Get provider details by address.
|
|
772
|
+
* RPC-first with LCD fallback. Provider is still v2 on chain.
|
|
719
773
|
* @param {string} provAddress - sentprov1... address
|
|
720
774
|
* @param {object} [opts]
|
|
721
775
|
* @param {string} [opts.lcdUrl]
|
|
722
776
|
* @returns {Promise<object|null>}
|
|
723
777
|
*/
|
|
724
778
|
export async function getProviderByAddress(provAddress, opts = {}) {
|
|
779
|
+
// RPC-first
|
|
780
|
+
try {
|
|
781
|
+
const rpc = await getRpcClient();
|
|
782
|
+
if (rpc) {
|
|
783
|
+
const provider = await _rpcQueryProvider(rpc, provAddress);
|
|
784
|
+
if (provider) return provider;
|
|
785
|
+
}
|
|
786
|
+
} catch { /* fall through to LCD */ }
|
|
787
|
+
|
|
788
|
+
// LCD fallback
|
|
725
789
|
try {
|
|
726
790
|
const data = await lcdQuery(`/sentinel/provider/v2/providers/${provAddress}`, opts);
|
|
727
791
|
return data.provider || null;
|
|
728
792
|
} catch { return null; }
|
|
729
793
|
}
|
|
730
794
|
|
|
795
|
+
/**
|
|
796
|
+
* Query authz grants between granter and grantee.
|
|
797
|
+
* RPC-first with LCD fallback.
|
|
798
|
+
* @param {string} lcdUrl - LCD endpoint
|
|
799
|
+
* @param {string} granter - Granter address (sent1...)
|
|
800
|
+
* @param {string} grantee - Grantee address (sent1...)
|
|
801
|
+
* @returns {Promise<Array>}
|
|
802
|
+
*/
|
|
803
|
+
export async function queryAuthzGrants(lcdUrl, granter, grantee) {
|
|
804
|
+
// RPC-first
|
|
805
|
+
try {
|
|
806
|
+
const rpc = await getRpcClient();
|
|
807
|
+
if (rpc) {
|
|
808
|
+
return await _rpcQueryAuthzGrants(rpc, granter, grantee);
|
|
809
|
+
}
|
|
810
|
+
} catch { /* fall through to LCD */ }
|
|
811
|
+
|
|
812
|
+
// LCD fallback
|
|
813
|
+
const { items } = await lcdPaginatedSafe(lcdUrl, `/cosmos/authz/v1beta1/grants?granter=${granter}&grantee=${grantee}`, 'grants');
|
|
814
|
+
return items;
|
|
815
|
+
}
|
|
816
|
+
|
|
731
817
|
// ─── VPN Settings Persistence ────────────────────────────────────────────────
|
|
732
818
|
// v27: Persistent user settings (backported from C# VpnSettings.cs).
|
|
733
819
|
// Stores preferences in ~/.sentinel-sdk/settings.json with restrictive permissions.
|