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/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/batch.js
CHANGED
|
@@ -29,10 +29,9 @@ import { sleep } 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
|
|
|
@@ -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/fee-grants.js
CHANGED
|
@@ -16,6 +16,12 @@ import { ValidationError, ErrorCodes } from '../errors.js';
|
|
|
16
16
|
import { lcd, lcdPaginatedSafe, lcdQueryAll } 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,29 +126,59 @@ 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
|
|
131
182
|
try {
|
|
132
183
|
const data = await lcd(lcdUrl, `/cosmos/feegrant/v1beta1/allowance/${granter}/${grantee}`);
|
|
133
184
|
return data.allowance || null;
|