blue-js-sdk 2.0.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.
Files changed (215) hide show
  1. package/CHANGELOG.md +446 -0
  2. package/LICENSE +21 -0
  3. package/README.md +75 -0
  4. package/ai-path/ADMIN-ELEVATION.md +116 -0
  5. package/ai-path/AI-MANIFESTO.md +185 -0
  6. package/ai-path/BREAKING.md +74 -0
  7. package/ai-path/CHECKLIST.md +619 -0
  8. package/ai-path/CONNECTION-STEPS.md +724 -0
  9. package/ai-path/DECISION-TREE.md +378 -0
  10. package/ai-path/DEPENDENCIES.md +459 -0
  11. package/ai-path/E2E-FLOW.md +1555 -0
  12. package/ai-path/FAILURES.md +403 -0
  13. package/ai-path/GUIDE.md +1217 -0
  14. package/ai-path/README.md +558 -0
  15. package/ai-path/SPLIT-TUNNEL.md +266 -0
  16. package/ai-path/cli.js +535 -0
  17. package/ai-path/connect.js +884 -0
  18. package/ai-path/discover.js +178 -0
  19. package/ai-path/environment.js +266 -0
  20. package/ai-path/errors.js +86 -0
  21. package/ai-path/examples/autonomous-agent.mjs +220 -0
  22. package/ai-path/examples/multi-region.mjs +174 -0
  23. package/ai-path/examples/one-shot.mjs +31 -0
  24. package/ai-path/index.js +60 -0
  25. package/ai-path/pricing.js +136 -0
  26. package/ai-path/recommend.js +413 -0
  27. package/ai-path/run-admin.vbs +25 -0
  28. package/ai-path/setup.js +291 -0
  29. package/ai-path/wallet.js +137 -0
  30. package/app-helpers.js +363 -0
  31. package/app-settings.js +95 -0
  32. package/app-types.js +267 -0
  33. package/audit.js +847 -0
  34. package/batch.js +293 -0
  35. package/bin/setup.js +376 -0
  36. package/chain/authz.js +109 -0
  37. package/chain/broadcast.js +472 -0
  38. package/chain/client.js +160 -0
  39. package/chain/fee-grants.js +305 -0
  40. package/chain/index.js +891 -0
  41. package/chain/lcd.js +313 -0
  42. package/chain/queries.js +547 -0
  43. package/chain/rpc.js +408 -0
  44. package/chain/wallet.js +141 -0
  45. package/cli/config.js +143 -0
  46. package/cli/index.js +463 -0
  47. package/cli/output.js +182 -0
  48. package/cli.js +491 -0
  49. package/client/index.js +251 -0
  50. package/client.js +271 -0
  51. package/config/index.js +255 -0
  52. package/connection/connect.js +849 -0
  53. package/connection/disconnect.js +180 -0
  54. package/connection/discovery.js +321 -0
  55. package/connection/index.js +76 -0
  56. package/connection/proxy.js +148 -0
  57. package/connection/resilience.js +428 -0
  58. package/connection/security.js +232 -0
  59. package/connection/state.js +369 -0
  60. package/connection/tunnel.js +691 -0
  61. package/consumer.js +132 -0
  62. package/cosmjs-setup.js +1884 -0
  63. package/defaults.js +366 -0
  64. package/disk-cache.js +107 -0
  65. package/dist/client.d.ts +108 -0
  66. package/dist/client.d.ts.map +1 -0
  67. package/dist/client.js +400 -0
  68. package/dist/client.js.map +1 -0
  69. package/dist/index.d.ts +8 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +8 -0
  72. package/dist/index.js.map +1 -0
  73. package/errors/index.js +112 -0
  74. package/errors.js +218 -0
  75. package/examples/README.md +64 -0
  76. package/examples/connect-direct.mjs +106 -0
  77. package/examples/connect-plan.mjs +125 -0
  78. package/examples/error-handling.mjs +109 -0
  79. package/examples/query-nodes.mjs +94 -0
  80. package/examples/wallet-basics.mjs +61 -0
  81. package/generated/amino/amino.ts +9 -0
  82. package/generated/cosmos/base/v1beta1/coin.ts +365 -0
  83. package/generated/cosmos_proto/cosmos.ts +323 -0
  84. package/generated/gogoproto/gogo.ts +9 -0
  85. package/generated/google/protobuf/descriptor.ts +7601 -0
  86. package/generated/google/protobuf/duration.ts +208 -0
  87. package/generated/google/protobuf/timestamp.ts +238 -0
  88. package/generated/sentinel/lease/v1/events.ts +924 -0
  89. package/generated/sentinel/lease/v1/lease.ts +292 -0
  90. package/generated/sentinel/lease/v1/msg.ts +949 -0
  91. package/generated/sentinel/lease/v1/params.ts +164 -0
  92. package/generated/sentinel/node/v3/events.ts +881 -0
  93. package/generated/sentinel/node/v3/msg.ts +1002 -0
  94. package/generated/sentinel/node/v3/node.ts +263 -0
  95. package/generated/sentinel/node/v3/params.ts +183 -0
  96. package/generated/sentinel/plan/v3/events.ts +675 -0
  97. package/generated/sentinel/plan/v3/msg.ts +1191 -0
  98. package/generated/sentinel/plan/v3/plan.ts +283 -0
  99. package/generated/sentinel/provider/v2/events.ts +171 -0
  100. package/generated/sentinel/provider/v2/msg.ts +480 -0
  101. package/generated/sentinel/provider/v2/params.ts +131 -0
  102. package/generated/sentinel/provider/v2/provider.ts +246 -0
  103. package/generated/sentinel/session/v3/events.ts +480 -0
  104. package/generated/sentinel/session/v3/msg.ts +616 -0
  105. package/generated/sentinel/session/v3/params.ts +260 -0
  106. package/generated/sentinel/session/v3/proof.ts +180 -0
  107. package/generated/sentinel/session/v3/session.ts +384 -0
  108. package/generated/sentinel/subscription/v3/events.ts +1181 -0
  109. package/generated/sentinel/subscription/v3/msg.ts +1305 -0
  110. package/generated/sentinel/subscription/v3/params.ts +167 -0
  111. package/generated/sentinel/subscription/v3/subscription.ts +315 -0
  112. package/generated/sentinel/types/v1/bandwidth.ts +124 -0
  113. package/generated/sentinel/types/v1/price.ts +149 -0
  114. package/generated/sentinel/types/v1/renewal.ts +87 -0
  115. package/generated/sentinel/types/v1/status.ts +54 -0
  116. package/generated/typeRegistry.ts +27 -0
  117. package/index.js +486 -0
  118. package/node-connect.js +3015 -0
  119. package/operator.js +134 -0
  120. package/package.json +113 -0
  121. package/plan-operations.js +199 -0
  122. package/preflight.js +352 -0
  123. package/pricing/index.js +262 -0
  124. package/proto/amino/amino.proto +84 -0
  125. package/proto/cosmos/base/v1beta1/coin.proto +61 -0
  126. package/proto/cosmos_proto/cosmos.proto +112 -0
  127. package/proto/gogoproto/gogo.proto +145 -0
  128. package/proto/google/api/annotations.proto +31 -0
  129. package/proto/google/api/http.proto +370 -0
  130. package/proto/google/protobuf/any.proto +106 -0
  131. package/proto/google/protobuf/duration.proto +115 -0
  132. package/proto/google/protobuf/timestamp.proto +145 -0
  133. package/proto/sentinel/lease/v1/events.proto +52 -0
  134. package/proto/sentinel/lease/v1/genesis.proto +15 -0
  135. package/proto/sentinel/lease/v1/lease.proto +25 -0
  136. package/proto/sentinel/lease/v1/msg.proto +62 -0
  137. package/proto/sentinel/lease/v1/params.proto +17 -0
  138. package/proto/sentinel/node/v3/events.proto +50 -0
  139. package/proto/sentinel/node/v3/genesis.proto +15 -0
  140. package/proto/sentinel/node/v3/msg.proto +63 -0
  141. package/proto/sentinel/node/v3/node.proto +27 -0
  142. package/proto/sentinel/node/v3/params.proto +21 -0
  143. package/proto/sentinel/node/v3/querier.proto +63 -0
  144. package/proto/sentinel/plan/v3/events.proto +41 -0
  145. package/proto/sentinel/plan/v3/genesis.proto +21 -0
  146. package/proto/sentinel/plan/v3/msg.proto +83 -0
  147. package/proto/sentinel/plan/v3/plan.proto +32 -0
  148. package/proto/sentinel/plan/v3/querier.proto +53 -0
  149. package/proto/sentinel/provider/v2/events.proto +16 -0
  150. package/proto/sentinel/provider/v2/genesis.proto +15 -0
  151. package/proto/sentinel/provider/v2/msg.proto +35 -0
  152. package/proto/sentinel/provider/v2/params.proto +17 -0
  153. package/proto/sentinel/provider/v2/provider.proto +24 -0
  154. package/proto/sentinel/provider/v3/genesis.proto +15 -0
  155. package/proto/sentinel/provider/v3/params.proto +13 -0
  156. package/proto/sentinel/session/v3/events.proto +30 -0
  157. package/proto/sentinel/session/v3/genesis.proto +15 -0
  158. package/proto/sentinel/session/v3/msg.proto +50 -0
  159. package/proto/sentinel/session/v3/params.proto +25 -0
  160. package/proto/sentinel/session/v3/proof.proto +25 -0
  161. package/proto/sentinel/session/v3/querier.proto +100 -0
  162. package/proto/sentinel/session/v3/session.proto +50 -0
  163. package/proto/sentinel/subscription/v2/allocation.proto +21 -0
  164. package/proto/sentinel/subscription/v2/payout.proto +22 -0
  165. package/proto/sentinel/subscription/v3/events.proto +65 -0
  166. package/proto/sentinel/subscription/v3/genesis.proto +17 -0
  167. package/proto/sentinel/subscription/v3/msg.proto +83 -0
  168. package/proto/sentinel/subscription/v3/params.proto +21 -0
  169. package/proto/sentinel/subscription/v3/subscription.proto +33 -0
  170. package/proto/sentinel/types/v1/bandwidth.proto +19 -0
  171. package/proto/sentinel/types/v1/price.proto +21 -0
  172. package/proto/sentinel/types/v1/renewal.proto +21 -0
  173. package/proto/sentinel/types/v1/status.proto +16 -0
  174. package/protocol/encoding.js +341 -0
  175. package/protocol/events.js +361 -0
  176. package/protocol/handshake.js +297 -0
  177. package/protocol/index.js +15 -0
  178. package/protocol/messages.js +346 -0
  179. package/protocol/plans.js +199 -0
  180. package/protocol/v2ray.js +268 -0
  181. package/protocol/v3.js +723 -0
  182. package/protocol/wireguard.js +125 -0
  183. package/security/index.js +132 -0
  184. package/session-manager.js +329 -0
  185. package/session-tracker.js +80 -0
  186. package/setup.js +376 -0
  187. package/speedtest/index.js +528 -0
  188. package/speedtest.js +567 -0
  189. package/src/client.ts +502 -0
  190. package/src/index.ts +20 -0
  191. package/state/index.js +347 -0
  192. package/state.js +516 -0
  193. package/test-all-chain-ops.js +493 -0
  194. package/test-all-logic.js +199 -0
  195. package/test-all-msg-types.js +292 -0
  196. package/test-every-connection.js +208 -0
  197. package/test-feegrant-connect.js +98 -0
  198. package/test-logic.js +148 -0
  199. package/test-mainnet.js +176 -0
  200. package/test-plan-lifecycle.js +335 -0
  201. package/tls-trust.js +132 -0
  202. package/tsconfig.build.json +20 -0
  203. package/tsconfig.json +34 -0
  204. package/types/chain.d.ts +746 -0
  205. package/types/connection.d.ts +425 -0
  206. package/types/errors.d.ts +174 -0
  207. package/types/index.d.ts +1380 -0
  208. package/types/nodes.d.ts +187 -0
  209. package/types/pricing.d.ts +156 -0
  210. package/types/protocol.d.ts +332 -0
  211. package/types/session.d.ts +236 -0
  212. package/types/settings.d.ts +192 -0
  213. package/v3protocol.js +1053 -0
  214. package/wallet/index.js +153 -0
  215. package/wireguard.js +307 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Sentinel SDK — Chain / Client Module
3
+ *
4
+ * CosmJS Registry building, SigningStargateClient creation,
5
+ * and all MSG_TYPES constants.
6
+ *
7
+ * Usage:
8
+ * import { createClient, buildRegistry, MSG_TYPES } from './chain/client.js';
9
+ * const client = await createClient(rpcUrl, wallet);
10
+ */
11
+
12
+ import { Registry } from '@cosmjs/proto-signing';
13
+ import { SigningStargateClient, GasPrice, defaultRegistryTypes } from '@cosmjs/stargate';
14
+
15
+ // Protobuf encoders from v3protocol.js
16
+ import {
17
+ encodeMsgStartSession,
18
+ encodeMsgEndSession,
19
+ encodeMsgStartSubscription,
20
+ encodeMsgSubStartSession,
21
+ encodeMsgCancelSubscription,
22
+ encodeMsgRenewSubscription,
23
+ encodeMsgShareSubscription,
24
+ encodeMsgUpdateSubscription,
25
+ encodeMsgUpdateSession,
26
+ encodeMsgRegisterNode,
27
+ encodeMsgUpdateNodeDetails,
28
+ encodeMsgUpdateNodeStatus,
29
+ encodeMsgUpdatePlanDetails,
30
+ } from '../v3protocol.js';
31
+
32
+ // Plan/provider/lease encoders from plan-operations.js
33
+ import {
34
+ encodeMsgRegisterProvider,
35
+ encodeMsgUpdateProviderDetails,
36
+ encodeMsgUpdateProviderStatus,
37
+ encodeMsgCreatePlan,
38
+ encodeMsgUpdatePlanStatus,
39
+ encodeMsgLinkNode,
40
+ encodeMsgUnlinkNode,
41
+ encodeMsgPlanStartSession,
42
+ encodeMsgStartLease,
43
+ encodeMsgEndLease,
44
+ } from '../plan-operations.js';
45
+
46
+ import { GAS_PRICE } from '../defaults.js';
47
+
48
+ // ─── CosmJS Registry ─────────────────────────────────────────────────────────
49
+
50
+ /**
51
+ * Adapter that wraps a manual protobuf encoder for CosmJS's Registry.
52
+ * CosmJS expects { fromPartial, encode, decode } — we only need encode.
53
+ */
54
+ function makeMsgType(encodeFn) {
55
+ return {
56
+ fromPartial: (v) => v,
57
+ encode: (inst) => ({ finish: () => encodeFn(inst) }),
58
+ decode: () => ({}),
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Build a CosmJS Registry with ALL 14 Sentinel message types registered.
64
+ * This is required for signAndBroadcast to encode Sentinel-specific messages.
65
+ */
66
+ export function buildRegistry() {
67
+ return new Registry([
68
+ ...defaultRegistryTypes,
69
+ // Direct node session (v3protocol.js)
70
+ ['/sentinel.node.v3.MsgStartSessionRequest', makeMsgType(encodeMsgStartSession)],
71
+ // End session (v3protocol.js)
72
+ ['/sentinel.session.v3.MsgCancelSessionRequest', makeMsgType(encodeMsgEndSession)],
73
+ // Subscription (v3protocol.js)
74
+ ['/sentinel.subscription.v3.MsgStartSubscriptionRequest', makeMsgType(encodeMsgStartSubscription)],
75
+ ['/sentinel.subscription.v3.MsgStartSessionRequest', makeMsgType(encodeMsgSubStartSession)],
76
+ // Plan (plan-operations.js)
77
+ ['/sentinel.plan.v3.MsgStartSessionRequest', makeMsgType(encodeMsgPlanStartSession)],
78
+ ['/sentinel.plan.v3.MsgCreatePlanRequest', makeMsgType(encodeMsgCreatePlan)],
79
+ ['/sentinel.plan.v3.MsgLinkNodeRequest', makeMsgType(encodeMsgLinkNode)],
80
+ ['/sentinel.plan.v3.MsgUnlinkNodeRequest', makeMsgType(encodeMsgUnlinkNode)],
81
+ ['/sentinel.plan.v3.MsgUpdatePlanStatusRequest', makeMsgType(encodeMsgUpdatePlanStatus)],
82
+ // Provider (plan-operations.js)
83
+ ['/sentinel.provider.v3.MsgRegisterProviderRequest', makeMsgType(encodeMsgRegisterProvider)],
84
+ ['/sentinel.provider.v3.MsgUpdateProviderDetailsRequest', makeMsgType(encodeMsgUpdateProviderDetails)],
85
+ ['/sentinel.provider.v3.MsgUpdateProviderStatusRequest', makeMsgType(encodeMsgUpdateProviderStatus)],
86
+ // Plan details update (v3 — NEW, from sentinel-go-sdk)
87
+ ['/sentinel.plan.v3.MsgUpdatePlanDetailsRequest', makeMsgType(encodeMsgUpdatePlanDetails)],
88
+ // Lease (plan-operations.js)
89
+ ['/sentinel.lease.v1.MsgStartLeaseRequest', makeMsgType(encodeMsgStartLease)],
90
+ ['/sentinel.lease.v1.MsgEndLeaseRequest', makeMsgType(encodeMsgEndLease)],
91
+ // Subscription management (v3 — from sentinel-go-sdk)
92
+ ['/sentinel.subscription.v3.MsgCancelSubscriptionRequest', makeMsgType(encodeMsgCancelSubscription)],
93
+ ['/sentinel.subscription.v3.MsgRenewSubscriptionRequest', makeMsgType(encodeMsgRenewSubscription)],
94
+ ['/sentinel.subscription.v3.MsgShareSubscriptionRequest', makeMsgType(encodeMsgShareSubscription)],
95
+ ['/sentinel.subscription.v3.MsgUpdateSubscriptionRequest', makeMsgType(encodeMsgUpdateSubscription)],
96
+ // Session management (v3)
97
+ ['/sentinel.session.v3.MsgUpdateSessionRequest', makeMsgType(encodeMsgUpdateSession)],
98
+ // Node operator (v3 — for node operators, NOT consumer apps)
99
+ ['/sentinel.node.v3.MsgRegisterNodeRequest', makeMsgType(encodeMsgRegisterNode)],
100
+ ['/sentinel.node.v3.MsgUpdateNodeDetailsRequest', makeMsgType(encodeMsgUpdateNodeDetails)],
101
+ ['/sentinel.node.v3.MsgUpdateNodeStatusRequest', makeMsgType(encodeMsgUpdateNodeStatus)],
102
+ ]);
103
+ }
104
+
105
+ // ─── Signing Client ──────────────────────────────────────────────────────────
106
+
107
+ /**
108
+ * Create a SigningStargateClient connected to Sentinel RPC.
109
+ * Gas price: from defaults.js GAS_PRICE (chain minimum).
110
+ */
111
+ export async function createClient(rpcUrl, wallet) {
112
+ return SigningStargateClient.connectWithSigner(rpcUrl, wallet, {
113
+ gasPrice: GasPrice.fromString(GAS_PRICE),
114
+ registry: buildRegistry(),
115
+ });
116
+ }
117
+
118
+ // ─── All Type URL Constants ──────────────────────────────────────────────────
119
+
120
+ export const MSG_TYPES = {
121
+ // Direct node session
122
+ START_SESSION: '/sentinel.node.v3.MsgStartSessionRequest',
123
+ END_SESSION: '/sentinel.session.v3.MsgCancelSessionRequest',
124
+ // Subscription
125
+ START_SUBSCRIPTION: '/sentinel.subscription.v3.MsgStartSubscriptionRequest',
126
+ SUB_START_SESSION: '/sentinel.subscription.v3.MsgStartSessionRequest',
127
+ // Plan
128
+ PLAN_START_SESSION: '/sentinel.plan.v3.MsgStartSessionRequest',
129
+ CREATE_PLAN: '/sentinel.plan.v3.MsgCreatePlanRequest',
130
+ UPDATE_PLAN_STATUS: '/sentinel.plan.v3.MsgUpdatePlanStatusRequest',
131
+ LINK_NODE: '/sentinel.plan.v3.MsgLinkNodeRequest',
132
+ UNLINK_NODE: '/sentinel.plan.v3.MsgUnlinkNodeRequest',
133
+ // Provider
134
+ REGISTER_PROVIDER: '/sentinel.provider.v3.MsgRegisterProviderRequest',
135
+ UPDATE_PROVIDER: '/sentinel.provider.v3.MsgUpdateProviderDetailsRequest',
136
+ UPDATE_PROVIDER_STATUS: '/sentinel.provider.v3.MsgUpdateProviderStatusRequest',
137
+ // Plan details update (v3 — NEW)
138
+ UPDATE_PLAN_DETAILS: '/sentinel.plan.v3.MsgUpdatePlanDetailsRequest',
139
+ // Lease
140
+ START_LEASE: '/sentinel.lease.v1.MsgStartLeaseRequest',
141
+ END_LEASE: '/sentinel.lease.v1.MsgEndLeaseRequest',
142
+ // Subscription management (v3)
143
+ CANCEL_SUBSCRIPTION: '/sentinel.subscription.v3.MsgCancelSubscriptionRequest',
144
+ RENEW_SUBSCRIPTION: '/sentinel.subscription.v3.MsgRenewSubscriptionRequest',
145
+ SHARE_SUBSCRIPTION: '/sentinel.subscription.v3.MsgShareSubscriptionRequest',
146
+ UPDATE_SUBSCRIPTION: '/sentinel.subscription.v3.MsgUpdateSubscriptionRequest',
147
+ // Session management (v3)
148
+ UPDATE_SESSION: '/sentinel.session.v3.MsgUpdateSessionRequest',
149
+ // Node operator (v3)
150
+ REGISTER_NODE: '/sentinel.node.v3.MsgRegisterNodeRequest',
151
+ UPDATE_NODE_DETAILS: '/sentinel.node.v3.MsgUpdateNodeDetailsRequest',
152
+ UPDATE_NODE_STATUS: '/sentinel.node.v3.MsgUpdateNodeStatusRequest',
153
+ // Cosmos FeeGrant
154
+ GRANT_FEE_ALLOWANCE: '/cosmos.feegrant.v1beta1.MsgGrantAllowance',
155
+ REVOKE_FEE_ALLOWANCE: '/cosmos.feegrant.v1beta1.MsgRevokeAllowance',
156
+ // Cosmos Authz
157
+ AUTHZ_GRANT: '/cosmos.authz.v1beta1.MsgGrant',
158
+ AUTHZ_REVOKE: '/cosmos.authz.v1beta1.MsgRevoke',
159
+ AUTHZ_EXEC: '/cosmos.authz.v1beta1.MsgExec',
160
+ };
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Sentinel SDK — Chain / Fee Grants Module
3
+ *
4
+ * FeeGrant message builders, queries, monitoring, and workflow helpers.
5
+ * Gas-free UX: granter pays fees for grantee's transactions.
6
+ *
7
+ * Usage:
8
+ * import { buildFeeGrantMsg, queryFeeGrants, monitorFeeGrants } from './chain/fee-grants.js';
9
+ * const msg = buildFeeGrantMsg(serviceAddr, userAddr, { spendLimit: 5000000 });
10
+ */
11
+
12
+ import { EventEmitter } from 'events';
13
+ import { protoString, protoInt64, protoEmbedded } from '../v3protocol.js';
14
+ import { LCD_ENDPOINTS } from '../defaults.js';
15
+ import { ValidationError, ErrorCodes } from '../errors.js';
16
+ import { lcd, lcdPaginatedSafe, lcdQueryAll } from './lcd.js';
17
+ import { isSameKey } from './wallet.js';
18
+ import { queryPlanSubscribers } from './queries.js';
19
+
20
+ // ─── Protobuf Helpers for FeeGrant ──────────────────────────────────────────
21
+ // Uses the same manual protobuf encoding as Sentinel types — no codegen needed.
22
+
23
+ function encodeCoin(denom, amount) {
24
+ return Buffer.concat([protoString(1, denom), protoString(2, String(amount))]);
25
+ }
26
+
27
+ function encodeTimestamp(date) {
28
+ const ms = date.getTime();
29
+ if (Number.isNaN(ms)) throw new ValidationError(ErrorCodes.INVALID_OPTIONS, 'encodeTimestamp(): invalid date', { date });
30
+ const seconds = BigInt(Math.floor(ms / 1000));
31
+ return Buffer.concat([protoInt64(1, seconds)]);
32
+ }
33
+
34
+ function encodeBasicAllowance(spendLimit, expiration) {
35
+ const parts = [];
36
+ if (spendLimit != null && spendLimit !== false) {
37
+ const coins = Array.isArray(spendLimit) ? spendLimit : [{ denom: 'udvpn', amount: String(spendLimit) }];
38
+ for (const coin of coins) {
39
+ parts.push(protoEmbedded(1, encodeCoin(coin.denom || 'udvpn', coin.amount)));
40
+ }
41
+ }
42
+ if (expiration) {
43
+ parts.push(protoEmbedded(2, encodeTimestamp(expiration instanceof Date ? expiration : new Date(expiration))));
44
+ }
45
+ return Buffer.concat(parts);
46
+ }
47
+
48
+ function encodeAllowedMsgAllowance(innerTypeUrl, innerBytes, allowedMessages) {
49
+ const parts = [protoEmbedded(1, encodeAny(innerTypeUrl, innerBytes))];
50
+ for (const msg of allowedMessages) {
51
+ parts.push(protoString(2, msg));
52
+ }
53
+ return Buffer.concat(parts);
54
+ }
55
+
56
+ function encodeAny(typeUrl, valueBytes) {
57
+ return Buffer.concat([
58
+ protoString(1, typeUrl),
59
+ protoEmbedded(2, valueBytes),
60
+ ]);
61
+ }
62
+
63
+ // ─── FeeGrant (cosmos.feegrant.v1beta1) ─────────────────────────────────────
64
+
65
+ /**
66
+ * Build a MsgGrantAllowance message.
67
+ * @param {string} granter - Address paying fees (sent1...)
68
+ * @param {string} grantee - Address receiving fee grant (sent1...)
69
+ * @param {object} opts
70
+ * @param {number|Array} opts.spendLimit - Max spend in udvpn (number) or [{denom, amount}]
71
+ * @param {Date|string} opts.expiration - Optional expiry date
72
+ * @param {string[]} opts.allowedMessages - Optional: restrict to specific msg types (uses AllowedMsgAllowance)
73
+ */
74
+ export function buildFeeGrantMsg(granter, grantee, opts = {}) {
75
+ const { spendLimit, expiration, allowedMessages } = opts;
76
+ const basicBytes = encodeBasicAllowance(spendLimit, expiration);
77
+
78
+ let allowanceTypeUrl, allowanceBytes;
79
+ if (allowedMessages?.length) {
80
+ allowanceTypeUrl = '/cosmos.feegrant.v1beta1.AllowedMsgAllowance';
81
+ allowanceBytes = encodeAllowedMsgAllowance(
82
+ '/cosmos.feegrant.v1beta1.BasicAllowance', basicBytes, allowedMessages
83
+ );
84
+ } else {
85
+ allowanceTypeUrl = '/cosmos.feegrant.v1beta1.BasicAllowance';
86
+ allowanceBytes = basicBytes;
87
+ }
88
+
89
+ // MsgGrantAllowance: field 1=granter, field 2=grantee, field 3=allowance(Any)
90
+ return {
91
+ typeUrl: '/cosmos.feegrant.v1beta1.MsgGrantAllowance',
92
+ value: { granter, grantee, allowance: { typeUrl: allowanceTypeUrl, value: Uint8Array.from(allowanceBytes) } },
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Build a MsgRevokeAllowance message.
98
+ */
99
+ export function buildRevokeFeeGrantMsg(granter, grantee) {
100
+ return {
101
+ typeUrl: '/cosmos.feegrant.v1beta1.MsgRevokeAllowance',
102
+ value: { granter, grantee },
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Query fee grants given to a grantee.
108
+ * @returns {Promise<Array>} Array of allowance objects
109
+ */
110
+ export async function queryFeeGrants(lcdUrl, grantee) {
111
+ const { items } = await lcdPaginatedSafe(lcdUrl, `/cosmos/feegrant/v1beta1/allowances/${grantee}`, 'allowances');
112
+ return items;
113
+ }
114
+
115
+ /**
116
+ * Query fee grants issued BY an address (where addr is the granter).
117
+ * @param {string} lcdUrl
118
+ * @param {string} granter - Address that issued the grants
119
+ * @returns {Promise<Array>}
120
+ */
121
+ export async function queryFeeGrantsIssued(lcdUrl, granter) {
122
+ const { items } = await lcdPaginatedSafe(lcdUrl, `/cosmos/feegrant/v1beta1/issued/${granter}`, 'allowances');
123
+ return items;
124
+ }
125
+
126
+ /**
127
+ * Query a specific fee grant between granter and grantee.
128
+ * @returns {Promise<object|null>} Allowance object or null
129
+ */
130
+ export async function queryFeeGrant(lcdUrl, granter, grantee) {
131
+ try {
132
+ const data = await lcd(lcdUrl, `/cosmos/feegrant/v1beta1/allowance/${granter}/${grantee}`);
133
+ return data.allowance || null;
134
+ } catch { return null; } // 404 = no grant
135
+ }
136
+
137
+ // ─── Fee Grant Workflow Helpers (v25b) ────────────────────────────────────────
138
+
139
+ /**
140
+ * Grant fee allowance to all plan subscribers who don't already have one.
141
+ * Filters out self-grants (granter === grantee) and already-granted addresses.
142
+ *
143
+ * @param {number|string} planId
144
+ * @param {object} opts
145
+ * @param {string} opts.granterAddress - Who pays fees (typically plan owner)
146
+ * @param {string} opts.lcdUrl - LCD endpoint
147
+ * @param {object} [opts.grantOpts] - Options for buildFeeGrantMsg (spendLimit, expiration, allowedMessages)
148
+ * @returns {Promise<{ msgs: Array, skipped: string[], newGrants: string[] }>} Messages ready for broadcast
149
+ */
150
+ export async function grantPlanSubscribers(planId, opts = {}) {
151
+ const { granterAddress, lcdUrl, grantOpts = {} } = opts;
152
+ if (!granterAddress) throw new ValidationError(ErrorCodes.INVALID_OPTIONS, 'granterAddress is required');
153
+
154
+ // Get subscribers
155
+ const { subscribers } = await queryPlanSubscribers(planId, { lcdUrl });
156
+
157
+ // Get existing grants ISSUED BY granter (not grants received)
158
+ const existingGrants = await queryFeeGrantsIssued(lcdUrl || LCD_ENDPOINTS[0].url, granterAddress);
159
+ const alreadyGranted = new Set(existingGrants.map(g => g.grantee));
160
+
161
+ const msgs = [];
162
+ const skipped = [];
163
+ const newGrants = [];
164
+
165
+ const now = new Date();
166
+ // Deduplicate by address and filter active+non-expired
167
+ const seen = new Set();
168
+ for (const sub of subscribers) {
169
+ const addr = sub.acc_address || sub.address;
170
+ if (!addr || seen.has(addr)) continue;
171
+ seen.add(addr);
172
+ // Skip self-grant (chain rejects granter === grantee)
173
+ if (addr === granterAddress || isSameKey(addr, granterAddress)) { skipped.push(addr); continue; }
174
+ // Skip inactive or expired
175
+ if (sub.status && sub.status !== 'active') { skipped.push(addr); continue; }
176
+ if (sub.inactive_at && new Date(sub.inactive_at) <= now) { skipped.push(addr); continue; }
177
+ // Skip already granted
178
+ if (alreadyGranted.has(addr)) { skipped.push(addr); continue; }
179
+ msgs.push(buildFeeGrantMsg(granterAddress, addr, grantOpts));
180
+ newGrants.push(addr);
181
+ }
182
+
183
+ return { msgs, skipped, newGrants };
184
+ }
185
+
186
+ /**
187
+ * Find fee grants expiring within N days.
188
+ *
189
+ * @param {string} lcdUrl - LCD endpoint
190
+ * @param {string} granteeOrGranter - Address to check grants for
191
+ * @param {number} withinDays - Check grants expiring within this many days (default: 7)
192
+ * @param {'grantee'|'granter'} [role='grantee'] - Whether to check as grantee or granter
193
+ * @returns {Promise<Array<{ granter: string, grantee: string, expiresAt: Date|null, daysLeft: number|null }>>}
194
+ */
195
+ export async function getExpiringGrants(lcdUrl, granteeOrGranter, withinDays = 7, role = 'grantee') {
196
+ const grants = role === 'grantee'
197
+ ? await queryFeeGrants(lcdUrl, granteeOrGranter)
198
+ : await queryFeeGrantsIssued(lcdUrl, granteeOrGranter);
199
+
200
+ const now = Date.now();
201
+ const cutoff = now + withinDays * 24 * 60 * 60_000;
202
+ const expiring = [];
203
+
204
+ for (const g of grants) {
205
+ // Fee grant allowances have complex nested @type structures:
206
+ // BasicAllowance: { expiration }
207
+ // PeriodicAllowance: { basic: { expiration } }
208
+ // AllowedMsgAllowance: { allowance: { expiration } or allowance: { basic: { expiration } } }
209
+ const a = g.allowance || {};
210
+ const inner = a.allowance || a; // unwrap AllowedMsgAllowance
211
+ const expStr = inner.expiration || inner.basic?.expiration || a.expiration || a.basic?.expiration;
212
+ if (!expStr) continue; // no expiry set
213
+ const expiresAt = new Date(expStr);
214
+ if (expiresAt.getTime() <= cutoff) {
215
+ expiring.push({
216
+ granter: g.granter,
217
+ grantee: g.grantee,
218
+ expiresAt,
219
+ daysLeft: Math.max(0, Math.round((expiresAt.getTime() - now) / (24 * 60 * 60_000))),
220
+ });
221
+ }
222
+ }
223
+ return expiring;
224
+ }
225
+
226
+ /**
227
+ * Revoke and re-grant expiring fee grants.
228
+ *
229
+ * @param {string} lcdUrl
230
+ * @param {string} granterAddress
231
+ * @param {number} withinDays - Renew grants expiring within N days
232
+ * @param {object} [grantOpts] - Options for new grants (spendLimit, expiration, allowedMessages)
233
+ * @returns {Promise<{ msgs: Array, renewed: string[] }>} Messages ready for broadcast
234
+ */
235
+ export async function renewExpiringGrants(lcdUrl, granterAddress, withinDays = 7, grantOpts = {}) {
236
+ const expiring = await getExpiringGrants(lcdUrl, granterAddress, withinDays, 'granter');
237
+ const msgs = [];
238
+ const renewed = [];
239
+
240
+ for (const g of expiring) {
241
+ if (g.grantee === granterAddress) continue; // skip self
242
+ msgs.push(buildRevokeFeeGrantMsg(granterAddress, g.grantee));
243
+ msgs.push(buildFeeGrantMsg(granterAddress, g.grantee, grantOpts));
244
+ renewed.push(g.grantee);
245
+ }
246
+
247
+ return { msgs, renewed };
248
+ }
249
+
250
+ // ─── Fee Grant Monitoring (v25b) ─────────────────────────────────────────────
251
+
252
+ /**
253
+ * Monitor fee grants for expiry. Returns an EventEmitter that checks grants on interval.
254
+ *
255
+ * @param {object} opts
256
+ * @param {string} opts.lcdUrl - LCD endpoint
257
+ * @param {string} opts.address - Address to monitor (as granter)
258
+ * @param {number} [opts.checkIntervalMs] - Check interval (default: 6 hours)
259
+ * @param {number} [opts.warnDays] - Emit 'expiring' when grant expires within N days (default: 7)
260
+ * @param {boolean} [opts.autoRenew] - Auto-revoke+re-grant expiring grants (default: false)
261
+ * @param {object} [opts.grantOpts] - Options for renewed grants
262
+ * @returns {EventEmitter} Emits 'expiring' and 'expired' events. Call .stop() to stop monitoring.
263
+ */
264
+ export function monitorFeeGrants(opts = {}) {
265
+ const { lcdUrl, address, checkIntervalMs = 6 * 60 * 60_000, warnDays = 7, autoRenew = false, grantOpts = {} } = opts;
266
+ if (!lcdUrl || !address) throw new ValidationError(ErrorCodes.INVALID_OPTIONS, 'monitorFeeGrants requires lcdUrl and address');
267
+
268
+ const emitter = new EventEmitter();
269
+ let timer = null;
270
+
271
+ const check = async () => {
272
+ try {
273
+ const expiring = await getExpiringGrants(lcdUrl, address, warnDays, 'granter');
274
+ const now = Date.now();
275
+
276
+ for (const g of expiring) {
277
+ if (g.expiresAt.getTime() <= now) {
278
+ emitter.emit('expired', g);
279
+ } else {
280
+ emitter.emit('expiring', g);
281
+ }
282
+ }
283
+
284
+ if (autoRenew && expiring.length > 0) {
285
+ const { msgs, renewed } = await renewExpiringGrants(lcdUrl, address, warnDays, grantOpts);
286
+ if (msgs.length > 0) {
287
+ emitter.emit('renew', { msgs, renewed });
288
+ }
289
+ }
290
+ } catch (err) {
291
+ emitter.emit('error', err);
292
+ }
293
+ };
294
+
295
+ // Start checking
296
+ check();
297
+ timer = setInterval(check, checkIntervalMs);
298
+ if (timer.unref) timer.unref(); // Don't prevent process exit
299
+
300
+ emitter.stop = () => {
301
+ if (timer) { clearInterval(timer); timer = null; }
302
+ };
303
+
304
+ return emitter;
305
+ }