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/chain/queries.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Sentinel SDK — Chain / Queries Module
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* RPC-first query functions with LCD fallback.
|
|
5
|
+
* All queries try RPC (protobuf via CosmJS ABCI) first for speed (~912x faster),
|
|
6
|
+
* then fall back to LCD (REST) if RPC fails.
|
|
6
7
|
*
|
|
7
8
|
* Usage:
|
|
8
9
|
* import { getBalance, fetchActiveNodes, queryNode } from './chain/queries.js';
|
|
@@ -16,10 +17,45 @@ import { LCD_ENDPOINTS, tryWithFallback } from '../defaults.js';
|
|
|
16
17
|
import { ValidationError, NodeError, ChainError, ErrorCodes } from '../errors.js';
|
|
17
18
|
import { extractSessionId } from '../v3protocol.js';
|
|
18
19
|
import { lcd, lcdQuery, lcdQueryAll, lcdPaginatedSafe } from './lcd.js';
|
|
20
|
+
import {
|
|
21
|
+
createRpcQueryClientWithFallback,
|
|
22
|
+
rpcQueryNodes,
|
|
23
|
+
rpcQueryNode,
|
|
24
|
+
rpcQueryNodesForPlan,
|
|
25
|
+
rpcQuerySession,
|
|
26
|
+
rpcQuerySessionsForAccount,
|
|
27
|
+
rpcQuerySubscription,
|
|
28
|
+
rpcQuerySubscriptionsForAccount,
|
|
29
|
+
rpcQuerySubscriptionsForPlan,
|
|
30
|
+
rpcQuerySubscriptionAllocations as rpcQuerySubAllocations,
|
|
31
|
+
rpcQueryPlan,
|
|
32
|
+
rpcQueryBalance,
|
|
33
|
+
rpcQueryProvider as _rpcQueryProvider,
|
|
34
|
+
rpcQueryAuthzGrants as _rpcQueryAuthzGrants,
|
|
35
|
+
} from './rpc.js';
|
|
19
36
|
|
|
20
37
|
// Re-export for convenience
|
|
21
38
|
export { extractSessionId };
|
|
22
39
|
|
|
40
|
+
// ─── RPC Client Helper ─────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
let _rpcClient = null;
|
|
43
|
+
let _rpcClientPromise = null;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get or create a cached RPC query client. Returns null if all RPC endpoints fail
|
|
47
|
+
* (caller should fall back to LCD).
|
|
48
|
+
*/
|
|
49
|
+
async function getRpcClient() {
|
|
50
|
+
if (_rpcClient) return _rpcClient;
|
|
51
|
+
if (_rpcClientPromise) return _rpcClientPromise;
|
|
52
|
+
_rpcClientPromise = createRpcQueryClientWithFallback()
|
|
53
|
+
.then(client => { _rpcClient = client; return client; })
|
|
54
|
+
.catch(() => { _rpcClient = null; return null; })
|
|
55
|
+
.finally(() => { _rpcClientPromise = null; });
|
|
56
|
+
return _rpcClientPromise;
|
|
57
|
+
}
|
|
58
|
+
|
|
23
59
|
// ─── Query Helpers ───────────────────────────────────────────────────────────
|
|
24
60
|
|
|
25
61
|
/**
|
|
@@ -35,20 +71,38 @@ export async function getBalance(client, address) {
|
|
|
35
71
|
/**
|
|
36
72
|
* Find an existing active session for a wallet+node pair.
|
|
37
73
|
* Returns session ID (BigInt) or null. Use this to avoid double-paying.
|
|
38
|
-
*
|
|
39
|
-
* Note: Sessions have a nested base_session object containing the actual data.
|
|
74
|
+
* RPC-first with LCD fallback.
|
|
40
75
|
*/
|
|
41
76
|
export async function findExistingSession(lcdUrl, walletAddr, nodeAddr) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
77
|
+
let sessions;
|
|
78
|
+
|
|
79
|
+
// RPC-first: returns decoded, flat session objects
|
|
80
|
+
try {
|
|
81
|
+
const rpc = await getRpcClient();
|
|
82
|
+
if (rpc) {
|
|
83
|
+
sessions = await rpcQuerySessionsForAccount(rpc, walletAddr, { limit: 500 });
|
|
84
|
+
}
|
|
85
|
+
} catch { /* RPC failed, fall through to LCD */ }
|
|
86
|
+
|
|
87
|
+
if (!sessions) {
|
|
88
|
+
// LCD fallback
|
|
89
|
+
const { items } = await lcdPaginatedSafe(lcdUrl, `/sentinel/session/v3/sessions?address=${walletAddr}&status=1`, 'sessions');
|
|
90
|
+
sessions = items.map(s => {
|
|
91
|
+
const bs = s.base_session || s;
|
|
92
|
+
return { ...bs, price: s.price, subscription_id: s.subscription_id };
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const s of sessions) {
|
|
97
|
+
if ((s.node_address || s.node) !== nodeAddr) continue;
|
|
98
|
+
// RPC returns status as number (1=active), LCD as string
|
|
99
|
+
const st = s.status;
|
|
100
|
+
if (st && st !== 1 && st !== 'active') continue;
|
|
101
|
+
const acct = s.acc_address || s.address;
|
|
48
102
|
if (acct && acct !== walletAddr) continue;
|
|
49
|
-
const maxBytes = parseInt(
|
|
50
|
-
const used = parseInt(
|
|
51
|
-
if (maxBytes === 0 || used < maxBytes) return BigInt(
|
|
103
|
+
const maxBytes = parseInt(s.max_bytes || '0');
|
|
104
|
+
const used = parseInt(s.download_bytes || '0') + parseInt(s.upload_bytes || '0');
|
|
105
|
+
if (maxBytes === 0 || used < maxBytes) return BigInt(s.id);
|
|
52
106
|
}
|
|
53
107
|
return null;
|
|
54
108
|
}
|
|
@@ -80,7 +134,7 @@ const NODE_CACHE_TTL = 5 * 60_000; // 5 minutes
|
|
|
80
134
|
export function invalidateNodeCache() { _nodeListCache = null; }
|
|
81
135
|
|
|
82
136
|
/**
|
|
83
|
-
* Fetch all active nodes
|
|
137
|
+
* Fetch all active nodes via RPC (primary) with LCD fallback.
|
|
84
138
|
* Returns array of node objects. Each node has:
|
|
85
139
|
* - `remote_url`: the first usable HTTPS URL (for primary connection)
|
|
86
140
|
* - `remoteAddrs`: ALL remote addresses (for fallback on connection failure)
|
|
@@ -93,7 +147,21 @@ export async function fetchActiveNodes(lcdUrl, limit = 500, maxPages = 20) {
|
|
|
93
147
|
return _nodeListCache.map(n => ({ ...n, planIds: [...(n.planIds || [])] }));
|
|
94
148
|
}
|
|
95
149
|
|
|
96
|
-
|
|
150
|
+
let items;
|
|
151
|
+
try {
|
|
152
|
+
// RPC-first: ~912x faster than LCD for bulk node queries
|
|
153
|
+
const rpc = await getRpcClient();
|
|
154
|
+
if (rpc) {
|
|
155
|
+
items = await rpcQueryNodes(rpc, { status: 1, limit });
|
|
156
|
+
}
|
|
157
|
+
} catch { /* RPC failed, fall through to LCD */ }
|
|
158
|
+
|
|
159
|
+
if (!items) {
|
|
160
|
+
// LCD fallback
|
|
161
|
+
const result = await lcdPaginatedSafe(lcdUrl, '/sentinel/node/v3/nodes?status=1', 'nodes', { limit });
|
|
162
|
+
items = result.items;
|
|
163
|
+
}
|
|
164
|
+
|
|
97
165
|
for (const n of items) {
|
|
98
166
|
// Preserve ALL remote addresses for fallback
|
|
99
167
|
const addrs = n.remote_addrs || [];
|
|
@@ -228,21 +296,35 @@ export async function getNodePrices(nodeAddress, lcdUrl) {
|
|
|
228
296
|
|
|
229
297
|
/**
|
|
230
298
|
* Query a wallet's active subscriptions.
|
|
299
|
+
* RPC-first with LCD fallback.
|
|
231
300
|
* @param {string} lcdUrl
|
|
232
301
|
* @param {string} walletAddr - sent1... address
|
|
233
|
-
* @returns {Promise<{
|
|
302
|
+
* @returns {Promise<{ items: any[], total: number|null }>}
|
|
234
303
|
*/
|
|
235
304
|
export async function querySubscriptions(lcdUrl, walletAddr, opts = {}) {
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
305
|
+
// RPC-first
|
|
306
|
+
try {
|
|
307
|
+
const rpc = await getRpcClient();
|
|
308
|
+
if (rpc) {
|
|
309
|
+
let subs = await rpcQuerySubscriptionsForAccount(rpc, walletAddr, { limit: 500 });
|
|
310
|
+
if (opts.status) {
|
|
311
|
+
const statusNum = opts.status === 'active' ? 1 : 2;
|
|
312
|
+
subs = subs.filter(s => s.status === statusNum);
|
|
313
|
+
}
|
|
314
|
+
return { items: subs, total: subs.length };
|
|
315
|
+
}
|
|
316
|
+
} catch { /* fall through to LCD */ }
|
|
317
|
+
|
|
318
|
+
// LCD fallback
|
|
319
|
+
let lcdPath = `/sentinel/subscription/v3/accounts/${walletAddr}/subscriptions`;
|
|
320
|
+
if (opts.status) lcdPath += `?status=${opts.status === 'active' ? '1' : '2'}`;
|
|
321
|
+
return lcdQueryAll(lcdPath, { lcdUrl, dataKey: 'subscriptions' });
|
|
240
322
|
}
|
|
241
323
|
|
|
242
324
|
/**
|
|
243
325
|
* Query a single session directly by ID — O(1) instead of scanning all wallet sessions.
|
|
244
326
|
* Returns the flattened session object or null if not found.
|
|
245
|
-
*
|
|
327
|
+
* RPC-first with LCD fallback.
|
|
246
328
|
*
|
|
247
329
|
* @param {string} lcdUrl - LCD endpoint URL
|
|
248
330
|
* @param {string|number|bigint} sessionId - Session ID to query
|
|
@@ -253,6 +335,16 @@ export async function querySubscriptions(lcdUrl, walletAddr, opts = {}) {
|
|
|
253
335
|
* if (session) console.log(`Session ${session.id} on node ${session.node_address}`);
|
|
254
336
|
*/
|
|
255
337
|
export async function querySessionById(lcdUrl, sessionId) {
|
|
338
|
+
// RPC-first
|
|
339
|
+
try {
|
|
340
|
+
const rpc = await getRpcClient();
|
|
341
|
+
if (rpc) {
|
|
342
|
+
const session = await rpcQuerySession(rpc, sessionId);
|
|
343
|
+
if (session) return session;
|
|
344
|
+
}
|
|
345
|
+
} catch { /* fall through to LCD */ }
|
|
346
|
+
|
|
347
|
+
// LCD fallback
|
|
256
348
|
try {
|
|
257
349
|
const data = await lcd(lcdUrl, `/sentinel/session/v3/sessions/${sessionId}`);
|
|
258
350
|
const raw = data?.session;
|
|
@@ -263,30 +355,45 @@ export async function querySessionById(lcdUrl, sessionId) {
|
|
|
263
355
|
|
|
264
356
|
/**
|
|
265
357
|
* Query session allocation (remaining bandwidth).
|
|
358
|
+
* RPC-first with LCD fallback.
|
|
266
359
|
* @param {string} lcdUrl
|
|
267
360
|
* @param {string|number|bigint} sessionId
|
|
268
361
|
* @returns {Promise<{ maxBytes: number, usedBytes: number, remainingBytes: number, percentUsed: number }|null>}
|
|
269
362
|
*/
|
|
270
363
|
export async function querySessionAllocation(lcdUrl, sessionId) {
|
|
364
|
+
let s = null;
|
|
365
|
+
|
|
366
|
+
// RPC-first: query session by ID returns flat decoded object
|
|
271
367
|
try {
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
};
|
|
284
|
-
}
|
|
368
|
+
const rpc = await getRpcClient();
|
|
369
|
+
if (rpc) {
|
|
370
|
+
s = await rpcQuerySession(rpc, sessionId);
|
|
371
|
+
}
|
|
372
|
+
} catch { /* fall through */ }
|
|
373
|
+
|
|
374
|
+
if (!s) {
|
|
375
|
+
// LCD fallback
|
|
376
|
+
try {
|
|
377
|
+
const data = await lcd(lcdUrl, `/sentinel/session/v3/sessions/${sessionId}`);
|
|
378
|
+
s = data.session?.base_session || data.session || null;
|
|
379
|
+
} catch { return null; }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!s) return null;
|
|
383
|
+
const maxBytes = parseInt(s.max_bytes || '0', 10);
|
|
384
|
+
const dl = parseInt(s.download_bytes || '0', 10);
|
|
385
|
+
const ul = parseInt(s.upload_bytes || '0', 10);
|
|
386
|
+
const usedBytes = dl + ul;
|
|
387
|
+
return {
|
|
388
|
+
maxBytes,
|
|
389
|
+
usedBytes,
|
|
390
|
+
remainingBytes: Math.max(0, maxBytes - usedBytes),
|
|
391
|
+
percentUsed: maxBytes > 0 ? Math.round((usedBytes / maxBytes) * 100) : 0,
|
|
392
|
+
};
|
|
285
393
|
}
|
|
286
394
|
|
|
287
395
|
/**
|
|
288
|
-
* Fetch a single node by address
|
|
289
|
-
* Tries the direct v3 endpoint first, falls back to paginated search.
|
|
396
|
+
* Fetch a single node by address via RPC (primary) with LCD fallback.
|
|
290
397
|
*
|
|
291
398
|
* @param {string} nodeAddress - sentnode1... address
|
|
292
399
|
* @param {object} [opts]
|
|
@@ -298,6 +405,21 @@ export async function queryNode(nodeAddress, opts = {}) {
|
|
|
298
405
|
throw new ValidationError(ErrorCodes.INVALID_NODE_ADDRESS, 'nodeAddress must be sentnode1...', { nodeAddress });
|
|
299
406
|
}
|
|
300
407
|
|
|
408
|
+
// RPC-first: single node query is fast and avoids LCD rate limits
|
|
409
|
+
try {
|
|
410
|
+
const rpc = await getRpcClient();
|
|
411
|
+
if (rpc) {
|
|
412
|
+
const node = await rpcQueryNode(rpc, nodeAddress);
|
|
413
|
+
if (node) {
|
|
414
|
+
node.remote_url = resolveNodeUrl(node);
|
|
415
|
+
const addrs = node.remote_addrs || [];
|
|
416
|
+
node.remoteAddrs = addrs.map(a => a.startsWith('http') ? a : `https://${a}`);
|
|
417
|
+
return node;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
} catch { /* RPC failed, fall through to LCD */ }
|
|
421
|
+
|
|
422
|
+
// LCD fallback
|
|
301
423
|
const fetchDirect = async (baseUrl) => {
|
|
302
424
|
try {
|
|
303
425
|
const data = await lcdQuery(`/sentinel/node/v3/nodes/${nodeAddress}`, { lcdUrl: baseUrl });
|
|
@@ -308,18 +430,19 @@ export async function queryNode(nodeAddress, opts = {}) {
|
|
|
308
430
|
} catch { /* fall through to full list */ }
|
|
309
431
|
const { items } = await lcdPaginatedSafe(baseUrl, '/sentinel/node/v3/nodes?status=1', 'nodes');
|
|
310
432
|
const found = items.find(n => n.address === nodeAddress);
|
|
311
|
-
if (!found) throw new NodeError(ErrorCodes.NODE_NOT_FOUND, `Node ${nodeAddress} not found
|
|
433
|
+
if (!found) throw new NodeError(ErrorCodes.NODE_NOT_FOUND, `Node ${nodeAddress} not found (may be inactive)`, { nodeAddress });
|
|
312
434
|
found.remote_url = resolveNodeUrl(found);
|
|
313
435
|
return found;
|
|
314
436
|
};
|
|
315
437
|
|
|
316
438
|
if (opts.lcdUrl) return fetchDirect(opts.lcdUrl);
|
|
317
|
-
const { result } = await tryWithFallback(LCD_ENDPOINTS, fetchDirect, `
|
|
439
|
+
const { result } = await tryWithFallback(LCD_ENDPOINTS, fetchDirect, `node lookup ${nodeAddress}`);
|
|
318
440
|
return result;
|
|
319
441
|
}
|
|
320
442
|
|
|
321
443
|
/**
|
|
322
444
|
* List all sessions for a wallet address.
|
|
445
|
+
* RPC-first with LCD fallback.
|
|
323
446
|
* @param {string} address - sent1... wallet address
|
|
324
447
|
* @param {string} [lcdUrl]
|
|
325
448
|
* @param {object} [opts]
|
|
@@ -327,10 +450,25 @@ export async function queryNode(nodeAddress, opts = {}) {
|
|
|
327
450
|
* @returns {Promise<{ items: ChainSession[], total: number }>}
|
|
328
451
|
*/
|
|
329
452
|
export async function querySessions(address, lcdUrl, opts = {}) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
453
|
+
// RPC-first: returns already-flat decoded sessions
|
|
454
|
+
try {
|
|
455
|
+
const rpc = await getRpcClient();
|
|
456
|
+
if (rpc) {
|
|
457
|
+
const sessions = await rpcQuerySessionsForAccount(rpc, address, { limit: 500 });
|
|
458
|
+
// Filter by status if requested (RPC returns all statuses)
|
|
459
|
+
let items = sessions;
|
|
460
|
+
if (opts.status) {
|
|
461
|
+
const statusNum = parseInt(opts.status, 10);
|
|
462
|
+
items = sessions.filter(s => s.status === statusNum);
|
|
463
|
+
}
|
|
464
|
+
return { items, total: items.length };
|
|
465
|
+
}
|
|
466
|
+
} catch { /* fall through to LCD */ }
|
|
467
|
+
|
|
468
|
+
// LCD fallback
|
|
469
|
+
let lcdPath = `/sentinel/session/v3/sessions?address=${address}`;
|
|
470
|
+
if (opts.status) lcdPath += `&status=${opts.status}`;
|
|
471
|
+
const result = await lcdPaginatedSafe(lcdUrl, lcdPath, 'sessions');
|
|
334
472
|
result.items = result.items.map(flattenSession);
|
|
335
473
|
return result;
|
|
336
474
|
}
|
|
@@ -369,11 +507,22 @@ export function flattenSession(session) {
|
|
|
369
507
|
|
|
370
508
|
/**
|
|
371
509
|
* Get a single subscription by ID.
|
|
510
|
+
* RPC-first with LCD fallback.
|
|
372
511
|
* @param {string|number} id - Subscription ID
|
|
373
512
|
* @param {string} [lcdUrl]
|
|
374
513
|
* @returns {Promise<Subscription|null>}
|
|
375
514
|
*/
|
|
376
515
|
export async function querySubscription(id, lcdUrl) {
|
|
516
|
+
// RPC-first
|
|
517
|
+
try {
|
|
518
|
+
const rpc = await getRpcClient();
|
|
519
|
+
if (rpc) {
|
|
520
|
+
const sub = await rpcQuerySubscription(rpc, id);
|
|
521
|
+
if (sub) return sub;
|
|
522
|
+
}
|
|
523
|
+
} catch { /* fall through */ }
|
|
524
|
+
|
|
525
|
+
// LCD fallback
|
|
377
526
|
try {
|
|
378
527
|
const data = await lcdQuery(`/sentinel/subscription/v3/subscriptions/${id}`, { lcdUrl });
|
|
379
528
|
return data.subscription || null;
|
|
@@ -382,6 +531,7 @@ export async function querySubscription(id, lcdUrl) {
|
|
|
382
531
|
|
|
383
532
|
/**
|
|
384
533
|
* Check if wallet has an active subscription for a specific plan.
|
|
534
|
+
* Uses querySubscriptions which is already RPC-first.
|
|
385
535
|
* @param {string} address - sent1... wallet address
|
|
386
536
|
* @param {number|string} planId - Plan ID to check
|
|
387
537
|
* @param {string} [lcdUrl]
|
|
@@ -396,14 +546,28 @@ export async function hasActiveSubscription(address, planId, lcdUrl) {
|
|
|
396
546
|
|
|
397
547
|
/**
|
|
398
548
|
* Query allocations for a subscription (who has how many bytes).
|
|
399
|
-
*
|
|
400
|
-
* (returns 501). The v2 path returns the same allocation data. Same situation as /plan/v3/plans/{id}.
|
|
549
|
+
* RPC-first with LCD fallback. Uses v2 allocation path (v3 returns 501 on chain).
|
|
401
550
|
*
|
|
402
551
|
* @param {string|number|bigint} subscriptionId
|
|
403
552
|
* @param {string} [lcdUrl]
|
|
404
553
|
* @returns {Promise<Array<{ id: string, address: string, grantedBytes: string, utilisedBytes: string }>>}
|
|
405
554
|
*/
|
|
406
555
|
export async function querySubscriptionAllocations(subscriptionId, lcdUrl) {
|
|
556
|
+
// RPC-first
|
|
557
|
+
try {
|
|
558
|
+
const rpc = await getRpcClient();
|
|
559
|
+
if (rpc) {
|
|
560
|
+
const allocs = await rpcQuerySubAllocations(rpc, subscriptionId, { limit: 100 });
|
|
561
|
+
return allocs.map(a => ({
|
|
562
|
+
id: a.id,
|
|
563
|
+
address: a.address,
|
|
564
|
+
grantedBytes: a.granted_bytes || '0',
|
|
565
|
+
utilisedBytes: a.utilised_bytes || '0',
|
|
566
|
+
}));
|
|
567
|
+
}
|
|
568
|
+
} catch { /* fall through */ }
|
|
569
|
+
|
|
570
|
+
// LCD fallback
|
|
407
571
|
try {
|
|
408
572
|
const data = await lcdQuery(`/sentinel/subscription/v2/subscriptions/${subscriptionId}/allocations`, { lcdUrl });
|
|
409
573
|
return (data.allocations || []).map(a => ({
|
|
@@ -419,6 +583,7 @@ export async function querySubscriptionAllocations(subscriptionId, lcdUrl) {
|
|
|
419
583
|
|
|
420
584
|
/**
|
|
421
585
|
* Query all subscriptions for a plan. Supports owner filtering.
|
|
586
|
+
* RPC-first with LCD fallback.
|
|
422
587
|
*
|
|
423
588
|
* @param {number|string} planId - Plan ID
|
|
424
589
|
* @param {object} [opts]
|
|
@@ -427,12 +592,30 @@ export async function querySubscriptionAllocations(subscriptionId, lcdUrl) {
|
|
|
427
592
|
* @returns {Promise<{ subscribers: Array<{ address: string, status: number, id: string }>, total: number|null }>}
|
|
428
593
|
*/
|
|
429
594
|
export async function queryPlanSubscribers(planId, opts = {}) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
595
|
+
let items;
|
|
596
|
+
let total;
|
|
597
|
+
|
|
598
|
+
// RPC-first
|
|
599
|
+
try {
|
|
600
|
+
const rpc = await getRpcClient();
|
|
601
|
+
if (rpc) {
|
|
602
|
+
items = await rpcQuerySubscriptionsForPlan(rpc, planId, { limit: 500 });
|
|
603
|
+
total = items.length;
|
|
604
|
+
}
|
|
605
|
+
} catch { /* fall through */ }
|
|
606
|
+
|
|
607
|
+
if (!items) {
|
|
608
|
+
// LCD fallback
|
|
609
|
+
const result = await lcdQueryAll(
|
|
610
|
+
`/sentinel/subscription/v3/plans/${planId}/subscriptions`,
|
|
611
|
+
{ lcdUrl: opts.lcdUrl, dataKey: 'subscriptions' },
|
|
612
|
+
);
|
|
613
|
+
items = result.items;
|
|
614
|
+
total = result.total;
|
|
615
|
+
}
|
|
616
|
+
|
|
434
617
|
let subscribers = items.map(s => ({
|
|
435
|
-
address: s.address || s.subscriber,
|
|
618
|
+
address: s.address || s.acc_address || s.subscriber,
|
|
436
619
|
status: s.status,
|
|
437
620
|
id: s.id || s.base_id,
|
|
438
621
|
...s,
|
|
@@ -466,26 +649,34 @@ export async function getPlanStats(planId, ownerAddress, opts = {}) {
|
|
|
466
649
|
// ─── v26: Field Experience Helpers ────────────────────────────────────────────
|
|
467
650
|
|
|
468
651
|
/**
|
|
469
|
-
* Query all nodes linked to a plan.
|
|
652
|
+
* Query all nodes linked to a plan via RPC (primary) with LCD fallback.
|
|
470
653
|
* @param {number|string} planId
|
|
471
654
|
* @param {string} [lcdUrl]
|
|
472
655
|
* @returns {Promise<{ items: any[], total: number|null }>}
|
|
473
656
|
*/
|
|
474
657
|
export async function queryPlanNodes(planId, lcdUrl) {
|
|
475
|
-
//
|
|
476
|
-
|
|
658
|
+
// RPC-first
|
|
659
|
+
try {
|
|
660
|
+
const rpc = await getRpcClient();
|
|
661
|
+
if (rpc) {
|
|
662
|
+
const nodes = await rpcQueryNodesForPlan(rpc, planId, { status: 1, limit: 5000 });
|
|
663
|
+
return { items: nodes, total: nodes.length };
|
|
664
|
+
}
|
|
665
|
+
} catch { /* RPC failed, fall through to LCD */ }
|
|
666
|
+
|
|
667
|
+
// LCD fallback — pagination is BROKEN on this endpoint, single high-limit request
|
|
477
668
|
const doQuery = async (baseUrl) => {
|
|
478
669
|
const data = await lcd(baseUrl, `/sentinel/node/v3/plans/${planId}/nodes?pagination.limit=5000`);
|
|
479
670
|
return { items: data.nodes || [], total: (data.nodes || []).length };
|
|
480
671
|
};
|
|
481
672
|
if (lcdUrl) return doQuery(lcdUrl);
|
|
482
|
-
const { result } = await tryWithFallback(LCD_ENDPOINTS, doQuery, `
|
|
673
|
+
const { result } = await tryWithFallback(LCD_ENDPOINTS, doQuery, `plan ${planId} nodes`);
|
|
483
674
|
return result;
|
|
484
675
|
}
|
|
485
676
|
|
|
486
677
|
/**
|
|
487
678
|
* Discover all available plans with metadata (subscriber count, node count, price).
|
|
488
|
-
*
|
|
679
|
+
* RPC-first: probes plan IDs via rpcQueryPlan, falls back to LCD.
|
|
489
680
|
*
|
|
490
681
|
* @param {string} [lcdUrl]
|
|
491
682
|
* @param {object} [opts]
|
|
@@ -501,19 +692,61 @@ export async function discoverPlans(lcdUrl, opts = {}) {
|
|
|
501
692
|
const baseLcd = lcdUrl || LCD_ENDPOINTS[0].url;
|
|
502
693
|
const plans = [];
|
|
503
694
|
|
|
695
|
+
// Try to get RPC client for plan queries
|
|
696
|
+
let rpc = null;
|
|
697
|
+
try { rpc = await getRpcClient(); } catch { /* LCD only */ }
|
|
698
|
+
|
|
504
699
|
for (let batch = 0; batch < Math.ceil(maxId / batchSize); batch++) {
|
|
505
700
|
const probes = [];
|
|
506
701
|
for (let i = batch * batchSize + 1; i <= Math.min((batch + 1) * batchSize, maxId); i++) {
|
|
507
702
|
probes.push((async (id) => {
|
|
508
703
|
try {
|
|
509
|
-
|
|
510
|
-
|
|
704
|
+
// RPC-first: query plan existence via RPC
|
|
705
|
+
let plan = null;
|
|
706
|
+
if (rpc) {
|
|
707
|
+
try { plan = await rpcQueryPlan(rpc, id); } catch { /* fall through */ }
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Get subscriber count — RPC for plan subs, LCD as fallback
|
|
711
|
+
let subCount = 0;
|
|
712
|
+
let price = null;
|
|
713
|
+
if (rpc) {
|
|
714
|
+
try {
|
|
715
|
+
const subs = await rpcQuerySubscriptionsForPlan(rpc, id, { limit: 1 });
|
|
716
|
+
subCount = subs.length; // Quick check — at least 1
|
|
717
|
+
price = subs[0]?.price || null;
|
|
718
|
+
// If RPC returned subs, plan exists even if rpcQueryPlan returned null
|
|
719
|
+
if (subCount > 0 && !plan) plan = { id };
|
|
720
|
+
} catch { /* fall through */ }
|
|
721
|
+
}
|
|
722
|
+
if (!plan && subCount === 0) {
|
|
723
|
+
// LCD fallback for subscriber count
|
|
724
|
+
try {
|
|
725
|
+
const subData = await lcd(baseLcd, `/sentinel/subscription/v3/plans/${id}/subscriptions?pagination.limit=1&pagination.count_total=true`);
|
|
726
|
+
subCount = parseInt(subData.pagination?.total || '0', 10);
|
|
727
|
+
price = subData.subscriptions?.[0]?.price || null;
|
|
728
|
+
if (subCount > 0) plan = { id };
|
|
729
|
+
} catch { /* plan doesn't exist */ }
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
if (!plan && !includeEmpty) return null;
|
|
511
733
|
if (subCount === 0 && !includeEmpty) return null;
|
|
512
|
-
|
|
513
|
-
//
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
734
|
+
|
|
735
|
+
// Get node count — RPC-first
|
|
736
|
+
let nodeCount = 0;
|
|
737
|
+
if (rpc) {
|
|
738
|
+
try {
|
|
739
|
+
const nodes = await rpcQueryNodesForPlan(rpc, id, { status: 1, limit: 5000 });
|
|
740
|
+
nodeCount = nodes.length;
|
|
741
|
+
} catch { /* fall through to LCD */ }
|
|
742
|
+
}
|
|
743
|
+
if (nodeCount === 0) {
|
|
744
|
+
try {
|
|
745
|
+
const nodeData = await lcd(baseLcd, `/sentinel/node/v3/plans/${id}/nodes?pagination.limit=5000`);
|
|
746
|
+
nodeCount = (nodeData.nodes || []).length;
|
|
747
|
+
} catch { /* no nodes */ }
|
|
748
|
+
}
|
|
749
|
+
|
|
517
750
|
return { id, subscribers: subCount, nodeCount, price, hasNodes: nodeCount > 0 };
|
|
518
751
|
} catch { return null; }
|
|
519
752
|
})(i));
|
|
@@ -527,18 +760,51 @@ export async function discoverPlans(lcdUrl, opts = {}) {
|
|
|
527
760
|
|
|
528
761
|
/**
|
|
529
762
|
* Get provider details by address.
|
|
763
|
+
* RPC-first with LCD fallback. Provider is still v2 on chain.
|
|
530
764
|
* @param {string} provAddress - sentprov1... address
|
|
531
765
|
* @param {object} [opts]
|
|
532
766
|
* @param {string} [opts.lcdUrl]
|
|
533
767
|
* @returns {Promise<object|null>}
|
|
534
768
|
*/
|
|
535
769
|
export async function getProviderByAddress(provAddress, opts = {}) {
|
|
770
|
+
// RPC-first
|
|
771
|
+
try {
|
|
772
|
+
const rpc = await getRpcClient();
|
|
773
|
+
if (rpc) {
|
|
774
|
+
const provider = await _rpcQueryProvider(rpc, provAddress);
|
|
775
|
+
if (provider) return provider;
|
|
776
|
+
}
|
|
777
|
+
} catch { /* fall through to LCD */ }
|
|
778
|
+
|
|
779
|
+
// LCD fallback
|
|
536
780
|
try {
|
|
537
781
|
const data = await lcdQuery(`/sentinel/provider/v2/providers/${provAddress}`, opts);
|
|
538
782
|
return data.provider || null;
|
|
539
783
|
} catch { return null; }
|
|
540
784
|
}
|
|
541
785
|
|
|
786
|
+
/**
|
|
787
|
+
* Query authz grants between granter and grantee.
|
|
788
|
+
* RPC-first with LCD fallback.
|
|
789
|
+
* @param {string} lcdUrl - LCD endpoint
|
|
790
|
+
* @param {string} granter - Granter address (sent1...)
|
|
791
|
+
* @param {string} grantee - Grantee address (sent1...)
|
|
792
|
+
* @returns {Promise<Array>}
|
|
793
|
+
*/
|
|
794
|
+
export async function queryAuthzGrants(lcdUrl, granter, grantee) {
|
|
795
|
+
// RPC-first
|
|
796
|
+
try {
|
|
797
|
+
const rpc = await getRpcClient();
|
|
798
|
+
if (rpc) {
|
|
799
|
+
return await _rpcQueryAuthzGrants(rpc, granter, grantee);
|
|
800
|
+
}
|
|
801
|
+
} catch { /* fall through to LCD */ }
|
|
802
|
+
|
|
803
|
+
// LCD fallback
|
|
804
|
+
const { items } = await lcdPaginatedSafe(lcdUrl, `/cosmos/authz/v1beta1/grants?granter=${granter}&grantee=${grantee}`, 'grants');
|
|
805
|
+
return items;
|
|
806
|
+
}
|
|
807
|
+
|
|
542
808
|
// ─── VPN Settings Persistence ────────────────────────────────────────────────
|
|
543
809
|
// v27: Persistent user settings (backported from C# VpnSettings.cs).
|
|
544
810
|
// Stores preferences in ~/.sentinel-sdk/settings.json with restrictive permissions.
|