blue-js-sdk 2.1.0 → 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/cli.js +13 -0
- package/ai-path/connect.js +146 -2
- package/ai-path/index.js +5 -0
- package/ai-path/pricing.js +5 -4
- package/chain/queries.js +242 -53
- package/chain/rpc.js +339 -7
- package/connection/connect.js +18 -3
- 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
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
import {
|
|
20
20
|
connectAuto,
|
|
21
21
|
connectDirect,
|
|
22
|
+
connectViaSubscription,
|
|
23
|
+
connectViaPlan,
|
|
22
24
|
disconnect as sdkDisconnect,
|
|
23
25
|
isConnected,
|
|
24
26
|
getStatus,
|
|
@@ -32,9 +34,12 @@ import {
|
|
|
32
34
|
getBalance as sdkGetBalance,
|
|
33
35
|
tryWithFallback,
|
|
34
36
|
RPC_ENDPOINTS,
|
|
37
|
+
LCD_ENDPOINTS,
|
|
38
|
+
queryFeeGrant,
|
|
35
39
|
// v1.5.0: RPC queries (protobuf, ~10x faster than LCD for balance checks)
|
|
36
40
|
createRpcQueryClientWithFallback,
|
|
37
41
|
rpcQueryBalance,
|
|
42
|
+
rpcQueryFeeGrant,
|
|
38
43
|
// v1.5.0: Typed event parsers (replaces string matching for session ID extraction)
|
|
39
44
|
extractSessionIdTyped,
|
|
40
45
|
NodeEventCreateSession,
|
|
@@ -191,6 +196,18 @@ function humanError(err) {
|
|
|
191
196
|
message: 'Connection was cancelled.',
|
|
192
197
|
nextAction: 'none',
|
|
193
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
|
+
},
|
|
194
211
|
};
|
|
195
212
|
|
|
196
213
|
const entry = messages[code];
|
|
@@ -245,6 +262,14 @@ async function preValidateBalance(mnemonic) {
|
|
|
245
262
|
/**
|
|
246
263
|
* Connect to Sentinel dVPN. The ONE function an AI agent needs.
|
|
247
264
|
*
|
|
265
|
+
* Three connection modes:
|
|
266
|
+
* 1. Direct payment — agent pays per-session from own wallet (default)
|
|
267
|
+
* 2. Subscription — operator provisioned a subscription for this agent
|
|
268
|
+
* 3. Plan — subscribe to plan + start session (optionally fee-granted)
|
|
269
|
+
*
|
|
270
|
+
* For modes 2 & 3, set opts.feeGranter to the operator's address — the
|
|
271
|
+
* agent can have 0 P2P balance and the operator covers gas.
|
|
272
|
+
*
|
|
248
273
|
* Every step is logged with numbered phases (STEP 1/7 through STEP 7/7)
|
|
249
274
|
* so an autonomous agent can track progress and diagnose failures.
|
|
250
275
|
*
|
|
@@ -254,6 +279,9 @@ async function preValidateBalance(mnemonic) {
|
|
|
254
279
|
* @param {string} [opts.nodeAddress] - Specific node (sentnode1...). Skips auto-pick.
|
|
255
280
|
* @param {string} [opts.dns] - DNS preset: 'google', 'cloudflare', 'hns'
|
|
256
281
|
* @param {string} [opts.protocol] - Preferred protocol: 'wireguard' or 'v2ray'
|
|
282
|
+
* @param {string|number} [opts.subscriptionId] - Connect via existing subscription (operator-provisioned)
|
|
283
|
+
* @param {string|number} [opts.planId] - Connect via plan (subscribes + starts session)
|
|
284
|
+
* @param {string} [opts.feeGranter] - Operator address that pays gas (sent1...). Skips balance check.
|
|
257
285
|
* @param {function} [opts.onProgress] - Progress callback: (stage, message) => void
|
|
258
286
|
* @param {number} [opts.timeout] - Connection timeout in ms (default: 120000 — 2 minutes)
|
|
259
287
|
* @param {boolean} [opts.silent] - If true, suppress step-by-step console output
|
|
@@ -338,15 +366,114 @@ export async function connect(opts = {}) {
|
|
|
338
366
|
const balCheck = await preValidateBalance(opts.mnemonic);
|
|
339
367
|
log(3, totalSteps, 'BALANCE', `Balance: ${balCheck.p2p} | Sufficient: ${balCheck.sufficient}`);
|
|
340
368
|
|
|
341
|
-
|
|
369
|
+
// Skip balance gate when fee granter is set — agent may have 0 P2P, operator pays gas
|
|
370
|
+
if (!balCheck.sufficient && !opts.dryRun && !opts.feeGranter) {
|
|
342
371
|
const err = new Error(`Insufficient balance: ${balCheck.p2p}. Need at least ${formatP2P(MIN_BALANCE_UDVPN)}. Fund address: ${walletAddress}`);
|
|
343
372
|
err.code = 'INSUFFICIENT_BALANCE';
|
|
344
373
|
err.nextAction = 'fund_wallet';
|
|
345
374
|
err.details = { address: walletAddress, balance: balCheck.p2p, minimum: formatP2P(MIN_BALANCE_UDVPN) };
|
|
346
375
|
throw err;
|
|
347
376
|
}
|
|
377
|
+
if (opts.feeGranter && !balCheck.sufficient) {
|
|
378
|
+
log(3, totalSteps, 'BALANCE', `Balance below minimum but fee granter ${opts.feeGranter} covers gas`);
|
|
379
|
+
}
|
|
348
380
|
timings.balance = Date.now() - t0;
|
|
349
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
|
+
|
|
350
477
|
// ── STEP 4/7: Node Selection ──────────────────────────────────────────────
|
|
351
478
|
|
|
352
479
|
t0 = Date.now();
|
|
@@ -549,14 +676,31 @@ export async function connect(opts = {}) {
|
|
|
549
676
|
try {
|
|
550
677
|
let result;
|
|
551
678
|
|
|
552
|
-
|
|
679
|
+
// ── Connection mode: subscription > plan > direct > auto ──────────────
|
|
680
|
+
if (opts.subscriptionId) {
|
|
681
|
+
// Subscription mode — operator already provisioned a subscription for this agent
|
|
682
|
+
log(5, totalSteps, 'SESSION', `Connecting via subscription ${opts.subscriptionId}${opts.feeGranter ? ' (fee granted)' : ''}...`);
|
|
683
|
+
sdkOpts.subscriptionId = opts.subscriptionId;
|
|
684
|
+
if (opts.feeGranter) sdkOpts.feeGranter = opts.feeGranter;
|
|
685
|
+
if (resolvedNodeAddress) sdkOpts.nodeAddress = resolvedNodeAddress;
|
|
686
|
+
result = await connectViaSubscription(sdkOpts);
|
|
687
|
+
} else if (opts.planId) {
|
|
688
|
+
// Plan mode — subscribe to plan + start session (optionally fee-granted)
|
|
689
|
+
log(5, totalSteps, 'SESSION', `Connecting via plan ${opts.planId}${opts.feeGranter ? ' (fee granted)' : ''}...`);
|
|
690
|
+
sdkOpts.planId = opts.planId;
|
|
691
|
+
if (opts.feeGranter) sdkOpts.feeGranter = opts.feeGranter;
|
|
692
|
+
if (resolvedNodeAddress) sdkOpts.nodeAddress = resolvedNodeAddress;
|
|
693
|
+
result = await connectViaPlan(sdkOpts);
|
|
694
|
+
} else if (resolvedNodeAddress) {
|
|
553
695
|
// Direct connection — either user specified nodeAddress or country discovery found one
|
|
554
696
|
sdkOpts.nodeAddress = resolvedNodeAddress;
|
|
697
|
+
if (opts.feeGranter) sdkOpts.feeGranter = opts.feeGranter;
|
|
555
698
|
result = await connectDirect(sdkOpts);
|
|
556
699
|
} else {
|
|
557
700
|
// No country filter or country discovery found nothing — auto-select globally
|
|
558
701
|
// Use higher maxAttempts to search more nodes
|
|
559
702
|
if (!sdkOpts.maxAttempts) sdkOpts.maxAttempts = 5;
|
|
703
|
+
if (opts.feeGranter) sdkOpts.feeGranter = opts.feeGranter;
|
|
560
704
|
result = await connectAuto(sdkOpts);
|
|
561
705
|
}
|
|
562
706
|
|
package/ai-path/index.js
CHANGED
|
@@ -65,10 +65,15 @@ export {
|
|
|
65
65
|
subscribeToPlan,
|
|
66
66
|
hasActiveSubscription,
|
|
67
67
|
querySubscriptions,
|
|
68
|
+
querySubscriptionAllocations,
|
|
68
69
|
queryPlanNodes,
|
|
69
70
|
queryFeeGrants,
|
|
70
71
|
buildFeeGrantMsg,
|
|
71
72
|
broadcastWithFeeGrant,
|
|
73
|
+
// v2.1.0: Subscription sharing + onboarding (operator provisions agent access)
|
|
74
|
+
shareSubscription,
|
|
75
|
+
shareSubscriptionWithFeeGrant,
|
|
76
|
+
onboardPlanUser,
|
|
72
77
|
rpcQueryNodesForPlan,
|
|
73
78
|
rpcQuerySubscriptionsForAccount,
|
|
74
79
|
} from '../index.js';
|
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 = {
|