blue-js-sdk 2.0.3 → 2.1.1
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/cli.js +13 -0
- package/ai-path/connect.js +34 -2
- package/ai-path/index.js +5 -0
- package/batch.js +2 -2
- package/chain/broadcast.js +113 -3
- package/chain/queries.js +21 -0
- package/connection/connect.js +22 -7
- package/docs/CHAIN-PROTOCOL-UPGRADE-PROPOSAL.md +662 -0
- package/docs/ON-CHAIN-FUNCTIONS.md +1310 -0
- package/index.js +12 -0
- package/package.json +2 -1
- package/plan-operations.js +18 -11
- package/protocol/encoding.js +38 -24
- package/protocol/messages.js +19 -19
- package/protocol/plans.js +18 -11
- package/protocol/v3.js +11 -7
- package/test-subscription-flows.js +457 -0
- package/v3protocol.js +38 -24
- package/test-all-chain-ops.js +0 -493
- package/test-feegrant-connect.js +0 -98
- package/test-logic.js +0 -148
package/ai-path/cli.js
CHANGED
|
@@ -125,6 +125,9 @@ function showHelp() {
|
|
|
125
125
|
console.log(` --protocol <type> Protocol: wireguard or v2ray`);
|
|
126
126
|
console.log(` --dns <preset> DNS: google, cloudflare, or hns (Handshake)`);
|
|
127
127
|
console.log(` --node <address> Connect to specific node (sentnode1...)`);
|
|
128
|
+
console.log(` --subscription <id> Connect via operator-provisioned subscription`);
|
|
129
|
+
console.log(` --plan <id> Connect via plan (subscribe + start session)`);
|
|
130
|
+
console.log(` --fee-granter <addr> Operator address that pays gas (sent1...)`);
|
|
128
131
|
console.log('');
|
|
129
132
|
console.log(`${c.bold}NODES OPTIONS${c.reset}`);
|
|
130
133
|
console.log(` --country <code> Filter by country`);
|
|
@@ -143,6 +146,10 @@ function showHelp() {
|
|
|
143
146
|
console.log(` sentinel-ai connect --country DE --protocol wireguard`);
|
|
144
147
|
console.log(` sentinel-ai connect --node sentnode1abc...`);
|
|
145
148
|
console.log('');
|
|
149
|
+
console.log(` ${c.dim}# Connect via subscription (operator-provisioned, zero P2P needed)${c.reset}`);
|
|
150
|
+
console.log(` sentinel-ai connect --subscription 1165072 --fee-granter sent1operator...`);
|
|
151
|
+
console.log(` sentinel-ai connect --plan 42 --fee-granter sent1operator...`);
|
|
152
|
+
console.log('');
|
|
146
153
|
console.log(` ${c.dim}# List nodes${c.reset}`);
|
|
147
154
|
console.log(` sentinel-ai nodes --country US --limit 10`);
|
|
148
155
|
console.log('');
|
|
@@ -295,12 +302,18 @@ async function cmdConnect(flags) {
|
|
|
295
302
|
if (flags.protocol) opts.protocol = flags.protocol;
|
|
296
303
|
if (flags.dns) opts.dns = flags.dns;
|
|
297
304
|
if (flags.node) opts.nodeAddress = flags.node;
|
|
305
|
+
if (flags.subscription) opts.subscriptionId = flags.subscription;
|
|
306
|
+
if (flags.plan) opts.planId = flags.plan;
|
|
307
|
+
if (flags['fee-granter']) opts.feeGranter = flags['fee-granter'];
|
|
298
308
|
|
|
299
309
|
console.log(`${info} Connecting to Sentinel dVPN...`);
|
|
300
310
|
if (flags.country) console.log(` Country: ${c.cyan}${flags.country}${c.reset}`);
|
|
301
311
|
if (flags.protocol) console.log(` Protocol: ${c.cyan}${flags.protocol}${c.reset}`);
|
|
302
312
|
if (flags.dns) console.log(` DNS: ${c.cyan}${flags.dns}${c.reset}`);
|
|
303
313
|
if (flags.node) console.log(` Node: ${c.cyan}${flags.node}${c.reset}`);
|
|
314
|
+
if (flags.subscription) console.log(` Subscription: ${c.cyan}${flags.subscription}${c.reset}`);
|
|
315
|
+
if (flags.plan) console.log(` Plan: ${c.cyan}${flags.plan}${c.reset}`);
|
|
316
|
+
if (flags['fee-granter']) console.log(` Fee granter: ${c.cyan}${flags['fee-granter']}${c.reset}`);
|
|
304
317
|
console.log('');
|
|
305
318
|
|
|
306
319
|
const { connect, disconnect } = await import('./index.js');
|
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,
|
|
@@ -245,6 +247,14 @@ async function preValidateBalance(mnemonic) {
|
|
|
245
247
|
/**
|
|
246
248
|
* Connect to Sentinel dVPN. The ONE function an AI agent needs.
|
|
247
249
|
*
|
|
250
|
+
* Three connection modes:
|
|
251
|
+
* 1. Direct payment — agent pays per-session from own wallet (default)
|
|
252
|
+
* 2. Subscription — operator provisioned a subscription for this agent
|
|
253
|
+
* 3. Plan — subscribe to plan + start session (optionally fee-granted)
|
|
254
|
+
*
|
|
255
|
+
* For modes 2 & 3, set opts.feeGranter to the operator's address — the
|
|
256
|
+
* agent can have 0 P2P balance and the operator covers gas.
|
|
257
|
+
*
|
|
248
258
|
* Every step is logged with numbered phases (STEP 1/7 through STEP 7/7)
|
|
249
259
|
* so an autonomous agent can track progress and diagnose failures.
|
|
250
260
|
*
|
|
@@ -254,6 +264,9 @@ async function preValidateBalance(mnemonic) {
|
|
|
254
264
|
* @param {string} [opts.nodeAddress] - Specific node (sentnode1...). Skips auto-pick.
|
|
255
265
|
* @param {string} [opts.dns] - DNS preset: 'google', 'cloudflare', 'hns'
|
|
256
266
|
* @param {string} [opts.protocol] - Preferred protocol: 'wireguard' or 'v2ray'
|
|
267
|
+
* @param {string|number} [opts.subscriptionId] - Connect via existing subscription (operator-provisioned)
|
|
268
|
+
* @param {string|number} [opts.planId] - Connect via plan (subscribes + starts session)
|
|
269
|
+
* @param {string} [opts.feeGranter] - Operator address that pays gas (sent1...). Skips balance check.
|
|
257
270
|
* @param {function} [opts.onProgress] - Progress callback: (stage, message) => void
|
|
258
271
|
* @param {number} [opts.timeout] - Connection timeout in ms (default: 120000 — 2 minutes)
|
|
259
272
|
* @param {boolean} [opts.silent] - If true, suppress step-by-step console output
|
|
@@ -338,13 +351,17 @@ export async function connect(opts = {}) {
|
|
|
338
351
|
const balCheck = await preValidateBalance(opts.mnemonic);
|
|
339
352
|
log(3, totalSteps, 'BALANCE', `Balance: ${balCheck.p2p} | Sufficient: ${balCheck.sufficient}`);
|
|
340
353
|
|
|
341
|
-
|
|
354
|
+
// Skip balance gate when fee granter is set — agent may have 0 P2P, operator pays gas
|
|
355
|
+
if (!balCheck.sufficient && !opts.dryRun && !opts.feeGranter) {
|
|
342
356
|
const err = new Error(`Insufficient balance: ${balCheck.p2p}. Need at least ${formatP2P(MIN_BALANCE_UDVPN)}. Fund address: ${walletAddress}`);
|
|
343
357
|
err.code = 'INSUFFICIENT_BALANCE';
|
|
344
358
|
err.nextAction = 'fund_wallet';
|
|
345
359
|
err.details = { address: walletAddress, balance: balCheck.p2p, minimum: formatP2P(MIN_BALANCE_UDVPN) };
|
|
346
360
|
throw err;
|
|
347
361
|
}
|
|
362
|
+
if (opts.feeGranter && !balCheck.sufficient) {
|
|
363
|
+
log(3, totalSteps, 'BALANCE', `Balance below minimum but fee granter ${opts.feeGranter} covers gas`);
|
|
364
|
+
}
|
|
348
365
|
timings.balance = Date.now() - t0;
|
|
349
366
|
|
|
350
367
|
// ── STEP 4/7: Node Selection ──────────────────────────────────────────────
|
|
@@ -549,7 +566,22 @@ export async function connect(opts = {}) {
|
|
|
549
566
|
try {
|
|
550
567
|
let result;
|
|
551
568
|
|
|
552
|
-
|
|
569
|
+
// ── Connection mode: subscription > plan > direct > auto ──────────────
|
|
570
|
+
if (opts.subscriptionId) {
|
|
571
|
+
// Subscription mode — operator already provisioned a subscription for this agent
|
|
572
|
+
log(5, totalSteps, 'SESSION', `Connecting via subscription ${opts.subscriptionId}${opts.feeGranter ? ' (fee granted)' : ''}...`);
|
|
573
|
+
sdkOpts.subscriptionId = opts.subscriptionId;
|
|
574
|
+
if (opts.feeGranter) sdkOpts.feeGranter = opts.feeGranter;
|
|
575
|
+
if (resolvedNodeAddress) sdkOpts.nodeAddress = resolvedNodeAddress;
|
|
576
|
+
result = await connectViaSubscription(sdkOpts);
|
|
577
|
+
} else if (opts.planId) {
|
|
578
|
+
// Plan mode — subscribe to plan + start session (optionally fee-granted)
|
|
579
|
+
log(5, totalSteps, 'SESSION', `Connecting via plan ${opts.planId}${opts.feeGranter ? ' (fee granted)' : ''}...`);
|
|
580
|
+
sdkOpts.planId = opts.planId;
|
|
581
|
+
if (opts.feeGranter) sdkOpts.feeGranter = opts.feeGranter;
|
|
582
|
+
if (resolvedNodeAddress) sdkOpts.nodeAddress = resolvedNodeAddress;
|
|
583
|
+
result = await connectViaPlan(sdkOpts);
|
|
584
|
+
} else if (resolvedNodeAddress) {
|
|
553
585
|
// Direct connection — either user specified nodeAddress or country discovery found one
|
|
554
586
|
sdkOpts.nodeAddress = resolvedNodeAddress;
|
|
555
587
|
result = await connectDirect(sdkOpts);
|
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/batch.js
CHANGED
|
@@ -142,10 +142,10 @@ async function _processBatch(client, account, batch, gigabytes, denom, ctx) {
|
|
|
142
142
|
typeUrl: MSG_TYPES.START_SESSION,
|
|
143
143
|
value: {
|
|
144
144
|
from: account.address,
|
|
145
|
-
|
|
145
|
+
nodeAddress: node.address,
|
|
146
146
|
gigabytes,
|
|
147
147
|
hours: 0,
|
|
148
|
-
|
|
148
|
+
maxPrice: {
|
|
149
149
|
denom: priceEntry.denom,
|
|
150
150
|
base_value: priceEntry.base_value,
|
|
151
151
|
quote_value: priceEntry.quote_value,
|
package/chain/broadcast.js
CHANGED
|
@@ -338,10 +338,10 @@ export function buildBatchStartSession(from, nodes) {
|
|
|
338
338
|
typeUrl: '/sentinel.node.v3.MsgStartSessionRequest',
|
|
339
339
|
value: {
|
|
340
340
|
from,
|
|
341
|
-
|
|
341
|
+
nodeAddress: n.nodeAddress,
|
|
342
342
|
gigabytes: n.gigabytes || 1,
|
|
343
343
|
hours: 0,
|
|
344
|
-
|
|
344
|
+
maxPrice: n.maxPrice,
|
|
345
345
|
},
|
|
346
346
|
}));
|
|
347
347
|
}
|
|
@@ -382,7 +382,7 @@ export function buildBatchSend(fromAddress, recipients) {
|
|
|
382
382
|
export function buildBatchLink(provAddress, planId, nodeAddresses) {
|
|
383
383
|
return nodeAddresses.map(addr => ({
|
|
384
384
|
typeUrl: '/sentinel.plan.v3.MsgLinkNodeRequest',
|
|
385
|
-
value: { from: provAddress, id: BigInt(planId),
|
|
385
|
+
value: { from: provAddress, id: BigInt(planId), nodeAddress: addr },
|
|
386
386
|
}));
|
|
387
387
|
}
|
|
388
388
|
|
|
@@ -432,6 +432,116 @@ export async function subscribeToPlan(client, fromAddress, planId, denom = 'udvp
|
|
|
432
432
|
return { subscriptionId: BigInt(subId), txHash: result.transactionHash };
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Share a subscription's bandwidth with another address.
|
|
437
|
+
* The chain only supports bytes-based sharing — there is NO time/duration field.
|
|
438
|
+
* For time-based plans (e.g. 1 month), the operator must manually remove the user
|
|
439
|
+
* when the period expires using cancelSubscription or by not renewing.
|
|
440
|
+
*
|
|
441
|
+
* @param {SigningStargateClient} client
|
|
442
|
+
* @param {string} ownerAddress - Subscription owner (sent1...)
|
|
443
|
+
* @param {number|string|bigint} subscriptionId - Subscription to share
|
|
444
|
+
* @param {string} recipientAddress - User to add (sent1...)
|
|
445
|
+
* @param {number|string|bigint} bytes - Bandwidth quota in bytes (e.g. 1073741824 = 1 GB)
|
|
446
|
+
* @returns {Promise<{ txHash: string }>}
|
|
447
|
+
*/
|
|
448
|
+
export async function shareSubscription(client, ownerAddress, subscriptionId, recipientAddress, bytes) {
|
|
449
|
+
const msg = {
|
|
450
|
+
typeUrl: '/sentinel.subscription.v3.MsgShareSubscriptionRequest',
|
|
451
|
+
value: { from: ownerAddress, id: BigInt(subscriptionId), accAddress: recipientAddress, bytes: String(bytes) },
|
|
452
|
+
};
|
|
453
|
+
const result = await broadcast(client, ownerAddress, [msg]);
|
|
454
|
+
return { txHash: result.transactionHash };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Share a subscription with fee grant — operator pays gas on behalf of user.
|
|
459
|
+
* Same as shareSubscription() but uses broadcastWithFeeGrant for the TX fee.
|
|
460
|
+
*
|
|
461
|
+
* @param {SigningStargateClient} client - Client with owner's wallet
|
|
462
|
+
* @param {string} ownerAddress - Subscription owner (sent1...)
|
|
463
|
+
* @param {number|string|bigint} subscriptionId - Subscription to share
|
|
464
|
+
* @param {string} recipientAddress - User to add (sent1...)
|
|
465
|
+
* @param {number|string|bigint} bytes - Bandwidth quota in bytes
|
|
466
|
+
* @param {string} granterAddress - Fee granter address (sent1...)
|
|
467
|
+
* @returns {Promise<{ txHash: string }>}
|
|
468
|
+
*/
|
|
469
|
+
export async function shareSubscriptionWithFeeGrant(client, ownerAddress, subscriptionId, recipientAddress, bytes, granterAddress) {
|
|
470
|
+
const msg = {
|
|
471
|
+
typeUrl: '/sentinel.subscription.v3.MsgShareSubscriptionRequest',
|
|
472
|
+
value: { from: ownerAddress, id: BigInt(subscriptionId), accAddress: recipientAddress, bytes: String(bytes) },
|
|
473
|
+
};
|
|
474
|
+
const result = await broadcastWithFeeGrant(client, ownerAddress, [msg], granterAddress);
|
|
475
|
+
return { txHash: result.transactionHash };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// ─── Plan User Onboarding (composite operation) ────────────────────────────
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Onboard a user to a plan subscription: subscribe → share bandwidth → optional fee grant.
|
|
482
|
+
* This is the complete "add user to plan" operation for plan operators.
|
|
483
|
+
*
|
|
484
|
+
* IMPORTANT: The chain only supports bytes-based sharing. There is NO time/duration
|
|
485
|
+
* field in MsgShareSubscriptionRequest. For time-based plans (e.g. 1 month), the
|
|
486
|
+
* operator must track expiry externally and remove/not-renew the user when time expires.
|
|
487
|
+
*
|
|
488
|
+
* Steps:
|
|
489
|
+
* 1. Subscribe to plan (operator pays, creates subscription)
|
|
490
|
+
* 2. Share the subscription with the user (allocate bytes)
|
|
491
|
+
* 3. Optionally grant fee allowance (so user's TX gas is paid by operator)
|
|
492
|
+
*
|
|
493
|
+
* @param {SigningStargateClient} client - Operator's signing client
|
|
494
|
+
* @param {string} operatorAddress - Plan operator/owner address (sent1...)
|
|
495
|
+
* @param {object} opts
|
|
496
|
+
* @param {number|string|bigint} opts.planId - Plan to subscribe to
|
|
497
|
+
* @param {string} opts.userAddress - User to onboard (sent1...)
|
|
498
|
+
* @param {number|string|bigint} opts.bytes - Bandwidth quota in bytes (e.g. 1073741824 = 1 GB)
|
|
499
|
+
* @param {string} [opts.denom='udvpn'] - Payment denomination
|
|
500
|
+
* @param {boolean} [opts.grantFee=false] - Also grant fee allowance to user
|
|
501
|
+
* @param {number} [opts.feeSpendLimit] - Max spend for fee grant in udvpn (default: 500000 = 0.5 P2P)
|
|
502
|
+
* @param {Date|string} [opts.feeExpiration] - Fee grant expiry date
|
|
503
|
+
* @param {Function} [opts.buildFeeGrant] - Custom buildFeeGrantMsg function (to avoid circular import)
|
|
504
|
+
* @returns {Promise<{ subscriptionId: bigint, shareTxHash: string, grantTxHash?: string }>}
|
|
505
|
+
*/
|
|
506
|
+
export async function onboardPlanUser(client, operatorAddress, opts) {
|
|
507
|
+
const {
|
|
508
|
+
planId, userAddress, bytes,
|
|
509
|
+
denom = 'udvpn',
|
|
510
|
+
grantFee = false,
|
|
511
|
+
feeSpendLimit = 500_000,
|
|
512
|
+
feeExpiration,
|
|
513
|
+
buildFeeGrant,
|
|
514
|
+
} = opts;
|
|
515
|
+
|
|
516
|
+
// Step 1: Subscribe to plan
|
|
517
|
+
const { subscriptionId, txHash: subTxHash } = await subscribeToPlan(client, operatorAddress, planId, denom);
|
|
518
|
+
|
|
519
|
+
// Step 2: Share subscription with user (bytes-based — no time/duration on chain)
|
|
520
|
+
const shareMsg = {
|
|
521
|
+
typeUrl: '/sentinel.subscription.v3.MsgShareSubscriptionRequest',
|
|
522
|
+
value: { from: operatorAddress, id: BigInt(subscriptionId), accAddress: userAddress, bytes: String(bytes) },
|
|
523
|
+
};
|
|
524
|
+
const shareResult = await broadcast(client, operatorAddress, [shareMsg]);
|
|
525
|
+
|
|
526
|
+
const result = {
|
|
527
|
+
subscriptionId,
|
|
528
|
+
subscribeTxHash: subTxHash,
|
|
529
|
+
shareTxHash: shareResult.transactionHash,
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
// Step 3: Optional fee grant
|
|
533
|
+
if (grantFee && buildFeeGrant) {
|
|
534
|
+
const grantMsg = buildFeeGrant(operatorAddress, userAddress, {
|
|
535
|
+
spendLimit: feeSpendLimit,
|
|
536
|
+
expiration: feeExpiration,
|
|
537
|
+
});
|
|
538
|
+
const grantResult = await broadcast(client, operatorAddress, [grantMsg]);
|
|
539
|
+
result.grantTxHash = grantResult.transactionHash;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return result;
|
|
543
|
+
}
|
|
544
|
+
|
|
435
545
|
/**
|
|
436
546
|
* Estimate the cost of starting a session with a node.
|
|
437
547
|
* Supports both gigabyte and hourly pricing. When preferHourly is true and
|
package/chain/queries.js
CHANGED
|
@@ -394,6 +394,27 @@ export async function hasActiveSubscription(address, planId, lcdUrl) {
|
|
|
394
394
|
return { has: false };
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Query allocations for a subscription (who has how many bytes).
|
|
399
|
+
* NOTE: Uses v2 endpoint because this specific v3 query hasn't been implemented yet
|
|
400
|
+
* (returns 501). The v2 path returns the same allocation data. Same situation as /plan/v3/plans/{id}.
|
|
401
|
+
*
|
|
402
|
+
* @param {string|number|bigint} subscriptionId
|
|
403
|
+
* @param {string} [lcdUrl]
|
|
404
|
+
* @returns {Promise<Array<{ id: string, address: string, grantedBytes: string, utilisedBytes: string }>>}
|
|
405
|
+
*/
|
|
406
|
+
export async function querySubscriptionAllocations(subscriptionId, lcdUrl) {
|
|
407
|
+
try {
|
|
408
|
+
const data = await lcdQuery(`/sentinel/subscription/v2/subscriptions/${subscriptionId}/allocations`, { lcdUrl });
|
|
409
|
+
return (data.allocations || []).map(a => ({
|
|
410
|
+
id: a.id,
|
|
411
|
+
address: a.address,
|
|
412
|
+
grantedBytes: a.granted_bytes || '0',
|
|
413
|
+
utilisedBytes: a.utilised_bytes || '0',
|
|
414
|
+
}));
|
|
415
|
+
} catch { return []; }
|
|
416
|
+
}
|
|
417
|
+
|
|
397
418
|
// ─── Plan Subscriber Helpers (v25b) ──────────────────────────────────────────
|
|
398
419
|
|
|
399
420
|
/**
|
package/connection/connect.js
CHANGED
|
@@ -130,7 +130,7 @@ async function connectInternal(opts, paymentStrategy, retryStrategy, state = _de
|
|
|
130
130
|
try {
|
|
131
131
|
const bal = await getBalance(client, account.address);
|
|
132
132
|
progress(onProgress, logFn, 'wallet', `${account.address} | ${bal.dvpn.toFixed(1)} P2P`);
|
|
133
|
-
if (!opts.dryRun && bal.udvpn < 100000) {
|
|
133
|
+
if (!opts.dryRun && !opts.feeGranter && bal.udvpn < 100000) {
|
|
134
134
|
throw new ChainError(ErrorCodes.INSUFFICIENT_BALANCE,
|
|
135
135
|
`Wallet has ${bal.dvpn.toFixed(2)} P2P — need at least 0.1 P2P for a session. Fund address ${account.address} with P2P tokens.`,
|
|
136
136
|
{ balance: bal, address: account.address }
|
|
@@ -440,10 +440,10 @@ export async function connectDirect(opts) {
|
|
|
440
440
|
typeUrl: MSG_TYPES.START_SESSION,
|
|
441
441
|
value: {
|
|
442
442
|
from: account.address,
|
|
443
|
-
|
|
443
|
+
nodeAddress: opts.nodeAddress,
|
|
444
444
|
gigabytes: sessionGigabytes,
|
|
445
445
|
hours: sessionHours,
|
|
446
|
-
|
|
446
|
+
maxPrice: { denom: 'udvpn', base_value: sessionMaxPrice.base_value, quote_value: sessionMaxPrice.quote_value },
|
|
447
447
|
},
|
|
448
448
|
};
|
|
449
449
|
|
|
@@ -477,10 +477,10 @@ export async function connectDirect(opts) {
|
|
|
477
477
|
typeUrl: MSG_TYPES.START_SESSION,
|
|
478
478
|
value: {
|
|
479
479
|
from: account.address,
|
|
480
|
-
|
|
480
|
+
nodeAddress: opts.nodeAddress,
|
|
481
481
|
gigabytes: retryGigabytes,
|
|
482
482
|
hours: retryHours,
|
|
483
|
-
|
|
483
|
+
maxPrice: { denom: 'udvpn', base_value: retryMaxPrice.base_value, quote_value: retryMaxPrice.quote_value },
|
|
484
484
|
},
|
|
485
485
|
};
|
|
486
486
|
checkAborted(signal);
|
|
@@ -769,8 +769,23 @@ export async function connectViaSubscription(opts) {
|
|
|
769
769
|
};
|
|
770
770
|
|
|
771
771
|
checkAborted(signal);
|
|
772
|
-
|
|
773
|
-
|
|
772
|
+
|
|
773
|
+
// Fee grant: operator pays gas for the agent (e.g., x402 managed plan flow)
|
|
774
|
+
const feeGranter = opts.feeGranter || null;
|
|
775
|
+
progress(null, opts.log || defaultLog, 'session', `Starting session via subscription ${opts.subscriptionId}${feeGranter ? ' (fee granted)' : ''}...`);
|
|
776
|
+
|
|
777
|
+
let result;
|
|
778
|
+
if (feeGranter) {
|
|
779
|
+
try {
|
|
780
|
+
result = await broadcastWithFeeGrant(client, account.address, [msg], feeGranter);
|
|
781
|
+
} catch (feeErr) {
|
|
782
|
+
// Fee grant TX failed — fall back to user-paid
|
|
783
|
+
progress(null, opts.log || defaultLog, 'session', 'Fee grant failed, paying gas from wallet...');
|
|
784
|
+
result = await broadcastWithInactiveRetry(client, account.address, [msg], opts.log || defaultLog, opts.onProgress);
|
|
785
|
+
}
|
|
786
|
+
} else {
|
|
787
|
+
result = await broadcastWithInactiveRetry(client, account.address, [msg], opts.log || defaultLog, opts.onProgress);
|
|
788
|
+
}
|
|
774
789
|
const extracted = extractId(result, /session/i, ['session_id', 'id']);
|
|
775
790
|
if (!extracted) throw new ChainError(ErrorCodes.SESSION_EXTRACT_FAILED, 'Failed to extract session ID from subscription TX result', { txHash: result.transactionHash });
|
|
776
791
|
const sessionId = BigInt(extracted);
|