blue-js-sdk 2.0.1 → 2.1.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/index.js CHANGED
@@ -58,8 +58,17 @@ export {
58
58
  rpcQueryBalance,
59
59
  rpcQueryNode,
60
60
  // v1.5.2: Session recovery (referenced in docs but was missing from exports)
61
- recoverOrphans,
61
+ recoverOrphans as recoverSession,
62
+ // v2.0.2: Plan operations for AI agents using subscription-based access
63
+ connectViaSubscription,
64
+ connectViaPlan,
65
+ subscribeToPlan,
66
+ hasActiveSubscription,
67
+ querySubscriptions,
68
+ queryPlanNodes,
69
+ queryFeeGrants,
70
+ buildFeeGrantMsg,
71
+ broadcastWithFeeGrant,
72
+ rpcQueryNodesForPlan,
73
+ rpcQuerySubscriptionsForAccount,
62
74
  } from '../index.js';
63
-
64
- // Re-export recoverOrphans as recoverSession (the name used in ai-path docs)
65
- export { recoverOrphans as recoverSession };
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
- node_address: node.address,
145
+ nodeAddress: node.address,
146
146
  gigabytes,
147
147
  hours: 0,
148
- max_price: {
148
+ maxPrice: {
149
149
  denom: priceEntry.denom,
150
150
  base_value: priceEntry.base_value,
151
151
  quote_value: priceEntry.quote_value,
@@ -338,10 +338,10 @@ export function buildBatchStartSession(from, nodes) {
338
338
  typeUrl: '/sentinel.node.v3.MsgStartSessionRequest',
339
339
  value: {
340
340
  from,
341
- node_address: n.nodeAddress,
341
+ nodeAddress: n.nodeAddress,
342
342
  gigabytes: n.gigabytes || 1,
343
343
  hours: 0,
344
- max_price: n.maxPrice,
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), node_address: addr },
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/client.js CHANGED
@@ -43,7 +43,8 @@ import {
43
43
  encodeMsgEndLease,
44
44
  } from '../plan-operations.js';
45
45
 
46
- import { GAS_PRICE } from '../defaults.js';
46
+ import { GAS_PRICE, RPC_ENDPOINTS } from '../defaults.js';
47
+ import { ValidationError, ChainError, ErrorCodes } from '../errors.js';
47
48
 
48
49
  // ─── CosmJS Registry ─────────────────────────────────────────────────────────
49
50
 
@@ -107,12 +108,59 @@ export function buildRegistry() {
107
108
  /**
108
109
  * Create a SigningStargateClient connected to Sentinel RPC.
109
110
  * Gas price: from defaults.js GAS_PRICE (chain minimum).
111
+ *
112
+ * Signatures:
113
+ * createClient(rpcUrl, wallet) — classic: connect to specific RPC with existing wallet
114
+ * createClient(mnemonic) — convenience: create wallet from mnemonic, try RPC endpoints with failover
115
+ *
116
+ * @param {string} rpcUrlOrMnemonic - Either an RPC URL (https://...) or a BIP39 mnemonic
117
+ * @param {DirectSecp256k1HdWallet} [wallet] - Wallet object (required when first arg is RPC URL)
118
+ * @returns {Promise<SigningStargateClient>} Connected signing client with full Sentinel registry
110
119
  */
111
- export async function createClient(rpcUrl, wallet) {
112
- return SigningStargateClient.connectWithSigner(rpcUrl, wallet, {
113
- gasPrice: GasPrice.fromString(GAS_PRICE),
114
- registry: buildRegistry(),
115
- });
120
+ export async function createClient(rpcUrlOrMnemonic, wallet) {
121
+ // Classic call: createClient(rpcUrl, wallet)
122
+ if (wallet) {
123
+ return SigningStargateClient.connectWithSigner(rpcUrlOrMnemonic, wallet, {
124
+ gasPrice: GasPrice.fromString(GAS_PRICE),
125
+ registry: buildRegistry(),
126
+ });
127
+ }
128
+
129
+ // If first arg looks like a URL, it's a missing wallet — throw helpful error
130
+ if (typeof rpcUrlOrMnemonic === 'string' && /^(https?|wss?):\/\//i.test(rpcUrlOrMnemonic)) {
131
+ throw new ValidationError(ErrorCodes.INVALID_MNEMONIC,
132
+ 'createClient(rpcUrl, wallet): wallet parameter is required when passing an RPC URL. ' +
133
+ 'Use createClient(mnemonic) for convenience, or createClient(rpcUrl, wallet) with an existing wallet.',
134
+ { value: rpcUrlOrMnemonic });
135
+ }
136
+
137
+ // Convenience call: createClient(mnemonic) — create wallet + try RPC endpoints
138
+ // Lazy import to avoid circular dependency
139
+ const { createWallet, validateMnemonic } = await import('./wallet.js');
140
+ validateMnemonic(rpcUrlOrMnemonic, 'createClient');
141
+ const { wallet: derivedWallet } = await createWallet(rpcUrlOrMnemonic);
142
+ const registry = buildRegistry();
143
+ const gasPrice = GasPrice.fromString(GAS_PRICE);
144
+
145
+ // Try each RPC endpoint until one connects
146
+ const errors = [];
147
+ for (const ep of RPC_ENDPOINTS) {
148
+ try {
149
+ const client = await SigningStargateClient.connectWithSigner(ep.url, derivedWallet, {
150
+ gasPrice,
151
+ registry,
152
+ });
153
+ return client;
154
+ } catch (err) {
155
+ errors.push({ endpoint: ep.url, name: ep.name, error: err.message });
156
+ }
157
+ }
158
+
159
+ // All endpoints failed
160
+ const tried = errors.map(e => ` ${e.name} (${e.endpoint}): ${e.error}`).join('\n');
161
+ throw new ChainError('ALL_ENDPOINTS_FAILED',
162
+ `createClient(mnemonic): failed to connect to all ${RPC_ENDPOINTS.length} RPC endpoints:\n${tried}`,
163
+ { endpoints: errors });
116
164
  }
117
165
 
118
166
  // ─── All Type URL Constants ──────────────────────────────────────────────────
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
  /**
@@ -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
- node_address: opts.nodeAddress,
443
+ nodeAddress: opts.nodeAddress,
444
444
  gigabytes: sessionGigabytes,
445
445
  hours: sessionHours,
446
- max_price: { denom: 'udvpn', base_value: sessionMaxPrice.base_value, quote_value: sessionMaxPrice.quote_value },
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
- node_address: opts.nodeAddress,
480
+ nodeAddress: opts.nodeAddress,
481
481
  gigabytes: retryGigabytes,
482
482
  hours: retryHours,
483
- max_price: { denom: 'udvpn', base_value: retryMaxPrice.base_value, quote_value: retryMaxPrice.quote_value },
483
+ maxPrice: { denom: 'udvpn', base_value: retryMaxPrice.base_value, quote_value: retryMaxPrice.quote_value },
484
484
  },
485
485
  };
486
486
  checkAborted(signal);
package/cosmjs-setup.js CHANGED
@@ -50,7 +50,7 @@ import {
50
50
  encodeMsgStartLease,
51
51
  encodeMsgEndLease,
52
52
  } from './plan-operations.js';
53
- import { GAS_PRICE, LCD_ENDPOINTS, tryWithFallback } from './defaults.js';
53
+ import { GAS_PRICE, RPC_ENDPOINTS, LCD_ENDPOINTS, tryWithFallback } from './defaults.js';
54
54
  import { ValidationError, NodeError, ChainError, ErrorCodes } from './errors.js';
55
55
  import path from 'path';
56
56
  import os from 'os';
@@ -216,12 +216,57 @@ export function buildRegistry() {
216
216
  /**
217
217
  * Create a SigningStargateClient connected to Sentinel RPC.
218
218
  * Gas price: from defaults.js GAS_PRICE (chain minimum).
219
- */
220
- export async function createClient(rpcUrl, wallet) {
221
- return SigningStargateClient.connectWithSigner(rpcUrl, wallet, {
222
- gasPrice: GasPrice.fromString(GAS_PRICE),
223
- registry: buildRegistry(),
224
- });
219
+ *
220
+ * Signatures:
221
+ * createClient(rpcUrl, wallet) — classic: connect to specific RPC with existing wallet
222
+ * createClient(mnemonic) — convenience: create wallet from mnemonic, try RPC endpoints with failover
223
+ *
224
+ * @param {string} rpcUrlOrMnemonic - Either an RPC URL (https://...) or a BIP39 mnemonic
225
+ * @param {DirectSecp256k1HdWallet} [wallet] - Wallet object (required when first arg is RPC URL)
226
+ * @returns {Promise<SigningStargateClient>} Connected signing client with full Sentinel registry
227
+ */
228
+ export async function createClient(rpcUrlOrMnemonic, wallet) {
229
+ // Classic call: createClient(rpcUrl, wallet)
230
+ if (wallet) {
231
+ return SigningStargateClient.connectWithSigner(rpcUrlOrMnemonic, wallet, {
232
+ gasPrice: GasPrice.fromString(GAS_PRICE),
233
+ registry: buildRegistry(),
234
+ });
235
+ }
236
+
237
+ // If first arg looks like a URL, it's a missing wallet — throw helpful error
238
+ if (typeof rpcUrlOrMnemonic === 'string' && /^(https?|wss?):\/\//i.test(rpcUrlOrMnemonic)) {
239
+ throw new ValidationError(ErrorCodes.INVALID_MNEMONIC,
240
+ 'createClient(rpcUrl, wallet): wallet parameter is required when passing an RPC URL. ' +
241
+ 'Use createClient(mnemonic) for convenience, or createClient(rpcUrl, wallet) with an existing wallet.',
242
+ { value: rpcUrlOrMnemonic });
243
+ }
244
+
245
+ // Convenience call: createClient(mnemonic) — create wallet + try RPC endpoints
246
+ validateMnemonic(rpcUrlOrMnemonic, 'createClient');
247
+ const { wallet: derivedWallet } = await createWallet(rpcUrlOrMnemonic);
248
+ const registry = buildRegistry();
249
+ const gasPrice = GasPrice.fromString(GAS_PRICE);
250
+
251
+ // Try each RPC endpoint until one connects
252
+ const errors = [];
253
+ for (const ep of RPC_ENDPOINTS) {
254
+ try {
255
+ const client = await SigningStargateClient.connectWithSigner(ep.url, derivedWallet, {
256
+ gasPrice,
257
+ registry,
258
+ });
259
+ return client;
260
+ } catch (err) {
261
+ errors.push({ endpoint: ep.url, name: ep.name, error: err.message });
262
+ }
263
+ }
264
+
265
+ // All endpoints failed
266
+ const tried = errors.map(e => ` ${e.name} (${e.endpoint}): ${e.error}`).join('\n');
267
+ throw new ChainError('ALL_ENDPOINTS_FAILED',
268
+ `createClient(mnemonic): failed to connect to all ${RPC_ENDPOINTS.length} RPC endpoints:\n${tried}`,
269
+ { endpoints: errors });
225
270
  }
226
271
 
227
272
  // ─── TX Helpers ──────────────────────────────────────────────────────────────