blue-js-sdk 2.1.1 → 2.2.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/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/chain/queries.js +242 -53
- package/chain/rpc.js +339 -7
- package/connection/resilience.js +10 -2
- package/index.js +5 -0
- package/node-connect.js +57 -16
- package/package.json +1 -1
package/ai-path/connect.js
CHANGED
|
@@ -34,9 +34,12 @@ import {
|
|
|
34
34
|
getBalance as sdkGetBalance,
|
|
35
35
|
tryWithFallback,
|
|
36
36
|
RPC_ENDPOINTS,
|
|
37
|
+
LCD_ENDPOINTS,
|
|
38
|
+
queryFeeGrant,
|
|
37
39
|
// v1.5.0: RPC queries (protobuf, ~10x faster than LCD for balance checks)
|
|
38
40
|
createRpcQueryClientWithFallback,
|
|
39
41
|
rpcQueryBalance,
|
|
42
|
+
rpcQueryFeeGrant,
|
|
40
43
|
// v1.5.0: Typed event parsers (replaces string matching for session ID extraction)
|
|
41
44
|
extractSessionIdTyped,
|
|
42
45
|
NodeEventCreateSession,
|
|
@@ -193,6 +196,18 @@ function humanError(err) {
|
|
|
193
196
|
message: 'Connection was cancelled.',
|
|
194
197
|
nextAction: 'none',
|
|
195
198
|
},
|
|
199
|
+
FEE_GRANT_NOT_FOUND: {
|
|
200
|
+
message: 'No fee grant from operator to agent. Operator must provision a grant first.',
|
|
201
|
+
nextAction: 'request_fee_grant',
|
|
202
|
+
},
|
|
203
|
+
FEE_GRANT_EXPIRED: {
|
|
204
|
+
message: 'Fee grant has expired. Operator must renew the grant.',
|
|
205
|
+
nextAction: 'request_fee_grant_renewal',
|
|
206
|
+
},
|
|
207
|
+
FEE_GRANT_EXHAUSTED: {
|
|
208
|
+
message: 'Fee grant spend limit exhausted. Operator must top up the grant.',
|
|
209
|
+
nextAction: 'request_fee_grant_renewal',
|
|
210
|
+
},
|
|
196
211
|
};
|
|
197
212
|
|
|
198
213
|
const entry = messages[code];
|
|
@@ -364,6 +379,101 @@ export async function connect(opts = {}) {
|
|
|
364
379
|
}
|
|
365
380
|
timings.balance = Date.now() - t0;
|
|
366
381
|
|
|
382
|
+
// ── STEP 3.5: Fee Grant Validity Check (when feeGranter is set) ───────────
|
|
383
|
+
// Verify the fee grant exists on-chain and hasn't expired before attempting
|
|
384
|
+
// a connection that would fail at broadcast time.
|
|
385
|
+
|
|
386
|
+
if (opts.feeGranter) {
|
|
387
|
+
try {
|
|
388
|
+
// RPC first (protobuf, ~10x faster), LCD fallback
|
|
389
|
+
let grant = null;
|
|
390
|
+
try {
|
|
391
|
+
const rpcClient = await createRpcQueryClientWithFallback();
|
|
392
|
+
grant = await rpcQueryFeeGrant(rpcClient, opts.feeGranter, walletAddress);
|
|
393
|
+
} catch {
|
|
394
|
+
// RPC failed — fall back to LCD with failover
|
|
395
|
+
const lcdResult = await tryWithFallback(
|
|
396
|
+
LCD_ENDPOINTS,
|
|
397
|
+
async (endpoint) => {
|
|
398
|
+
const url = endpoint?.url || endpoint;
|
|
399
|
+
return queryFeeGrant(url, opts.feeGranter, walletAddress);
|
|
400
|
+
},
|
|
401
|
+
'fee grant pre-check (LCD fallback)',
|
|
402
|
+
);
|
|
403
|
+
grant = lcdResult.result;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (!grant) {
|
|
407
|
+
const err = new Error(`No fee grant found from ${opts.feeGranter} to ${walletAddress}. Operator must create a fee grant before agent can connect with 0 P2P.`);
|
|
408
|
+
err.code = 'FEE_GRANT_NOT_FOUND';
|
|
409
|
+
err.nextAction = 'request_fee_grant';
|
|
410
|
+
err.details = { granter: opts.feeGranter, grantee: walletAddress };
|
|
411
|
+
throw err;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Unwrap grant structure: AllowedMsgAllowance > BasicAllowance
|
|
415
|
+
// Chain returns: { allowance: { "@type": "AllowedMsg...", allowance: { "@type": "Basic...", spend_limit, expiration }, allowed_messages: [...] } }
|
|
416
|
+
const outerAllowance = grant.allowance || grant;
|
|
417
|
+
const isAllowedMsg = outerAllowance['@type']?.includes('AllowedMsgAllowance');
|
|
418
|
+
const inner = isAllowedMsg ? (outerAllowance.allowance || outerAllowance) : outerAllowance;
|
|
419
|
+
const expiration = inner.expiration || outerAllowance.expiration;
|
|
420
|
+
|
|
421
|
+
// Check expiration
|
|
422
|
+
if (expiration) {
|
|
423
|
+
const expiresAt = new Date(expiration);
|
|
424
|
+
const now = new Date();
|
|
425
|
+
if (expiresAt <= now) {
|
|
426
|
+
const err = new Error(`Fee grant from ${opts.feeGranter} expired at ${expiresAt.toISOString()}. Operator must renew the fee grant.`);
|
|
427
|
+
err.code = 'FEE_GRANT_EXPIRED';
|
|
428
|
+
err.nextAction = 'request_fee_grant_renewal';
|
|
429
|
+
err.details = { granter: opts.feeGranter, grantee: walletAddress, expiredAt: expiresAt.toISOString() };
|
|
430
|
+
throw err;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const hoursLeft = (expiresAt - now) / 3600000;
|
|
434
|
+
if (hoursLeft < 1) {
|
|
435
|
+
log(3, totalSteps, 'FEE_GRANT', `Warning: Fee grant expires in ${Math.round(hoursLeft * 60)} minutes`);
|
|
436
|
+
} else {
|
|
437
|
+
log(3, totalSteps, 'FEE_GRANT', `Fee grant valid, expires ${expiresAt.toISOString()} (${Math.round(hoursLeft)}h remaining)`);
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
log(3, totalSteps, 'FEE_GRANT', 'Fee grant valid (no expiration)');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Check spend_limit — if set, ensure there's enough remaining for at least one TX
|
|
444
|
+
const spendLimit = inner.spend_limit;
|
|
445
|
+
if (spendLimit && Array.isArray(spendLimit)) {
|
|
446
|
+
const udvpnLimit = spendLimit.find(c => c.denom === 'udvpn');
|
|
447
|
+
if (udvpnLimit) {
|
|
448
|
+
const remaining = parseInt(udvpnLimit.amount, 10) || 0;
|
|
449
|
+
if (remaining < 20000) { // 20,000 udvpn = minimum for one session TX
|
|
450
|
+
const err = new Error(`Fee grant from ${opts.feeGranter} has insufficient spend limit: ${remaining} udvpn remaining (need 20,000 for session TX). Operator must top up the grant.`);
|
|
451
|
+
err.code = 'FEE_GRANT_EXHAUSTED';
|
|
452
|
+
err.nextAction = 'request_fee_grant_renewal';
|
|
453
|
+
err.details = { granter: opts.feeGranter, grantee: walletAddress, remainingUdvpn: remaining };
|
|
454
|
+
throw err;
|
|
455
|
+
}
|
|
456
|
+
log(3, totalSteps, 'FEE_GRANT', `Spend limit: ${remaining} udvpn remaining`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Check allowed_messages — verify it includes the messages we need
|
|
461
|
+
const allowedMessages = isAllowedMsg ? (outerAllowance.allowed_messages || []) : [];
|
|
462
|
+
if (allowedMessages.length > 0) {
|
|
463
|
+
const needsStart = allowedMessages.some(m =>
|
|
464
|
+
m.includes('MsgStartSession') || m.includes('MsgStartSessionRequest'),
|
|
465
|
+
);
|
|
466
|
+
if (!needsStart) {
|
|
467
|
+
log(3, totalSteps, 'FEE_GRANT', `Warning: allowed_messages doesn't include MsgStartSession — TX may fail`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} catch (err) {
|
|
471
|
+
if (err.code === 'FEE_GRANT_NOT_FOUND' || err.code === 'FEE_GRANT_EXPIRED' || err.code === 'FEE_GRANT_EXHAUSTED') throw err;
|
|
472
|
+
// Non-critical — LCD query failed but fee grant may still work at broadcast time
|
|
473
|
+
log(3, totalSteps, 'FEE_GRANT', `Could not verify fee grant (${err.message}) — proceeding anyway`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
367
477
|
// ── STEP 4/7: Node Selection ──────────────────────────────────────────────
|
|
368
478
|
|
|
369
479
|
t0 = Date.now();
|
|
@@ -584,11 +694,13 @@ export async function connect(opts = {}) {
|
|
|
584
694
|
} else if (resolvedNodeAddress) {
|
|
585
695
|
// Direct connection — either user specified nodeAddress or country discovery found one
|
|
586
696
|
sdkOpts.nodeAddress = resolvedNodeAddress;
|
|
697
|
+
if (opts.feeGranter) sdkOpts.feeGranter = opts.feeGranter;
|
|
587
698
|
result = await connectDirect(sdkOpts);
|
|
588
699
|
} else {
|
|
589
700
|
// No country filter or country discovery found nothing — auto-select globally
|
|
590
701
|
// Use higher maxAttempts to search more nodes
|
|
591
702
|
if (!sdkOpts.maxAttempts) sdkOpts.maxAttempts = 5;
|
|
703
|
+
if (opts.feeGranter) sdkOpts.feeGranter = opts.feeGranter;
|
|
592
704
|
result = await connectAuto(sdkOpts);
|
|
593
705
|
}
|
|
594
706
|
|
package/ai-path/pricing.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
estimateSessionCost,
|
|
9
|
-
|
|
9
|
+
getNodePrices,
|
|
10
10
|
formatP2P,
|
|
11
11
|
PRICING_REFERENCE,
|
|
12
12
|
DENOM,
|
|
@@ -81,11 +81,12 @@ export async function estimateCost(opts = {}) {
|
|
|
81
81
|
const gb = opts.gigabytes || 1;
|
|
82
82
|
const gasCost = 40000; // ~0.04 P2P per TX
|
|
83
83
|
|
|
84
|
-
// If specific node given, get exact price
|
|
84
|
+
// If specific node given, get exact price via RPC-first node query
|
|
85
85
|
if (opts.nodeAddress) {
|
|
86
86
|
try {
|
|
87
|
-
const
|
|
88
|
-
const
|
|
87
|
+
const prices = await getNodePrices(opts.nodeAddress);
|
|
88
|
+
const perGbUdvpn = prices.gigabyte.udvpn || 0;
|
|
89
|
+
const total = perGbUdvpn * gb;
|
|
89
90
|
const grandTotal = total + gasCost;
|
|
90
91
|
|
|
91
92
|
const result = {
|
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,43 @@ 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
|
+
} from './rpc.js';
|
|
19
34
|
|
|
20
35
|
// Re-export for convenience
|
|
21
36
|
export { extractSessionId };
|
|
22
37
|
|
|
38
|
+
// ─── RPC Client Helper ─────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
let _rpcClient = null;
|
|
41
|
+
let _rpcClientPromise = null;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get or create a cached RPC query client. Returns null if all RPC endpoints fail
|
|
45
|
+
* (caller should fall back to LCD).
|
|
46
|
+
*/
|
|
47
|
+
async function getRpcClient() {
|
|
48
|
+
if (_rpcClient) return _rpcClient;
|
|
49
|
+
if (_rpcClientPromise) return _rpcClientPromise;
|
|
50
|
+
_rpcClientPromise = createRpcQueryClientWithFallback()
|
|
51
|
+
.then(client => { _rpcClient = client; return client; })
|
|
52
|
+
.catch(() => { _rpcClient = null; return null; })
|
|
53
|
+
.finally(() => { _rpcClientPromise = null; });
|
|
54
|
+
return _rpcClientPromise;
|
|
55
|
+
}
|
|
56
|
+
|
|
23
57
|
// ─── Query Helpers ───────────────────────────────────────────────────────────
|
|
24
58
|
|
|
25
59
|
/**
|
|
@@ -35,20 +69,38 @@ export async function getBalance(client, address) {
|
|
|
35
69
|
/**
|
|
36
70
|
* Find an existing active session for a wallet+node pair.
|
|
37
71
|
* 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.
|
|
72
|
+
* RPC-first with LCD fallback.
|
|
40
73
|
*/
|
|
41
74
|
export async function findExistingSession(lcdUrl, walletAddr, nodeAddr) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
75
|
+
let sessions;
|
|
76
|
+
|
|
77
|
+
// RPC-first: returns decoded, flat session objects
|
|
78
|
+
try {
|
|
79
|
+
const rpc = await getRpcClient();
|
|
80
|
+
if (rpc) {
|
|
81
|
+
sessions = await rpcQuerySessionsForAccount(rpc, walletAddr, { limit: 500 });
|
|
82
|
+
}
|
|
83
|
+
} catch { /* RPC failed, fall through to LCD */ }
|
|
84
|
+
|
|
85
|
+
if (!sessions) {
|
|
86
|
+
// LCD fallback
|
|
87
|
+
const { items } = await lcdPaginatedSafe(lcdUrl, `/sentinel/session/v3/sessions?address=${walletAddr}&status=1`, 'sessions');
|
|
88
|
+
sessions = items.map(s => {
|
|
89
|
+
const bs = s.base_session || s;
|
|
90
|
+
return { ...bs, price: s.price, subscription_id: s.subscription_id };
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const s of sessions) {
|
|
95
|
+
if ((s.node_address || s.node) !== nodeAddr) continue;
|
|
96
|
+
// RPC returns status as number (1=active), LCD as string
|
|
97
|
+
const st = s.status;
|
|
98
|
+
if (st && st !== 1 && st !== 'active') continue;
|
|
99
|
+
const acct = s.acc_address || s.address;
|
|
48
100
|
if (acct && acct !== walletAddr) continue;
|
|
49
|
-
const maxBytes = parseInt(
|
|
50
|
-
const used = parseInt(
|
|
51
|
-
if (maxBytes === 0 || used < maxBytes) return BigInt(
|
|
101
|
+
const maxBytes = parseInt(s.max_bytes || '0');
|
|
102
|
+
const used = parseInt(s.download_bytes || '0') + parseInt(s.upload_bytes || '0');
|
|
103
|
+
if (maxBytes === 0 || used < maxBytes) return BigInt(s.id);
|
|
52
104
|
}
|
|
53
105
|
return null;
|
|
54
106
|
}
|
|
@@ -80,7 +132,7 @@ const NODE_CACHE_TTL = 5 * 60_000; // 5 minutes
|
|
|
80
132
|
export function invalidateNodeCache() { _nodeListCache = null; }
|
|
81
133
|
|
|
82
134
|
/**
|
|
83
|
-
* Fetch all active nodes
|
|
135
|
+
* Fetch all active nodes via RPC (primary) with LCD fallback.
|
|
84
136
|
* Returns array of node objects. Each node has:
|
|
85
137
|
* - `remote_url`: the first usable HTTPS URL (for primary connection)
|
|
86
138
|
* - `remoteAddrs`: ALL remote addresses (for fallback on connection failure)
|
|
@@ -93,7 +145,21 @@ export async function fetchActiveNodes(lcdUrl, limit = 500, maxPages = 20) {
|
|
|
93
145
|
return _nodeListCache.map(n => ({ ...n, planIds: [...(n.planIds || [])] }));
|
|
94
146
|
}
|
|
95
147
|
|
|
96
|
-
|
|
148
|
+
let items;
|
|
149
|
+
try {
|
|
150
|
+
// RPC-first: ~912x faster than LCD for bulk node queries
|
|
151
|
+
const rpc = await getRpcClient();
|
|
152
|
+
if (rpc) {
|
|
153
|
+
items = await rpcQueryNodes(rpc, { status: 1, limit });
|
|
154
|
+
}
|
|
155
|
+
} catch { /* RPC failed, fall through to LCD */ }
|
|
156
|
+
|
|
157
|
+
if (!items) {
|
|
158
|
+
// LCD fallback
|
|
159
|
+
const result = await lcdPaginatedSafe(lcdUrl, '/sentinel/node/v3/nodes?status=1', 'nodes', { limit });
|
|
160
|
+
items = result.items;
|
|
161
|
+
}
|
|
162
|
+
|
|
97
163
|
for (const n of items) {
|
|
98
164
|
// Preserve ALL remote addresses for fallback
|
|
99
165
|
const addrs = n.remote_addrs || [];
|
|
@@ -228,21 +294,35 @@ export async function getNodePrices(nodeAddress, lcdUrl) {
|
|
|
228
294
|
|
|
229
295
|
/**
|
|
230
296
|
* Query a wallet's active subscriptions.
|
|
297
|
+
* RPC-first with LCD fallback.
|
|
231
298
|
* @param {string} lcdUrl
|
|
232
299
|
* @param {string} walletAddr - sent1... address
|
|
233
|
-
* @returns {Promise<{
|
|
300
|
+
* @returns {Promise<{ items: any[], total: number|null }>}
|
|
234
301
|
*/
|
|
235
302
|
export async function querySubscriptions(lcdUrl, walletAddr, opts = {}) {
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
303
|
+
// RPC-first
|
|
304
|
+
try {
|
|
305
|
+
const rpc = await getRpcClient();
|
|
306
|
+
if (rpc) {
|
|
307
|
+
let subs = await rpcQuerySubscriptionsForAccount(rpc, walletAddr, { limit: 500 });
|
|
308
|
+
if (opts.status) {
|
|
309
|
+
const statusNum = opts.status === 'active' ? 1 : 2;
|
|
310
|
+
subs = subs.filter(s => s.status === statusNum);
|
|
311
|
+
}
|
|
312
|
+
return { items: subs, total: subs.length };
|
|
313
|
+
}
|
|
314
|
+
} catch { /* fall through to LCD */ }
|
|
315
|
+
|
|
316
|
+
// LCD fallback
|
|
317
|
+
let lcdPath = `/sentinel/subscription/v3/accounts/${walletAddr}/subscriptions`;
|
|
318
|
+
if (opts.status) lcdPath += `?status=${opts.status === 'active' ? '1' : '2'}`;
|
|
319
|
+
return lcdQueryAll(lcdPath, { lcdUrl, dataKey: 'subscriptions' });
|
|
240
320
|
}
|
|
241
321
|
|
|
242
322
|
/**
|
|
243
323
|
* Query a single session directly by ID — O(1) instead of scanning all wallet sessions.
|
|
244
324
|
* Returns the flattened session object or null if not found.
|
|
245
|
-
*
|
|
325
|
+
* RPC-first with LCD fallback.
|
|
246
326
|
*
|
|
247
327
|
* @param {string} lcdUrl - LCD endpoint URL
|
|
248
328
|
* @param {string|number|bigint} sessionId - Session ID to query
|
|
@@ -253,6 +333,16 @@ export async function querySubscriptions(lcdUrl, walletAddr, opts = {}) {
|
|
|
253
333
|
* if (session) console.log(`Session ${session.id} on node ${session.node_address}`);
|
|
254
334
|
*/
|
|
255
335
|
export async function querySessionById(lcdUrl, sessionId) {
|
|
336
|
+
// RPC-first
|
|
337
|
+
try {
|
|
338
|
+
const rpc = await getRpcClient();
|
|
339
|
+
if (rpc) {
|
|
340
|
+
const session = await rpcQuerySession(rpc, sessionId);
|
|
341
|
+
if (session) return session;
|
|
342
|
+
}
|
|
343
|
+
} catch { /* fall through to LCD */ }
|
|
344
|
+
|
|
345
|
+
// LCD fallback
|
|
256
346
|
try {
|
|
257
347
|
const data = await lcd(lcdUrl, `/sentinel/session/v3/sessions/${sessionId}`);
|
|
258
348
|
const raw = data?.session;
|
|
@@ -263,30 +353,45 @@ export async function querySessionById(lcdUrl, sessionId) {
|
|
|
263
353
|
|
|
264
354
|
/**
|
|
265
355
|
* Query session allocation (remaining bandwidth).
|
|
356
|
+
* RPC-first with LCD fallback.
|
|
266
357
|
* @param {string} lcdUrl
|
|
267
358
|
* @param {string|number|bigint} sessionId
|
|
268
359
|
* @returns {Promise<{ maxBytes: number, usedBytes: number, remainingBytes: number, percentUsed: number }|null>}
|
|
269
360
|
*/
|
|
270
361
|
export async function querySessionAllocation(lcdUrl, sessionId) {
|
|
362
|
+
let s = null;
|
|
363
|
+
|
|
364
|
+
// RPC-first: query session by ID returns flat decoded object
|
|
271
365
|
try {
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
};
|
|
284
|
-
}
|
|
366
|
+
const rpc = await getRpcClient();
|
|
367
|
+
if (rpc) {
|
|
368
|
+
s = await rpcQuerySession(rpc, sessionId);
|
|
369
|
+
}
|
|
370
|
+
} catch { /* fall through */ }
|
|
371
|
+
|
|
372
|
+
if (!s) {
|
|
373
|
+
// LCD fallback
|
|
374
|
+
try {
|
|
375
|
+
const data = await lcd(lcdUrl, `/sentinel/session/v3/sessions/${sessionId}`);
|
|
376
|
+
s = data.session?.base_session || data.session || null;
|
|
377
|
+
} catch { return null; }
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (!s) return null;
|
|
381
|
+
const maxBytes = parseInt(s.max_bytes || '0', 10);
|
|
382
|
+
const dl = parseInt(s.download_bytes || '0', 10);
|
|
383
|
+
const ul = parseInt(s.upload_bytes || '0', 10);
|
|
384
|
+
const usedBytes = dl + ul;
|
|
385
|
+
return {
|
|
386
|
+
maxBytes,
|
|
387
|
+
usedBytes,
|
|
388
|
+
remainingBytes: Math.max(0, maxBytes - usedBytes),
|
|
389
|
+
percentUsed: maxBytes > 0 ? Math.round((usedBytes / maxBytes) * 100) : 0,
|
|
390
|
+
};
|
|
285
391
|
}
|
|
286
392
|
|
|
287
393
|
/**
|
|
288
|
-
* Fetch a single node by address
|
|
289
|
-
* Tries the direct v3 endpoint first, falls back to paginated search.
|
|
394
|
+
* Fetch a single node by address via RPC (primary) with LCD fallback.
|
|
290
395
|
*
|
|
291
396
|
* @param {string} nodeAddress - sentnode1... address
|
|
292
397
|
* @param {object} [opts]
|
|
@@ -298,6 +403,21 @@ export async function queryNode(nodeAddress, opts = {}) {
|
|
|
298
403
|
throw new ValidationError(ErrorCodes.INVALID_NODE_ADDRESS, 'nodeAddress must be sentnode1...', { nodeAddress });
|
|
299
404
|
}
|
|
300
405
|
|
|
406
|
+
// RPC-first: single node query is fast and avoids LCD rate limits
|
|
407
|
+
try {
|
|
408
|
+
const rpc = await getRpcClient();
|
|
409
|
+
if (rpc) {
|
|
410
|
+
const node = await rpcQueryNode(rpc, nodeAddress);
|
|
411
|
+
if (node) {
|
|
412
|
+
node.remote_url = resolveNodeUrl(node);
|
|
413
|
+
const addrs = node.remote_addrs || [];
|
|
414
|
+
node.remoteAddrs = addrs.map(a => a.startsWith('http') ? a : `https://${a}`);
|
|
415
|
+
return node;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
} catch { /* RPC failed, fall through to LCD */ }
|
|
419
|
+
|
|
420
|
+
// LCD fallback
|
|
301
421
|
const fetchDirect = async (baseUrl) => {
|
|
302
422
|
try {
|
|
303
423
|
const data = await lcdQuery(`/sentinel/node/v3/nodes/${nodeAddress}`, { lcdUrl: baseUrl });
|
|
@@ -308,18 +428,19 @@ export async function queryNode(nodeAddress, opts = {}) {
|
|
|
308
428
|
} catch { /* fall through to full list */ }
|
|
309
429
|
const { items } = await lcdPaginatedSafe(baseUrl, '/sentinel/node/v3/nodes?status=1', 'nodes');
|
|
310
430
|
const found = items.find(n => n.address === nodeAddress);
|
|
311
|
-
if (!found) throw new NodeError(ErrorCodes.NODE_NOT_FOUND, `Node ${nodeAddress} not found
|
|
431
|
+
if (!found) throw new NodeError(ErrorCodes.NODE_NOT_FOUND, `Node ${nodeAddress} not found (may be inactive)`, { nodeAddress });
|
|
312
432
|
found.remote_url = resolveNodeUrl(found);
|
|
313
433
|
return found;
|
|
314
434
|
};
|
|
315
435
|
|
|
316
436
|
if (opts.lcdUrl) return fetchDirect(opts.lcdUrl);
|
|
317
|
-
const { result } = await tryWithFallback(LCD_ENDPOINTS, fetchDirect, `
|
|
437
|
+
const { result } = await tryWithFallback(LCD_ENDPOINTS, fetchDirect, `node lookup ${nodeAddress}`);
|
|
318
438
|
return result;
|
|
319
439
|
}
|
|
320
440
|
|
|
321
441
|
/**
|
|
322
442
|
* List all sessions for a wallet address.
|
|
443
|
+
* RPC-first with LCD fallback.
|
|
323
444
|
* @param {string} address - sent1... wallet address
|
|
324
445
|
* @param {string} [lcdUrl]
|
|
325
446
|
* @param {object} [opts]
|
|
@@ -327,10 +448,25 @@ export async function queryNode(nodeAddress, opts = {}) {
|
|
|
327
448
|
* @returns {Promise<{ items: ChainSession[], total: number }>}
|
|
328
449
|
*/
|
|
329
450
|
export async function querySessions(address, lcdUrl, opts = {}) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
451
|
+
// RPC-first: returns already-flat decoded sessions
|
|
452
|
+
try {
|
|
453
|
+
const rpc = await getRpcClient();
|
|
454
|
+
if (rpc) {
|
|
455
|
+
const sessions = await rpcQuerySessionsForAccount(rpc, address, { limit: 500 });
|
|
456
|
+
// Filter by status if requested (RPC returns all statuses)
|
|
457
|
+
let items = sessions;
|
|
458
|
+
if (opts.status) {
|
|
459
|
+
const statusNum = parseInt(opts.status, 10);
|
|
460
|
+
items = sessions.filter(s => s.status === statusNum);
|
|
461
|
+
}
|
|
462
|
+
return { items, total: items.length };
|
|
463
|
+
}
|
|
464
|
+
} catch { /* fall through to LCD */ }
|
|
465
|
+
|
|
466
|
+
// LCD fallback
|
|
467
|
+
let lcdPath = `/sentinel/session/v3/sessions?address=${address}`;
|
|
468
|
+
if (opts.status) lcdPath += `&status=${opts.status}`;
|
|
469
|
+
const result = await lcdPaginatedSafe(lcdUrl, lcdPath, 'sessions');
|
|
334
470
|
result.items = result.items.map(flattenSession);
|
|
335
471
|
return result;
|
|
336
472
|
}
|
|
@@ -369,11 +505,22 @@ export function flattenSession(session) {
|
|
|
369
505
|
|
|
370
506
|
/**
|
|
371
507
|
* Get a single subscription by ID.
|
|
508
|
+
* RPC-first with LCD fallback.
|
|
372
509
|
* @param {string|number} id - Subscription ID
|
|
373
510
|
* @param {string} [lcdUrl]
|
|
374
511
|
* @returns {Promise<Subscription|null>}
|
|
375
512
|
*/
|
|
376
513
|
export async function querySubscription(id, lcdUrl) {
|
|
514
|
+
// RPC-first
|
|
515
|
+
try {
|
|
516
|
+
const rpc = await getRpcClient();
|
|
517
|
+
if (rpc) {
|
|
518
|
+
const sub = await rpcQuerySubscription(rpc, id);
|
|
519
|
+
if (sub) return sub;
|
|
520
|
+
}
|
|
521
|
+
} catch { /* fall through */ }
|
|
522
|
+
|
|
523
|
+
// LCD fallback
|
|
377
524
|
try {
|
|
378
525
|
const data = await lcdQuery(`/sentinel/subscription/v3/subscriptions/${id}`, { lcdUrl });
|
|
379
526
|
return data.subscription || null;
|
|
@@ -382,6 +529,7 @@ export async function querySubscription(id, lcdUrl) {
|
|
|
382
529
|
|
|
383
530
|
/**
|
|
384
531
|
* Check if wallet has an active subscription for a specific plan.
|
|
532
|
+
* Uses querySubscriptions which is already RPC-first.
|
|
385
533
|
* @param {string} address - sent1... wallet address
|
|
386
534
|
* @param {number|string} planId - Plan ID to check
|
|
387
535
|
* @param {string} [lcdUrl]
|
|
@@ -396,14 +544,28 @@ export async function hasActiveSubscription(address, planId, lcdUrl) {
|
|
|
396
544
|
|
|
397
545
|
/**
|
|
398
546
|
* 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}.
|
|
547
|
+
* RPC-first with LCD fallback. Uses v2 allocation path (v3 returns 501 on chain).
|
|
401
548
|
*
|
|
402
549
|
* @param {string|number|bigint} subscriptionId
|
|
403
550
|
* @param {string} [lcdUrl]
|
|
404
551
|
* @returns {Promise<Array<{ id: string, address: string, grantedBytes: string, utilisedBytes: string }>>}
|
|
405
552
|
*/
|
|
406
553
|
export async function querySubscriptionAllocations(subscriptionId, lcdUrl) {
|
|
554
|
+
// RPC-first
|
|
555
|
+
try {
|
|
556
|
+
const rpc = await getRpcClient();
|
|
557
|
+
if (rpc) {
|
|
558
|
+
const allocs = await rpcQuerySubAllocations(rpc, subscriptionId, { limit: 100 });
|
|
559
|
+
return allocs.map(a => ({
|
|
560
|
+
id: a.id,
|
|
561
|
+
address: a.address,
|
|
562
|
+
grantedBytes: a.granted_bytes || '0',
|
|
563
|
+
utilisedBytes: a.utilised_bytes || '0',
|
|
564
|
+
}));
|
|
565
|
+
}
|
|
566
|
+
} catch { /* fall through */ }
|
|
567
|
+
|
|
568
|
+
// LCD fallback
|
|
407
569
|
try {
|
|
408
570
|
const data = await lcdQuery(`/sentinel/subscription/v2/subscriptions/${subscriptionId}/allocations`, { lcdUrl });
|
|
409
571
|
return (data.allocations || []).map(a => ({
|
|
@@ -419,6 +581,7 @@ export async function querySubscriptionAllocations(subscriptionId, lcdUrl) {
|
|
|
419
581
|
|
|
420
582
|
/**
|
|
421
583
|
* Query all subscriptions for a plan. Supports owner filtering.
|
|
584
|
+
* RPC-first with LCD fallback.
|
|
422
585
|
*
|
|
423
586
|
* @param {number|string} planId - Plan ID
|
|
424
587
|
* @param {object} [opts]
|
|
@@ -427,12 +590,30 @@ export async function querySubscriptionAllocations(subscriptionId, lcdUrl) {
|
|
|
427
590
|
* @returns {Promise<{ subscribers: Array<{ address: string, status: number, id: string }>, total: number|null }>}
|
|
428
591
|
*/
|
|
429
592
|
export async function queryPlanSubscribers(planId, opts = {}) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
593
|
+
let items;
|
|
594
|
+
let total;
|
|
595
|
+
|
|
596
|
+
// RPC-first
|
|
597
|
+
try {
|
|
598
|
+
const rpc = await getRpcClient();
|
|
599
|
+
if (rpc) {
|
|
600
|
+
items = await rpcQuerySubscriptionsForPlan(rpc, planId, { limit: 500 });
|
|
601
|
+
total = items.length;
|
|
602
|
+
}
|
|
603
|
+
} catch { /* fall through */ }
|
|
604
|
+
|
|
605
|
+
if (!items) {
|
|
606
|
+
// LCD fallback
|
|
607
|
+
const result = await lcdQueryAll(
|
|
608
|
+
`/sentinel/subscription/v3/plans/${planId}/subscriptions`,
|
|
609
|
+
{ lcdUrl: opts.lcdUrl, dataKey: 'subscriptions' },
|
|
610
|
+
);
|
|
611
|
+
items = result.items;
|
|
612
|
+
total = result.total;
|
|
613
|
+
}
|
|
614
|
+
|
|
434
615
|
let subscribers = items.map(s => ({
|
|
435
|
-
address: s.address || s.subscriber,
|
|
616
|
+
address: s.address || s.acc_address || s.subscriber,
|
|
436
617
|
status: s.status,
|
|
437
618
|
id: s.id || s.base_id,
|
|
438
619
|
...s,
|
|
@@ -466,20 +647,28 @@ export async function getPlanStats(planId, ownerAddress, opts = {}) {
|
|
|
466
647
|
// ─── v26: Field Experience Helpers ────────────────────────────────────────────
|
|
467
648
|
|
|
468
649
|
/**
|
|
469
|
-
* Query all nodes linked to a plan.
|
|
650
|
+
* Query all nodes linked to a plan via RPC (primary) with LCD fallback.
|
|
470
651
|
* @param {number|string} planId
|
|
471
652
|
* @param {string} [lcdUrl]
|
|
472
653
|
* @returns {Promise<{ items: any[], total: number|null }>}
|
|
473
654
|
*/
|
|
474
655
|
export async function queryPlanNodes(planId, lcdUrl) {
|
|
475
|
-
//
|
|
476
|
-
|
|
656
|
+
// RPC-first
|
|
657
|
+
try {
|
|
658
|
+
const rpc = await getRpcClient();
|
|
659
|
+
if (rpc) {
|
|
660
|
+
const nodes = await rpcQueryNodesForPlan(rpc, planId, { status: 1, limit: 5000 });
|
|
661
|
+
return { items: nodes, total: nodes.length };
|
|
662
|
+
}
|
|
663
|
+
} catch { /* RPC failed, fall through to LCD */ }
|
|
664
|
+
|
|
665
|
+
// LCD fallback — pagination is BROKEN on this endpoint, single high-limit request
|
|
477
666
|
const doQuery = async (baseUrl) => {
|
|
478
667
|
const data = await lcd(baseUrl, `/sentinel/node/v3/plans/${planId}/nodes?pagination.limit=5000`);
|
|
479
668
|
return { items: data.nodes || [], total: (data.nodes || []).length };
|
|
480
669
|
};
|
|
481
670
|
if (lcdUrl) return doQuery(lcdUrl);
|
|
482
|
-
const { result } = await tryWithFallback(LCD_ENDPOINTS, doQuery, `
|
|
671
|
+
const { result } = await tryWithFallback(LCD_ENDPOINTS, doQuery, `plan ${planId} nodes`);
|
|
483
672
|
return result;
|
|
484
673
|
}
|
|
485
674
|
|