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.
@@ -0,0 +1,1310 @@
1
+ # Sentinel On-Chain Function Catalog
2
+
3
+ Complete reference for all on-chain message types and query functions supported by the Sentinel SDK, with exact signatures for both JavaScript and C# implementations.
4
+
5
+ **Chain version:** v3 (v2 paths return "Not Implemented" — except provider, which remains v2)
6
+ **Token:** Display `P2P`, chain denom `udvpn` (1 P2P = 1,000,000 udvpn)
7
+ **Gas price:** 0.2 udvpn per gas unit
8
+
9
+ ---
10
+
11
+ ## Overview
12
+
13
+ | Category | Count | Modules |
14
+ |----------|-------|---------|
15
+ | Sentinel node messages | 4 | `sentinel.node.v3` |
16
+ | Sentinel session messages | 2 | `sentinel.session.v3` |
17
+ | Sentinel subscription messages | 6 | `sentinel.subscription.v3` |
18
+ | Sentinel plan messages | 6 | `sentinel.plan.v3` |
19
+ | Sentinel provider messages | 3 | `sentinel.provider.v3` |
20
+ | Sentinel lease messages | 2 | `sentinel.lease.v1` |
21
+ | Cosmos feegrant messages | 2 | `cosmos.feegrant.v1beta1` |
22
+ | Cosmos authz messages | 3 | `cosmos.authz.v1beta1` |
23
+ | Cosmos bank messages | 1 | `cosmos.bank.v1beta1` |
24
+ | **Total registered** | **29** | |
25
+
26
+ ---
27
+
28
+ ## Address Formats
29
+
30
+ | Prefix | Type | Used By |
31
+ |--------|------|---------|
32
+ | `sent1...` | Account address | Consumers, operators (signing `from`) |
33
+ | `sentnode1...` | Node address | Node addresses in messages |
34
+ | `sentprov1...` | Provider address | Plan/provider `from` fields |
35
+
36
+ ---
37
+
38
+ ## Module: Node (`sentinel.node.v3`)
39
+
40
+ ### MsgStartSessionRequest
41
+
42
+ Start a direct pay-per-GB or pay-per-hour session on a node. Charges the wallet for bandwidth before any data flows.
43
+
44
+ - **Type URL:** `/sentinel.node.v3.MsgStartSessionRequest`
45
+ - **Audience:** Consumer apps (end users)
46
+ - **Gas estimate:** ~200,000 gas (~40,000 udvpn fee)
47
+ - **Cost:** Node's gigabyte or hourly price, paid from wallet balance
48
+
49
+ **Protobuf fields:**
50
+
51
+ | Field | Number | Wire Type | Description |
52
+ |-------|--------|-----------|-------------|
53
+ | `from` | 1 | string | Account address (`sent1...`) |
54
+ | `node_address` | 2 | string | Node address (`sentnode1...`) |
55
+ | `gigabytes` | 3 | int64 | GB to purchase (set to 0 for hourly) |
56
+ | `hours` | 4 | int64 | Hours to purchase (set to 0 for GB) |
57
+ | `max_price` | 5 | embedded Price | Maximum acceptable price |
58
+
59
+ **JS — build layer (`protocol/messages.js`):**
60
+ ```js
61
+ import { buildMsgStartSession } from './protocol/messages.js';
62
+
63
+ const msg = buildMsgStartSession({
64
+ from, // string — sent1... account address
65
+ nodeAddress, // string — sentnode1... node address
66
+ gigabytes, // number — default: 1. Set to 0 for hourly.
67
+ hours, // number — default: 0. Set to 1+ for hourly.
68
+ maxPrice, // { denom, base_value, quote_value } or undefined
69
+ });
70
+ // Returns: { typeUrl: '/sentinel.node.v3.MsgStartSessionRequest', value: object }
71
+ ```
72
+
73
+ **JS — encode layer (`v3protocol.js` / `protocol/encoding.js`):**
74
+ ```js
75
+ encodeMsgStartSession({ from, node_address, nodeAddress, gigabytes, hours, max_price, maxPrice })
76
+ // Returns: Uint8Array (raw protobuf bytes)
77
+ ```
78
+
79
+ **C# (`MessageBuilder.Session.cs`):**
80
+ ```csharp
81
+ SentinelMessage msg = MessageBuilder.StartSession(
82
+ from, // string — sent1... account address
83
+ nodeAddress, // string — sentnode1... node address
84
+ gigabytes, // long — default: 1. Set to 0 for hourly.
85
+ maxPrice, // PriceEntry? — optional max price
86
+ hours // long — default: 0. Set to 1+ for hourly.
87
+ );
88
+ // Validates: gigabytes 0-100, hours >= 0, not both zero
89
+ ```
90
+
91
+ **Notes:**
92
+ - Exactly one of `gigabytes` or `hours` must be > 0; setting both > 0 is undefined behavior
93
+ - `maxPrice` must come from the node's LCD `gigabyte_prices` (GB sessions) or `hourly_prices` (hourly sessions) — the chain validates this exactly
94
+ - The chain enforces `max_price` strictly: if the node raises its price after you query, the TX fails with "invalid price"
95
+ - Session ID is extracted from TX events after broadcast — use `extractId(result, /session/i, ['session_id', 'id'])`
96
+ - After TX success, the handshake (POST to node URL) must complete within ~60 seconds before the chain auto-cancels
97
+
98
+ ---
99
+
100
+ ### MsgRegisterNodeRequest
101
+
102
+ Register a new node operator. One-time operation per node.
103
+
104
+ - **Type URL:** `/sentinel.node.v3.MsgRegisterNodeRequest`
105
+ - **Audience:** Node operators only (NOT consumer apps)
106
+ - **Gas estimate:** ~200,000 gas
107
+
108
+ **Protobuf fields:**
109
+
110
+ | Field | Number | Wire Type | Description |
111
+ |-------|--------|-----------|-------------|
112
+ | `from` | 1 | string | Account address (`sent1...`) |
113
+ | `gigabyte_prices` | 2 | embedded Price (repeated) | Per-GB prices |
114
+ | `hourly_prices` | 3 | embedded Price (repeated) | Per-hour prices |
115
+ | `remote_addrs` | 4 | string (repeated) | Node endpoints (e.g. `"1.2.3.4:8585"`) |
116
+
117
+ **JS:**
118
+ ```js
119
+ import { buildMsgRegisterNode } from './protocol/messages.js';
120
+ const msg = buildMsgRegisterNode({ from, gigabytePrices, hourlyPrices, remoteAddrs });
121
+
122
+ // encodeMsg variant:
123
+ encodeMsgRegisterNode({ from, gigabytePrices, gigabyte_prices, hourlyPrices, hourly_prices, remoteAddrs, remote_addrs })
124
+ ```
125
+
126
+ **C#:**
127
+ ```csharp
128
+ SentinelMessage msg = MessageBuilder.RegisterNode(from, gigabytePrices, hourlyPrices, remoteAddrs);
129
+ ```
130
+
131
+ ---
132
+
133
+ ### MsgUpdateNodeDetailsRequest
134
+
135
+ Update an existing node's pricing and/or endpoints.
136
+
137
+ - **Type URL:** `/sentinel.node.v3.MsgUpdateNodeDetailsRequest`
138
+ - **Audience:** Node operators only
139
+
140
+ **Protobuf fields:** Same as `MsgRegisterNodeRequest`.
141
+
142
+ **JS:**
143
+ ```js
144
+ const msg = buildMsgUpdateNodeDetails({ from, gigabytePrices, hourlyPrices, remoteAddrs });
145
+ encodeMsgUpdateNodeDetails({ from, gigabytePrices, gigabyte_prices, hourlyPrices, hourly_prices, remoteAddrs, remote_addrs })
146
+ ```
147
+
148
+ **C#:**
149
+ ```csharp
150
+ SentinelMessage msg = MessageBuilder.UpdateNodeDetails(from, gigabytePrices, hourlyPrices, remoteAddrs);
151
+ ```
152
+
153
+ ---
154
+
155
+ ### MsgUpdateNodeStatusRequest
156
+
157
+ Activate or deactivate a node.
158
+
159
+ - **Type URL:** `/sentinel.node.v3.MsgUpdateNodeStatusRequest`
160
+ - **Audience:** Node operators only
161
+
162
+ **Protobuf fields:**
163
+
164
+ | Field | Number | Wire Type | Description |
165
+ |-------|--------|-----------|-------------|
166
+ | `from` | 1 | string | Account address |
167
+ | `status` | 2 | int64/enum | 1=active, 2=inactive\_pending, 3=inactive |
168
+
169
+ **JS:**
170
+ ```js
171
+ const msg = buildMsgUpdateNodeStatus({ from, status });
172
+ encodeMsgUpdateNodeStatus({ from, status })
173
+ ```
174
+
175
+ **C#:**
176
+ ```csharp
177
+ SentinelMessage msg = MessageBuilder.UpdateNodeStatus(from, status);
178
+ ```
179
+
180
+ ---
181
+
182
+ ## Module: Session (`sentinel.session.v3`)
183
+
184
+ ### MsgCancelSessionRequest
185
+
186
+ Cancel (end) an active session. Formerly named `MsgEndSession` in v2.
187
+
188
+ - **Type URL:** `/sentinel.session.v3.MsgCancelSessionRequest`
189
+ - **Audience:** Consumer apps (end users)
190
+ - **Gas estimate:** ~150,000 gas
191
+
192
+ **Protobuf fields:**
193
+
194
+ | Field | Number | Wire Type | Description |
195
+ |-------|--------|-----------|-------------|
196
+ | `from` | 1 | string | Account address (`sent1...`) |
197
+ | `id` | 2 | uint64 | Session ID |
198
+
199
+ **JS:**
200
+ ```js
201
+ import { buildMsgCancelSession, buildMsgEndSession } from './protocol/messages.js';
202
+ // Both names work — buildMsgEndSession is an alias:
203
+ const msg = buildMsgCancelSession({ from, id }); // id: number | bigint
204
+
205
+ // encode variant:
206
+ encodeMsgEndSession({ from, id }) // from v3protocol.js / encoding.js
207
+ ```
208
+
209
+ **C#:**
210
+ ```csharp
211
+ SentinelMessage msg = MessageBuilder.EndSession(from, sessionId); // sessionId: ulong
212
+ // Validates: sessionId > 0
213
+ ```
214
+
215
+ **Notes:**
216
+ - v3 removed the `rating` field that existed in v2 — do not send it
217
+ - `buildMsgEndSession` is a JS alias for `buildMsgCancelSession` (backward compatibility)
218
+ - In `broadcast.js`, `buildEndSessionMsg(from, sessionId)` uses `BigInt(sessionId)` for the `id` field while `buildMsgCancelSession` uses `Number(id)` — this is a known inconsistency. Use `buildMsgCancelSession` from `messages.js` for consumer apps.
219
+
220
+ ---
221
+
222
+ ### MsgUpdateSessionRequest
223
+
224
+ Report bandwidth usage for a session. Called by node operators, not consumers.
225
+
226
+ - **Type URL:** `/sentinel.session.v3.MsgUpdateSessionRequest`
227
+ - **Audience:** Node operators only
228
+
229
+ **Protobuf fields:**
230
+
231
+ | Field | Number | Wire Type | Description |
232
+ |-------|--------|-----------|-------------|
233
+ | `from` | 1 | string | Account address |
234
+ | `id` | 2 | uint64 | Session ID |
235
+ | `download_bytes` | 3 | int64 | Bytes downloaded |
236
+ | `upload_bytes` | 4 | int64 | Bytes uploaded |
237
+
238
+ **JS:**
239
+ ```js
240
+ const msg = buildMsgUpdateSession({ from, id, downloadBytes, uploadBytes });
241
+ encodeMsgUpdateSession({ from, id, downloadBytes, download_bytes, uploadBytes, upload_bytes })
242
+ ```
243
+
244
+ **C#:**
245
+ ```csharp
246
+ SentinelMessage msg = MessageBuilder.UpdateSession(from, sessionId, downloadBytes, uploadBytes);
247
+ ```
248
+
249
+ ---
250
+
251
+ ## Module: Subscription (`sentinel.subscription.v3`)
252
+
253
+ ### MsgStartSubscriptionRequest
254
+
255
+ Subscribe to a plan (without starting a session). Use `MsgStartSessionRequest` via plan to subscribe + start session in one TX.
256
+
257
+ - **Type URL:** `/sentinel.subscription.v3.MsgStartSubscriptionRequest`
258
+ - **Audience:** Consumer apps (plan-based flow)
259
+ - **Gas estimate:** ~250,000 gas
260
+
261
+ **Protobuf fields:**
262
+
263
+ | Field | Number | Wire Type | Description |
264
+ |-------|--------|-----------|-------------|
265
+ | `from` | 1 | string | Account address (`sent1...`) |
266
+ | `id` | 2 | uint64 | Plan ID |
267
+ | `denom` | 3 | string | Payment denom (default: `"udvpn"`) |
268
+ | `renewal_price_policy` | 4 | int64 | 0=unspecified (omit when 0) |
269
+
270
+ **JS:**
271
+ ```js
272
+ const msg = buildMsgStartSubscription({ from, id, denom, renewalPricePolicy });
273
+ // id is converted to Number(id) internally
274
+ encodeMsgStartSubscription({ from, id, denom, renewalPricePolicy, renewal_price_policy })
275
+ ```
276
+
277
+ **C#:**
278
+ ```csharp
279
+ SentinelMessage msg = MessageBuilder.StartSubscription(from, planId, denom, renewalPricePolicy);
280
+ // planId: ulong. renewalPricePolicy: int, default 0
281
+ // Validates: planId > 0
282
+ ```
283
+
284
+ ---
285
+
286
+ ### MsgStartSessionRequest (subscription variant)
287
+
288
+ Start a session on an existing subscription (plan already subscribed).
289
+
290
+ - **Type URL:** `/sentinel.subscription.v3.MsgStartSessionRequest`
291
+ - **Audience:** Consumer apps (plan-based flow)
292
+
293
+ **Protobuf fields:**
294
+
295
+ | Field | Number | Wire Type | Description |
296
+ |-------|--------|-----------|-------------|
297
+ | `from` | 1 | string | Account address (`sent1...`) |
298
+ | `id` | 2 | uint64 | Subscription ID |
299
+ | `node_address` | 3 | string | Node address (`sentnode1...`) |
300
+
301
+ **JS:**
302
+ ```js
303
+ const msg = buildMsgSubStartSession({ from, id, nodeAddress });
304
+ // id is converted to Number(id) internally
305
+ encodeMsgSubStartSession({ from, id, nodeAddress, node_address })
306
+ ```
307
+
308
+ **C#:**
309
+ ```csharp
310
+ SentinelMessage msg = MessageBuilder.SubStartSession(from, subscriptionId, nodeAddress);
311
+ // subscriptionId: ulong
312
+ // Validates: subscriptionId > 0, nodeAddress not empty
313
+ ```
314
+
315
+ ---
316
+
317
+ ### MsgCancelSubscriptionRequest
318
+
319
+ Cancel an active subscription.
320
+
321
+ - **Type URL:** `/sentinel.subscription.v3.MsgCancelSubscriptionRequest`
322
+ - **Audience:** Consumer apps
323
+
324
+ **Protobuf fields:**
325
+
326
+ | Field | Number | Wire Type | Description |
327
+ |-------|--------|-----------|-------------|
328
+ | `from` | 1 | string | Account address |
329
+ | `id` | 2 | uint64 | Subscription ID |
330
+
331
+ **JS:**
332
+ ```js
333
+ const msg = buildMsgCancelSubscription({ from, id });
334
+ encodeMsgCancelSubscription({ from, id })
335
+ ```
336
+
337
+ **C#:**
338
+ ```csharp
339
+ SentinelMessage msg = MessageBuilder.CancelSubscription(from, subscriptionId);
340
+ ```
341
+
342
+ ---
343
+
344
+ ### MsgRenewSubscriptionRequest
345
+
346
+ Renew an expiring subscription.
347
+
348
+ - **Type URL:** `/sentinel.subscription.v3.MsgRenewSubscriptionRequest`
349
+
350
+ **Protobuf fields:**
351
+
352
+ | Field | Number | Wire Type | Description |
353
+ |-------|--------|-----------|-------------|
354
+ | `from` | 1 | string | Account address |
355
+ | `id` | 2 | uint64 | Subscription ID |
356
+ | `denom` | 3 | string | Payment denom (default: `"udvpn"`) |
357
+
358
+ **JS:**
359
+ ```js
360
+ const msg = buildMsgRenewSubscription({ from, id, denom });
361
+ encodeMsgRenewSubscription({ from, id, denom })
362
+ ```
363
+
364
+ **C#:**
365
+ ```csharp
366
+ SentinelMessage msg = MessageBuilder.RenewSubscription(from, subscriptionId, denom);
367
+ ```
368
+
369
+ ---
370
+
371
+ ### MsgShareSubscriptionRequest
372
+
373
+ Share a subscription's bandwidth with another address.
374
+
375
+ - **Type URL:** `/sentinel.subscription.v3.MsgShareSubscriptionRequest`
376
+ - **Audience:** Plan operators (sharing with users)
377
+
378
+ **Protobuf fields:**
379
+
380
+ | Field | Number | Wire Type | Description |
381
+ |-------|--------|-----------|-------------|
382
+ | `from` | 1 | string | Subscription owner address |
383
+ | `id` | 2 | uint64 | Subscription ID |
384
+ | `acc_address` | 3 | string | Recipient address |
385
+ | `bytes` | 4 | **string** | Bandwidth quota in bytes (cosmossdk.io/math.Int) |
386
+
387
+ **JS:**
388
+ ```js
389
+ const msg = buildMsgShareSubscription({ from, id, accAddress, bytes });
390
+ // bytes converted to String(bytes) internally
391
+ encodeMsgShareSubscription({ from, id, accAddress, acc_address, bytes })
392
+ ```
393
+
394
+ **C#:**
395
+ ```csharp
396
+ SentinelMessage msg = MessageBuilder.ShareSubscription(from, subscriptionId, accAddress, bytes);
397
+ // bytes: long, written as string (WriteStringField) — NOT varint
398
+ ```
399
+
400
+ **Critical note:** The `bytes` field is `cosmossdk.io/math.Int` — its protobuf wire type is **string (wire type 2)**, not varint (wire type 0). Using varint encoding causes a silent TX failure. Both JS and C# SDKs correctly use `String(bytes)` / `bytes.ToString()`.
401
+
402
+ **Sharing semantics:** The chain only supports bytes-based bandwidth sharing. There is no time/duration field. For monthly plans, the operator must track expiry externally and remove users when time expires.
403
+
404
+ ---
405
+
406
+ ### MsgUpdateSubscriptionRequest
407
+
408
+ Update a subscription's renewal price policy.
409
+
410
+ - **Type URL:** `/sentinel.subscription.v3.MsgUpdateSubscriptionRequest`
411
+
412
+ **Protobuf fields:**
413
+
414
+ | Field | Number | Wire Type | Description |
415
+ |-------|--------|-----------|-------------|
416
+ | `from` | 1 | string | Account address |
417
+ | `id` | 2 | uint64 | Subscription ID |
418
+ | `renewal_price_policy` | 3 | int64/enum | Renewal policy value |
419
+
420
+ **JS:**
421
+ ```js
422
+ const msg = buildMsgUpdateSubscription({ from, id, renewalPricePolicy });
423
+ encodeMsgUpdateSubscription({ from, id, renewalPricePolicy, renewal_price_policy })
424
+ ```
425
+
426
+ **C#:**
427
+ ```csharp
428
+ SentinelMessage msg = MessageBuilder.UpdateSubscription(from, subscriptionId, renewalPricePolicy);
429
+ ```
430
+
431
+ ---
432
+
433
+ ## Module: Plan (`sentinel.plan.v3`)
434
+
435
+ ### MsgStartSessionRequest (plan variant)
436
+
437
+ Subscribe to a plan AND start a session in one TX. This is the recommended single-step flow for plan-based consumer apps.
438
+
439
+ - **Type URL:** `/sentinel.plan.v3.MsgStartSessionRequest`
440
+ - **Audience:** Consumer apps (plan-based flow — recommended)
441
+
442
+ **Protobuf fields:**
443
+
444
+ | Field | Number | Wire Type | Description |
445
+ |-------|--------|-----------|-------------|
446
+ | `from` | 1 | string | Account address (`sent1...`) |
447
+ | `id` | 2 | uint64 | Plan ID |
448
+ | `denom` | 3 | string | Payment denom (default: `"udvpn"`) |
449
+ | `renewal_price_policy` | 4 | int64 | Omit when 0 |
450
+ | `node_address` | 5 | string | Node to start session on |
451
+
452
+ **JS:**
453
+ ```js
454
+ const msg = buildMsgPlanStartSession({ from, id, denom, renewalPricePolicy, nodeAddress });
455
+ encodeMsgPlanStartSession({ from, id, denom, renewalPricePolicy, renewal_price_policy, nodeAddress, node_address })
456
+ ```
457
+
458
+ **C#:**
459
+ ```csharp
460
+ SentinelMessage msg = MessageBuilder.PlanStartSession(from, planId, denom, nodeAddress);
461
+ // planId: ulong. nodeAddress is optional (string?)
462
+ // Validates: planId > 0
463
+ ```
464
+
465
+ ---
466
+
467
+ ### MsgCreatePlanRequest
468
+
469
+ Create a new subscription plan. Plans start **INACTIVE** by default — a separate `MsgUpdatePlanStatusRequest` is required to activate.
470
+
471
+ - **Type URL:** `/sentinel.plan.v3.MsgCreatePlanRequest`
472
+ - **Audience:** Plan operators only
473
+
474
+ **Protobuf fields:**
475
+
476
+ | Field | Number | Wire Type | Description |
477
+ |-------|--------|-----------|-------------|
478
+ | `from` | 1 | string | Provider address (`sentprov1...`) |
479
+ | `bytes` | 2 | string | Total bandwidth (e.g. `"10000000000"` = 10 GB) |
480
+ | `duration` | 3 | embedded Duration | Plan validity period |
481
+ | `prices` | 4 | embedded Price (repeated) | Subscription cost |
482
+ | `is_private` | 5 | bool/varint | Optional, omit when false |
483
+
484
+ **JS:**
485
+ ```js
486
+ const msg = buildMsgCreatePlan({ from, bytes, duration, prices, isPrivate });
487
+ // duration: { seconds: N } or number (seconds)
488
+ // bytes: converted to String(bytes)
489
+ encodeMsgCreatePlan({ from, bytes, duration, prices, isPrivate })
490
+ ```
491
+
492
+ **C#:**
493
+ ```csharp
494
+ SentinelMessage msg = MessageBuilder.CreatePlan(from, bytes, durationSeconds, prices, isPrivate);
495
+ // from: sentprov1... provider address
496
+ // bytes: string
497
+ // durationSeconds: long (must be > 0)
498
+ // prices: PriceEntry[]
499
+ // Validates: durationSeconds > 0
500
+ ```
501
+
502
+ **Notes:**
503
+ - Plans are **INACTIVE** after creation. You must broadcast `MsgUpdatePlanStatusRequest` with `status=1` to activate.
504
+ - The `from` field must be the `sentprov1...` provider address, not the `sent1...` account address.
505
+
506
+ ---
507
+
508
+ ### MsgUpdatePlanDetailsRequest
509
+
510
+ Update plan bandwidth, duration, or prices without recreating.
511
+
512
+ - **Type URL:** `/sentinel.plan.v3.MsgUpdatePlanDetailsRequest`
513
+
514
+ **Protobuf fields:**
515
+
516
+ | Field | Number | Wire Type | Description |
517
+ |-------|--------|-----------|-------------|
518
+ | `from` | 1 | string | Provider address (`sentprov1...`) |
519
+ | `id` | 2 | uint64 | Plan ID |
520
+ | `bytes` | 3 | string | New bandwidth (optional) |
521
+ | `duration` | 4 | embedded Duration | New validity period (optional) |
522
+ | `prices` | 5 | embedded Price (repeated) | New prices (optional) |
523
+
524
+ **JS:**
525
+ ```js
526
+ const msg = buildMsgUpdatePlanDetails({ from, id, bytes, duration, prices });
527
+ encodeMsgUpdatePlanDetails({ from, id, bytes, duration, prices })
528
+ ```
529
+
530
+ **C#:**
531
+ ```csharp
532
+ SentinelMessage msg = MessageBuilder.UpdatePlanDetails(from, planId, bytes, durationSeconds, prices);
533
+ // All params after planId are nullable/optional
534
+ ```
535
+
536
+ ---
537
+
538
+ ### MsgUpdatePlanStatusRequest
539
+
540
+ Activate or deactivate a plan.
541
+
542
+ - **Type URL:** `/sentinel.plan.v3.MsgUpdatePlanStatusRequest`
543
+
544
+ **Protobuf fields:**
545
+
546
+ | Field | Number | Wire Type | Description |
547
+ |-------|--------|-----------|-------------|
548
+ | `from` | 1 | string | Provider address (`sentprov1...`) |
549
+ | `id` | 2 | uint64 | Plan ID |
550
+ | `status` | 3 | int64/enum | 1=active, 2=inactive\_pending, 3=inactive |
551
+
552
+ **JS:**
553
+ ```js
554
+ const msg = buildMsgUpdatePlanStatus({ from, id, status });
555
+ encodeMsgUpdatePlanStatus({ from, id, status })
556
+ ```
557
+
558
+ **C#:**
559
+ ```csharp
560
+ SentinelMessage msg = MessageBuilder.UpdatePlanStatus(from, planId, status);
561
+ // Validates: planId > 0, status 1-3
562
+ ```
563
+
564
+ ---
565
+
566
+ ### MsgLinkNodeRequest
567
+
568
+ Link a leased node to a plan. The node must have an active lease from the provider before linking.
569
+
570
+ - **Type URL:** `/sentinel.plan.v3.MsgLinkNodeRequest`
571
+
572
+ **Protobuf fields:**
573
+
574
+ | Field | Number | Wire Type | Description |
575
+ |-------|--------|-----------|-------------|
576
+ | `from` | 1 | string | Provider address (`sentprov1...`) |
577
+ | `id` | 2 | uint64 | Plan ID |
578
+ | `node_address` | 3 | string | Node address (`sentnode1...`) |
579
+
580
+ **JS:**
581
+ ```js
582
+ const msg = buildMsgLinkNode({ from, id, nodeAddress });
583
+ encodeMsgLinkNode({ from, id, nodeAddress, node_address })
584
+ ```
585
+
586
+ **C#:**
587
+ ```csharp
588
+ SentinelMessage msg = MessageBuilder.LinkNode(from, planId, nodeAddress);
589
+ // Validates: planId > 0, nodeAddress not empty
590
+ ```
591
+
592
+ **Notes:**
593
+ - Fails with "lease not found" if no active lease exists for this node
594
+ - Fails with "duplicate node for plan" if node is already linked
595
+ - Sequence: `StartLease` → wait for TX → `LinkNode`
596
+
597
+ ---
598
+
599
+ ### MsgUnlinkNodeRequest
600
+
601
+ Remove a node from a plan.
602
+
603
+ - **Type URL:** `/sentinel.plan.v3.MsgUnlinkNodeRequest`
604
+
605
+ **Protobuf fields:** Same structure as `MsgLinkNodeRequest`.
606
+
607
+ **JS:**
608
+ ```js
609
+ const msg = buildMsgUnlinkNode({ from, id, nodeAddress });
610
+ encodeMsgUnlinkNode({ from, id, nodeAddress, node_address })
611
+ ```
612
+
613
+ **C#:**
614
+ ```csharp
615
+ SentinelMessage msg = MessageBuilder.UnlinkNode(from, planId, nodeAddress);
616
+ ```
617
+
618
+ ---
619
+
620
+ ## Module: Provider (`sentinel.provider.v3`)
621
+
622
+ ### MsgRegisterProviderRequest
623
+
624
+ Register as a dVPN provider. One wallet = one provider. One-time operation.
625
+
626
+ - **Type URL:** `/sentinel.provider.v3.MsgRegisterProviderRequest`
627
+ - **Audience:** Plan operators only
628
+
629
+ **Protobuf fields:**
630
+
631
+ | Field | Number | Wire Type | Description |
632
+ |-------|--------|-----------|-------------|
633
+ | `from` | 1 | string | Account address (`sent1...`) |
634
+ | `name` | 2 | string | Provider display name |
635
+ | `identity` | 3 | string | Optional identity |
636
+ | `website` | 4 | string | Optional website URL |
637
+ | `description` | 5 | string | Optional description |
638
+
639
+ **JS:**
640
+ ```js
641
+ const msg = buildMsgRegisterProvider({ from, name, identity, website, description });
642
+ encodeMsgRegisterProvider({ from, name, identity, website, description })
643
+ ```
644
+
645
+ **C#:**
646
+ ```csharp
647
+ SentinelMessage msg = MessageBuilder.RegisterProvider(from, name, identity, website, description);
648
+ // identity, website, description: nullable strings
649
+ // Validates: from and name not empty
650
+ ```
651
+
652
+ **Notes:**
653
+ - Fails with "duplicate provider" if wallet already registered a provider
654
+ - After registration, the provider address (`sentprov1...`) is derived from the account address (`sent1...`)
655
+ - Provider endpoint remains at LCD v2: `/sentinel/provider/v2/providers/{sentprov1...}` (not migrated to v3)
656
+
657
+ ---
658
+
659
+ ### MsgUpdateProviderDetailsRequest
660
+
661
+ Update provider metadata.
662
+
663
+ - **Type URL:** `/sentinel.provider.v3.MsgUpdateProviderDetailsRequest`
664
+
665
+ **Protobuf fields:** Same as `MsgRegisterProviderRequest`.
666
+
667
+ **JS:**
668
+ ```js
669
+ const msg = buildMsgUpdateProviderDetails({ from, name, identity, website, description });
670
+ encodeMsgUpdateProviderDetails({ from, name, identity, website, description })
671
+ // Note: from should be sentprov1... in v3protocol, but JS accepts both
672
+ ```
673
+
674
+ **C#:**
675
+ ```csharp
676
+ SentinelMessage msg = MessageBuilder.UpdateProviderDetails(from, name, identity, website, description);
677
+ // from: sentprov1... provider address
678
+ // All params after from are nullable/optional
679
+ ```
680
+
681
+ ---
682
+
683
+ ### MsgUpdateProviderStatusRequest
684
+
685
+ Activate or deactivate a provider.
686
+
687
+ - **Type URL:** `/sentinel.provider.v3.MsgUpdateProviderStatusRequest`
688
+
689
+ **Protobuf fields:**
690
+
691
+ | Field | Number | Wire Type | Description |
692
+ |-------|--------|-----------|-------------|
693
+ | `from` | 1 | string | Account address (`sent1...`) |
694
+ | `status` | 2 | int64/enum | 1=active, 2=inactive\_pending, 3=inactive |
695
+
696
+ **JS:**
697
+ ```js
698
+ const msg = buildMsgUpdateProviderStatus({ from, status });
699
+ encodeMsgUpdateProviderStatus({ from, status })
700
+ ```
701
+
702
+ **C#:**
703
+ ```csharp
704
+ SentinelMessage msg = MessageBuilder.UpdateProviderStatus(from, status);
705
+ // Validates: status 1-3
706
+ ```
707
+
708
+ ---
709
+
710
+ ## Module: Lease (`sentinel.lease.v1`)
711
+
712
+ ### MsgStartLeaseRequest
713
+
714
+ Lease a node from its operator. Providers use leases to access nodes for their plans.
715
+
716
+ - **Type URL:** `/sentinel.lease.v1.MsgStartLeaseRequest`
717
+ - **Audience:** Plan operators only
718
+
719
+ **Protobuf fields:**
720
+
721
+ | Field | Number | Wire Type | Description |
722
+ |-------|--------|-----------|-------------|
723
+ | `from` | 1 | string | Provider address (`sentprov1...`) |
724
+ | `node_address` | 2 | string | Node address (`sentnode1...`) |
725
+ | `hours` | 3 | int64 | Lease duration in hours |
726
+ | `max_price` | 4 | embedded Price | Max hourly price (must match node exactly) |
727
+ | `renewal_price_policy` | 5 | int64 | Omit when 0 |
728
+
729
+ **JS:**
730
+ ```js
731
+ const msg = buildMsgStartLease({ from, nodeAddress, hours, maxPrice, renewalPricePolicy });
732
+ encodeMsgStartLease({ from, nodeAddress, node_address, hours, maxPrice, max_price, renewalPricePolicy, renewal_price_policy })
733
+ ```
734
+
735
+ **C#:**
736
+ ```csharp
737
+ SentinelMessage msg = MessageBuilder.StartLease(from, nodeAddress, hours, maxPrice, renewalPricePolicy);
738
+ // from: sentprov1... provider address
739
+ // hours: long (must be > 0)
740
+ // maxPrice: PriceEntry? (optional but recommended)
741
+ // Validates: hours > 0
742
+ ```
743
+
744
+ **Critical:** `maxPrice` must exactly match the node's current `hourly_prices` entry from LCD. Any mismatch (including stale price after a node update) → chain error "invalid price".
745
+
746
+ ---
747
+
748
+ ### MsgEndLeaseRequest
749
+
750
+ End an active lease.
751
+
752
+ - **Type URL:** `/sentinel.lease.v1.MsgEndLeaseRequest`
753
+ - **Audience:** Plan operators only
754
+
755
+ **Protobuf fields:**
756
+
757
+ | Field | Number | Wire Type | Description |
758
+ |-------|--------|-----------|-------------|
759
+ | `from` | 1 | string | Provider address (`sentprov1...`) |
760
+ | `id` | 2 | uint64 | Lease ID |
761
+
762
+ **JS:**
763
+ ```js
764
+ const msg = buildMsgEndLease({ from, id });
765
+ encodeMsgEndLease({ from, id })
766
+ ```
767
+
768
+ **C#:**
769
+ ```csharp
770
+ SentinelMessage msg = MessageBuilder.EndLease(from, leaseId);
771
+ // leaseId: ulong (must be > 0)
772
+ ```
773
+
774
+ ---
775
+
776
+ ## Module: Cosmos Feegrant (`cosmos.feegrant.v1beta1`)
777
+
778
+ ### MsgGrantAllowance
779
+
780
+ Grant a fee allowance — the granter pays gas fees on behalf of the grantee.
781
+
782
+ - **Type URL:** `/cosmos.feegrant.v1beta1.MsgGrantAllowance`
783
+ - **Audience:** Plan operators (paying user gas), any wallet
784
+
785
+ **Protobuf fields:**
786
+
787
+ | Field | Number | Wire Type | Description |
788
+ |-------|--------|-----------|-------------|
789
+ | `granter` | 1 | string | Address paying fees |
790
+ | `grantee` | 2 | string | Address receiving grant |
791
+ | `allowance` | 3 | embedded Any | Wraps `BasicAllowance` |
792
+
793
+ `BasicAllowance` fields:
794
+
795
+ | Field | Number | Type | Description |
796
+ |-------|--------|------|-------------|
797
+ | `spend_limit` | 1 | repeated Coin | Optional max spend |
798
+ | `expiration` | 2 | Timestamp | Optional expiry |
799
+
800
+ **JS (build from `broadcast.js`):**
801
+ ```js
802
+ // No dedicated buildMsgGrantAllowance in messages.js — construct directly or use broadcast helpers:
803
+ import { broadcastWithFeeGrant } from './chain/broadcast.js';
804
+
805
+ // For consumer TX with fee grant:
806
+ await broadcastWithFeeGrant(client, signerAddress, msgs, granterAddress, memo);
807
+ // gasPerMsg = 200,000; gasLimit = max(300,000, msgs.length * gasPerMsg)
808
+ ```
809
+
810
+ **C#:**
811
+ ```csharp
812
+ SentinelMessage msg = MessageBuilder.GrantFeeAllowance(
813
+ granter, // string — address paying fees
814
+ grantee, // string — address receiving grant
815
+ spendLimitUdvpn, // long? — optional max spend
816
+ expiration // DateTime? — optional expiry (UTC)
817
+ );
818
+ ```
819
+
820
+ **Notes:**
821
+ - `client.simulate()` does NOT work with fee grants (CosmJS limitation) — use a fixed gas estimate instead
822
+ - Recommended fixed gas: 300,000 per single-message TX; scale by `msgs.length * 200,000` for multi-message
823
+
824
+ ---
825
+
826
+ ### MsgRevokeAllowance
827
+
828
+ Revoke a previously granted fee allowance.
829
+
830
+ - **Type URL:** `/cosmos.feegrant.v1beta1.MsgRevokeAllowance`
831
+
832
+ **Protobuf fields:**
833
+
834
+ | Field | Number | Wire Type | Description |
835
+ |-------|--------|-----------|-------------|
836
+ | `granter` | 1 | string | Address that granted fees |
837
+ | `grantee` | 2 | string | Address whose grant is revoked |
838
+
839
+ **C#:**
840
+ ```csharp
841
+ SentinelMessage msg = MessageBuilder.RevokeFeeAllowance(granter, grantee);
842
+ ```
843
+
844
+ **JS:** Construct the `{ typeUrl, value }` object directly using `MSG_TYPES.REVOKE_FEE_ALLOWANCE`.
845
+
846
+ ---
847
+
848
+ ## Module: Cosmos Authz (`cosmos.authz.v1beta1`)
849
+
850
+ ### MsgGrant
851
+
852
+ Grant authorization for a grantee to execute a specific message type on behalf of the granter.
853
+
854
+ - **Type URL:** `/cosmos.authz.v1beta1.MsgGrant`
855
+
856
+ **Protobuf fields:**
857
+
858
+ | Field | Number | Wire Type | Description |
859
+ |-------|--------|-----------|-------------|
860
+ | `granter` | 1 | string | Address granting permission |
861
+ | `grantee` | 2 | string | Address receiving permission |
862
+ | `grant` | 3 | embedded Grant | Contains GenericAuthorization + optional expiry |
863
+
864
+ `Grant` wraps `GenericAuthorization` (which contains the `msg` type URL) inside a `google.protobuf.Any`.
865
+
866
+ **C#:**
867
+ ```csharp
868
+ SentinelMessage msg = MessageBuilder.AuthzGrant(
869
+ granter, // string
870
+ grantee, // string
871
+ msgTypeUrl, // string — e.g. "/sentinel.node.v3.MsgStartSessionRequest"
872
+ expiration // DateTime? — optional
873
+ );
874
+ ```
875
+
876
+ **JS:** Construct directly using `MSG_TYPES.AUTHZ_GRANT`.
877
+
878
+ ---
879
+
880
+ ### MsgRevoke
881
+
882
+ Revoke a previously granted authorization.
883
+
884
+ - **Type URL:** `/cosmos.authz.v1beta1.MsgRevoke`
885
+
886
+ **Protobuf fields:**
887
+
888
+ | Field | Number | Wire Type | Description |
889
+ |-------|--------|-----------|-------------|
890
+ | `granter` | 1 | string | Address that granted permission |
891
+ | `grantee` | 2 | string | Address whose permission is revoked |
892
+ | `msg_type_url` | 3 | string | Message type URL to revoke |
893
+
894
+ **C#:**
895
+ ```csharp
896
+ SentinelMessage msg = MessageBuilder.AuthzRevoke(granter, grantee, msgTypeUrl);
897
+ ```
898
+
899
+ ---
900
+
901
+ ### MsgExec
902
+
903
+ Execute messages on behalf of a granter using a previously granted authorization.
904
+
905
+ - **Type URL:** `/cosmos.authz.v1beta1.MsgExec`
906
+
907
+ **Protobuf fields:**
908
+
909
+ | Field | Number | Wire Type | Description |
910
+ |-------|--------|-----------|-------------|
911
+ | `grantee` | 1 | string | Address executing on behalf of granter |
912
+ | `msgs` | 2 | repeated Any | Messages to execute (pre-built, wrapped as Any) |
913
+
914
+ **C#:**
915
+ ```csharp
916
+ SentinelMessage exec = MessageBuilder.AuthzExec(grantee, new[] { innerMsg1, innerMsg2 });
917
+ // innerMsgs: SentinelMessage[] — each wrapped as Any internally
918
+ // Validates: at least one inner message required
919
+ ```
920
+
921
+ ---
922
+
923
+ ## Module: Cosmos Bank (`cosmos.bank.v1beta1`)
924
+
925
+ ### MsgSend
926
+
927
+ Transfer P2P tokens between addresses.
928
+
929
+ - **Type URL:** `/cosmos.bank.v1beta1.MsgSend`
930
+
931
+ **Protobuf fields:**
932
+
933
+ | Field | Number | Wire Type | Description |
934
+ |-------|--------|-----------|-------------|
935
+ | `from_address` | 1 | string | Sender address |
936
+ | `to_address` | 2 | string | Recipient address |
937
+ | `amount` | 3 | repeated Coin | Token amount(s) |
938
+
939
+ **JS (`broadcast.js`):**
940
+ ```js
941
+ import { sendTokens } from './chain/broadcast.js';
942
+
943
+ // High-level:
944
+ await sendTokens(client, fromAddress, toAddress, amountUdvpn, memo);
945
+ // amountUdvpn: string | number | bigint | { amount, denom }
946
+
947
+ // Low-level batch:
948
+ const msgs = buildBatchSend(fromAddress, [
949
+ { address: to1, amountUdvpn: 1000000 },
950
+ { address: to2, amountUdvpn: 2000000 },
951
+ ]);
952
+ await broadcast(client, fromAddress, msgs);
953
+ ```
954
+
955
+ **C#:**
956
+ ```csharp
957
+ SentinelMessage msg = MessageBuilder.Send(from, to, amountUdvpn);
958
+ // amountUdvpn: long (must be > 0)
959
+ ```
960
+
961
+ ---
962
+
963
+ ## JS SDK — Registry and Broadcast Layer
964
+
965
+ ### `buildRegistry()` — `chain/client.js`
966
+
967
+ Registers all 23 Sentinel message types with CosmJS. Required before any `signAndBroadcast` call.
968
+
969
+ ```js
970
+ import { buildRegistry } from './chain/client.js';
971
+ // Called internally by createClient() — no manual call needed for most use cases.
972
+ ```
973
+
974
+ All registered type URLs (from `buildRegistry()`):
975
+
976
+ ```
977
+ /sentinel.node.v3.MsgStartSessionRequest
978
+ /sentinel.session.v3.MsgCancelSessionRequest
979
+ /sentinel.subscription.v3.MsgStartSubscriptionRequest
980
+ /sentinel.subscription.v3.MsgStartSessionRequest
981
+ /sentinel.plan.v3.MsgStartSessionRequest
982
+ /sentinel.plan.v3.MsgCreatePlanRequest
983
+ /sentinel.plan.v3.MsgLinkNodeRequest
984
+ /sentinel.plan.v3.MsgUnlinkNodeRequest
985
+ /sentinel.plan.v3.MsgUpdatePlanStatusRequest
986
+ /sentinel.provider.v3.MsgRegisterProviderRequest
987
+ /sentinel.provider.v3.MsgUpdateProviderDetailsRequest
988
+ /sentinel.provider.v3.MsgUpdateProviderStatusRequest
989
+ /sentinel.plan.v3.MsgUpdatePlanDetailsRequest
990
+ /sentinel.lease.v1.MsgStartLeaseRequest
991
+ /sentinel.lease.v1.MsgEndLeaseRequest
992
+ /sentinel.subscription.v3.MsgCancelSubscriptionRequest
993
+ /sentinel.subscription.v3.MsgRenewSubscriptionRequest
994
+ /sentinel.subscription.v3.MsgShareSubscriptionRequest
995
+ /sentinel.subscription.v3.MsgUpdateSubscriptionRequest
996
+ /sentinel.session.v3.MsgUpdateSessionRequest
997
+ /sentinel.node.v3.MsgRegisterNodeRequest
998
+ /sentinel.node.v3.MsgUpdateNodeDetailsRequest
999
+ /sentinel.node.v3.MsgUpdateNodeStatusRequest
1000
+ ```
1001
+
1002
+ Cosmos standard types (feegrant, authz, bank) are included via `defaultRegistryTypes`.
1003
+
1004
+ ### `MSG_TYPES` constants — `chain/client.js`
1005
+
1006
+ ```js
1007
+ import { MSG_TYPES } from './chain/client.js';
1008
+ // Same constants also exported as TYPE_URLS from './protocol/messages.js'
1009
+ ```
1010
+
1011
+ ### `broadcast(client, signerAddress, msgs, fee)` — `chain/broadcast.js`
1012
+
1013
+ Simple single-shot broadcast. Use for one-off TXs.
1014
+
1015
+ ```js
1016
+ import { broadcast } from './chain/broadcast.js';
1017
+ const result = await broadcast(client, address, [msg]);
1018
+ // fee: optional. Null/'auto'/{gas,amount} — defaults to 'auto'
1019
+ // Throws ChainError on network failure or non-zero code
1020
+ ```
1021
+
1022
+ ### `createSafeBroadcaster(rpcUrl, wallet, signerAddress)` — `chain/broadcast.js`
1023
+
1024
+ Mutex-serialized broadcaster with sequence recovery and RPC rotation. Use for any workflow sending multiple TXs.
1025
+
1026
+ ```js
1027
+ const { safeBroadcast } = createSafeBroadcaster(rpcUrl, wallet, signerAddress);
1028
+ const result = await safeBroadcast([msg1, msg2]); // batch = one TX
1029
+ // Retries: 5 attempts, 2s/4s/6s backoff, sequence reset on error code 32
1030
+ // RPC rotation: tries all endpoints on connection failure
1031
+ ```
1032
+
1033
+ ### `broadcastWithFeeGrant(client, signerAddress, msgs, granterAddress, memo)` — `chain/broadcast.js`
1034
+
1035
+ Broadcast with fee grant — granter pays gas.
1036
+
1037
+ ```js
1038
+ const result = await broadcastWithFeeGrant(client, signerAddress, msgs, granterAddress);
1039
+ // Fixed gas: max(300_000, msgs.length * 200_000)
1040
+ // DO NOT use client.simulate() with fee grants — it fails
1041
+ ```
1042
+
1043
+ ### Broadcast helper functions
1044
+
1045
+ ```js
1046
+ // Build batch start session messages
1047
+ buildBatchStartSession(from, nodes)
1048
+ // nodes: Array<{ nodeAddress, gigabytes?, maxPrice }>
1049
+
1050
+ // Build end session message
1051
+ buildEndSessionMsg(from, sessionId)
1052
+ // WARNING: uses BigInt(sessionId) — inconsistent with buildMsgCancelSession which uses Number(id)
1053
+
1054
+ // Batch token send
1055
+ buildBatchSend(fromAddress, recipients)
1056
+ // recipients: Array<{ address, amountUdvpn }>
1057
+
1058
+ // Batch link nodes to plan
1059
+ buildBatchLink(provAddress, planId, nodeAddresses)
1060
+
1061
+ // Send tokens
1062
+ sendTokens(client, fromAddress, toAddress, amountUdvpn, memo)
1063
+
1064
+ // Subscribe to plan (returns subscriptionId from events)
1065
+ subscribeToPlan(client, fromAddress, planId, denom)
1066
+
1067
+ // Share subscription
1068
+ shareSubscription(client, ownerAddress, subscriptionId, recipientAddress, bytes)
1069
+ shareSubscriptionWithFeeGrant(client, ownerAddress, subscriptionId, recipientAddress, bytes, granterAddress)
1070
+
1071
+ // Onboard user to plan (subscribe + share + optional fee grant)
1072
+ onboardPlanUser(client, operatorAddress, { planId, userAddress, bytes, denom, grantFee, feeSpendLimit, feeExpiration, buildFeeGrant })
1073
+
1074
+ // Estimate session cost
1075
+ estimateSessionCost(nodeInfo, gigabytes, { preferHourly, hours })
1076
+ // Returns: { udvpn, dvpn, gasUdvpn, totalUdvpn, mode, hourlyUdvpn, gigabyteUdvpn }
1077
+
1078
+ // Gas fee estimation
1079
+ estimateBatchFee(msgCount, msgType)
1080
+ // msgType: 'startSession' | 'feeGrant' | 'send' | 'link'
1081
+ ```
1082
+
1083
+ ### TX Event extraction
1084
+
1085
+ ```js
1086
+ // Extract single ID from TX events (session, subscription, plan, lease)
1087
+ extractId(txResult, /session/i, ['session_id', 'id'])
1088
+ extractId(txResult, /subscription/i, ['subscription_id', 'id'])
1089
+ extractId(txResult, /plan/i, ['plan_id', 'id'])
1090
+
1091
+ // Extract all session IDs from a batch TX
1092
+ extractAllSessionIds(txResult) // returns bigint[]
1093
+
1094
+ // Decode base64-encoded TX events
1095
+ decodeTxEvents(events)
1096
+
1097
+ // Serialize result (BigInt → string for JSON responses)
1098
+ serializeResult(connectResult)
1099
+
1100
+ // Parse chain error to user-friendly message
1101
+ parseChainError(raw)
1102
+ ```
1103
+
1104
+ ---
1105
+
1106
+ ## JS SDK — Query Functions
1107
+
1108
+ ### LCD Queries (`chain/queries.js`)
1109
+
1110
+ | Function | LCD Path | Returns |
1111
+ |----------|----------|---------|
1112
+ | `getBalance(client, address)` | `/cosmos/bank/v1beta1/balances/{addr}` | `{ udvpn: number, dvpn: number }` |
1113
+ | `fetchActiveNodes(lcdUrl, limit, maxPages)` | `/sentinel/node/v3/nodes?status=1` | `Node[]` (cached 5 min) |
1114
+ | `queryNode(nodeAddress, opts)` | `/sentinel/node/v3/nodes/{addr}` | `Node` object |
1115
+ | `getNodePrices(nodeAddress, lcdUrl)` | via `queryNode()` | `{ gigabyte, hourly, denom, nodeAddress }` |
1116
+ | `querySubscriptions(lcdUrl, walletAddr, opts)` | `/sentinel/subscription/v3/accounts/{addr}/subscriptions` | `{ subscriptions, total }` |
1117
+ | `querySubscription(id, lcdUrl)` | `/sentinel/subscription/v3/subscriptions/{id}` | `Subscription \| null` |
1118
+ | `hasActiveSubscription(address, planId, lcdUrl)` | via `querySubscriptions()` | `{ has: boolean, subscription? }` |
1119
+ | `querySubscriptionAllocations(subscriptionId, lcdUrl)` | `/sentinel/subscription/v2/subscriptions/{id}/allocations` | `Allocation[]` |
1120
+ | `querySessions(address, lcdUrl, opts)` | `/sentinel/session/v3/sessions?address={addr}` | `{ items: Session[], total }` |
1121
+ | `querySessionById(lcdUrl, sessionId)` | `/sentinel/session/v3/sessions/{id}` | `Session \| null` |
1122
+ | `querySessionAllocation(lcdUrl, sessionId)` | `/sentinel/session/v3/sessions/{id}` | `{ maxBytes, usedBytes, remainingBytes, percentUsed }` |
1123
+ | `findExistingSession(lcdUrl, walletAddr, nodeAddr)` | `/sentinel/session/v3/sessions?address={addr}&status=1` | `BigInt \| null` |
1124
+ | `queryPlanNodes(planId, lcdUrl)` | `/sentinel/node/v3/plans/{id}/nodes?pagination.limit=5000` | `{ items, total }` |
1125
+ | `queryPlanSubscribers(planId, opts)` | `/sentinel/subscription/v3/plans/{id}/subscriptions` | `{ subscribers, total }` |
1126
+ | `getPlanStats(planId, ownerAddress, opts)` | via `queryPlanSubscribers()` | `{ subscriberCount, totalOnChain, ownerSubscribed }` |
1127
+ | `discoverPlans(lcdUrl, opts)` | Probes subscriptions + nodes per plan ID | `DiscoveredPlan[]` |
1128
+ | `discoverPlanIds(lcdUrl, maxId)` | via `discoverPlans()` | `number[]` |
1129
+ | `getProviderByAddress(provAddress, opts)` | `/sentinel/provider/v2/providers/{addr}` | `Provider \| null` |
1130
+ | `getNetworkOverview(lcdUrl)` | via `fetchActiveNodes()` | `{ totalNodes, byCountry, byType, averagePrice, nodes }` |
1131
+ | `flattenSession(session)` | — (utility) | Flattened session object |
1132
+ | `resolveNodeUrl(node)` | — (utility) | HTTPS URL string |
1133
+ | `invalidateNodeCache()` | — (utility) | Clears 5-min node cache |
1134
+ | `loadVpnSettings()` | `~/.sentinel-sdk/settings.json` | `Record<string, any>` |
1135
+ | `saveVpnSettings(settings)` | `~/.sentinel-sdk/settings.json` | void |
1136
+
1137
+ **Important note on `querySubscriptionAllocations`:** This function uses the v2 LCD path because the v3 equivalent returns 501 Not Implemented. Same situation applies to `/sentinel/plan/v3/plans/{id}` — the plan detail endpoint is also 501. Use `discoverPlans()` instead.
1138
+
1139
+ **`flattenSession()` is mandatory** — sessions from `/sentinel/session/v3/sessions` have fields nested under `base_session`. Accessing `session.id` without flattening returns `undefined`. `querySessions()` auto-flattens; `querySessionById()` also flattens.
1140
+
1141
+ ### RPC Queries (`chain/rpc.js`)
1142
+
1143
+ RPC queries use protobuf transport via CosmJS ABCI — approximately 912x faster than LCD for bulk operations.
1144
+
1145
+ | Function | gRPC Path | Returns |
1146
+ |----------|-----------|---------|
1147
+ | `createRpcQueryClient(rpcUrl)` | — | `{ queryClient, rpc, tmClient }` |
1148
+ | `createRpcQueryClientWithFallback()` | Tries all RPC endpoints | `{ queryClient, rpc, tmClient, url }` |
1149
+ | `disconnectRpc()` | — | Clears cached client |
1150
+ | `rpcQueryNodes(client, { status, limit })` | `/sentinel.node.v3.QueryService/QueryNodes` | `Node[]` |
1151
+ | `rpcQueryNode(client, address)` | `/sentinel.node.v3.QueryService/QueryNode` | `Node \| null` |
1152
+ | `rpcQueryNodesForPlan(client, planId, { status, limit })` | `/sentinel.node.v3.QueryService/QueryNodesForPlan` | `Node[]` |
1153
+ | `rpcQuerySessionsForAccount(client, address, { limit })` | `/sentinel.session.v3.QueryService/QuerySessionsForAccount` | `Uint8Array[]` (raw) |
1154
+ | `rpcQuerySubscriptionsForAccount(client, address, { limit })` | `/sentinel.subscription.v3.QueryService/QuerySubscriptionsForAccount` | `Uint8Array[]` (raw) |
1155
+ | `rpcQueryPlan(client, planId)` | `/sentinel.plan.v3.QueryService/QueryPlan` | `Uint8Array \| null` (raw) |
1156
+ | `rpcQueryBalance(client, address, denom)` | `/cosmos.bank.v1beta1.Query/Balance` | `{ denom, amount }` |
1157
+
1158
+ **Note:** RPC session/subscription/plan responses return raw `Uint8Array` protobuf bytes that require type-specific decoding. For most use cases, LCD queries return parsed JSON and are easier to work with. Use RPC for high-throughput bulk operations (e.g., scanning 1000+ nodes).
1159
+
1160
+ ---
1161
+
1162
+ ## C# SDK — Query Methods (`ChainClient.Queries.cs`)
1163
+
1164
+ | Method | LCD Path | Returns |
1165
+ |--------|----------|---------|
1166
+ | `GetBalanceAsync(address)` | `/cosmos/bank/v1beta1/balances/{addr}/by_denom?denom=udvpn` | `Balance` |
1167
+ | `GetActiveNodesAsync(limit)` | `/sentinel/node/v3/nodes?status=1&pagination.limit={N}` | `List<ChainNode>` |
1168
+ | `GetNodeAsync(nodeAddress)` | `/sentinel/node/v3/nodes/{addr}` | `ChainNode?` |
1169
+ | `GetSubscriptionsAsync(address)` | `/sentinel/subscription/v3/accounts/{addr}/subscriptions` | `List<Subscription>` |
1170
+ | `GetSessionsAsync(address, status)` | `/sentinel/session/v3/accounts/{addr}/sessions?status={N}` | `List<ChainSession>` |
1171
+ | `GetPlanNodesAsync(planId)` | `/sentinel/node/v3/plans/{id}/nodes?pagination.limit=5000` | `List<ChainNode>` |
1172
+ | `DiscoverPlansAsync(maxId)` | Probes subscriptions + nodes per plan ID | `List<DiscoveredPlan>` |
1173
+ | `GetAccountInfoAsync(address)` | `/cosmos/auth/v1beta1/accounts/{addr}` | `(ulong AccountNumber, ulong Sequence)` |
1174
+
1175
+ **C# `GetSessionsAsync` note:** Uses `/sentinel/session/v3/accounts/{addr}/sessions` (not `/sessions?address={addr}`). The query-param format may return all sessions unfiltered — prefer the `/accounts/{addr}/sessions` path.
1176
+
1177
+ ---
1178
+
1179
+ ## C# SDK — Transaction Builder (`TransactionBuilder.cs`)
1180
+
1181
+ The `TransactionBuilder` implements SIGN\_MODE\_DIRECT with secp256k1 signing from scratch (no CosmJS dependency).
1182
+
1183
+ ```csharp
1184
+ var txBuilder = new TransactionBuilder(wallet, client);
1185
+
1186
+ // Optional: use fee grant (granter pays gas)
1187
+ txBuilder.FeeGranter = providerAddress;
1188
+
1189
+ // Broadcast pre-encoded SentinelMessage objects
1190
+ TxResult result = await txBuilder.BroadcastAsync(
1191
+ MessageBuilder.StartSession(from, nodeAddress, gigabytes: 1, maxPrice)
1192
+ );
1193
+
1194
+ // Broadcast protobuf IMessage objects (if using generated protos)
1195
+ TxResult result = await txBuilder.BroadcastProtobufAsync(protoMsg);
1196
+ ```
1197
+
1198
+ Gas estimation (C#): `200,000 * message_count * 1.4` safety multiplier. Fee = `ceil(gas * gasPrice)`.
1199
+
1200
+ Sequence retry: up to 6 attempts with 2s/4s/6s backoff. Checks if previous TX was already committed before retrying (avoids double-spend).
1201
+
1202
+ High-level composite operations:
1203
+
1204
+ ```csharp
1205
+ // Share bandwidth
1206
+ TxResult result = await txBuilder.ShareSubscriptionAsync(subscriptionId, recipientAddress, bytes);
1207
+
1208
+ // Complete plan user onboarding (subscribe + share + optional fee grant)
1209
+ OnboardResult result = await txBuilder.OnboardPlanUserAsync(
1210
+ planId, userAddress, bytes,
1211
+ denom: "udvpn",
1212
+ grantFee: false,
1213
+ feeSpendLimit: 500_000,
1214
+ feeExpiration: null
1215
+ );
1216
+ // Returns: OnboardResult { SubscriptionId, SubscribeTxHash, ShareTxHash, GrantTxHash? }
1217
+ ```
1218
+
1219
+ ---
1220
+
1221
+ ## Parity Matrix
1222
+
1223
+ | Message Type | JS `buildMsg` | JS `encodeMsg` | JS broadcast | C# Builder | C# TX | Status |
1224
+ |-------------|---------------|----------------|--------------|------------|-------|--------|
1225
+ | StartSession (node) | `buildMsgStartSession` | `encodeMsgStartSession` | `broadcast` | `StartSession` | `BroadcastAsync` | Verified |
1226
+ | CancelSession | `buildMsgCancelSession` | `encodeMsgEndSession` | `broadcast` | `EndSession` | `BroadcastAsync` | Verified |
1227
+ | UpdateSession | `buildMsgUpdateSession` | `encodeMsgUpdateSession` | `broadcast` | `UpdateSession` | `BroadcastAsync` | Untested |
1228
+ | StartSubscription | `buildMsgStartSubscription` | `encodeMsgStartSubscription` | `subscribeToPlan` | `StartSubscription` | `BroadcastAsync` | Verified |
1229
+ | SubStartSession | `buildMsgSubStartSession` | `encodeMsgSubStartSession` | `broadcast` | `SubStartSession` | `BroadcastAsync` | Verified |
1230
+ | CancelSubscription | `buildMsgCancelSubscription` | `encodeMsgCancelSubscription` | `broadcast` | `CancelSubscription` | `BroadcastAsync` | Untested |
1231
+ | RenewSubscription | `buildMsgRenewSubscription` | `encodeMsgRenewSubscription` | `broadcast` | `RenewSubscription` | `BroadcastAsync` | Untested |
1232
+ | ShareSubscription | `buildMsgShareSubscription` | `encodeMsgShareSubscription` | `shareSubscription` | `ShareSubscription` | `ShareSubscriptionAsync` | Verified |
1233
+ | UpdateSubscription | `buildMsgUpdateSubscription` | `encodeMsgUpdateSubscription` | `broadcast` | `UpdateSubscription` | `BroadcastAsync` | Untested |
1234
+ | PlanStartSession | `buildMsgPlanStartSession` | `encodeMsgPlanStartSession` | `broadcast` | `PlanStartSession` | `BroadcastAsync` | Verified |
1235
+ | CreatePlan | `buildMsgCreatePlan` | `encodeMsgCreatePlan` | `broadcast` | `CreatePlan` | `BroadcastAsync` | Verified |
1236
+ | UpdatePlanDetails | `buildMsgUpdatePlanDetails` | `encodeMsgUpdatePlanDetails` | `broadcast` | `UpdatePlanDetails` | `BroadcastAsync` | Untested |
1237
+ | UpdatePlanStatus | `buildMsgUpdatePlanStatus` | `encodeMsgUpdatePlanStatus` | `broadcast` | `UpdatePlanStatus` | `BroadcastAsync` | Verified |
1238
+ | LinkNode | `buildMsgLinkNode` | `encodeMsgLinkNode` | `broadcast` | `LinkNode` | `BroadcastAsync` | Verified |
1239
+ | UnlinkNode | `buildMsgUnlinkNode` | `encodeMsgUnlinkNode` | `broadcast` | `UnlinkNode` | `BroadcastAsync` | Untested |
1240
+ | RegisterProvider | `buildMsgRegisterProvider` | `encodeMsgRegisterProvider` | `broadcast` | `RegisterProvider` | `BroadcastAsync` | Verified |
1241
+ | UpdateProviderDetails | `buildMsgUpdateProviderDetails` | `encodeMsgUpdateProviderDetails` | `broadcast` | `UpdateProviderDetails` | `BroadcastAsync` | Untested |
1242
+ | UpdateProviderStatus | `buildMsgUpdateProviderStatus` | `encodeMsgUpdateProviderStatus` | `broadcast` | `UpdateProviderStatus` | `BroadcastAsync` | Untested |
1243
+ | StartLease | `buildMsgStartLease` | `encodeMsgStartLease` | `broadcast` | `StartLease` | `BroadcastAsync` | Verified |
1244
+ | EndLease | `buildMsgEndLease` | `encodeMsgEndLease` | `broadcast` | `EndLease` | `BroadcastAsync` | Untested |
1245
+ | RegisterNode | `buildMsgRegisterNode` | `encodeMsgRegisterNode` | `broadcast` | `RegisterNode` | `BroadcastAsync` | Untested |
1246
+ | UpdateNodeDetails | `buildMsgUpdateNodeDetails` | `encodeMsgUpdateNodeDetails` | `broadcast` | `UpdateNodeDetails` | `BroadcastAsync` | Untested |
1247
+ | UpdateNodeStatus | `buildMsgUpdateNodeStatus` | `encodeMsgUpdateNodeStatus` | `broadcast` | `UpdateNodeStatus` | `BroadcastAsync` | Untested |
1248
+ | GrantFeeAllowance | — (construct directly) | — | `broadcastWithFeeGrant` | `GrantFeeAllowance` | `BroadcastAsync` | Verified |
1249
+ | RevokeFeeAllowance | — (construct directly) | — | `broadcast` | `RevokeFeeAllowance` | `BroadcastAsync` | Untested |
1250
+ | AuthzGrant | — (construct directly) | — | `broadcast` | `AuthzGrant` | `BroadcastAsync` | Untested |
1251
+ | AuthzRevoke | — (construct directly) | — | `broadcast` | `AuthzRevoke` | `BroadcastAsync` | Untested |
1252
+ | AuthzExec | — (construct directly) | — | `broadcast` | `AuthzExec` | `BroadcastAsync` | Untested |
1253
+ | MsgSend (bank) | — | — | `sendTokens` | `Send` | `BroadcastAsync` | Verified |
1254
+
1255
+ **Status key:**
1256
+ - **Verified** — mainnet-tested with real wallet, real nodes, real tokens
1257
+ - **Untested** — code written and cross-referenced, not yet mainnet-verified in this language
1258
+
1259
+ **JS gaps:**
1260
+ - No dedicated `buildMsgGrantAllowance` / `buildMsgRevoke*` / `buildMsgAuthz*` functions in `messages.js` — these must be constructed as `{ typeUrl, value }` objects directly
1261
+ - `buildEndSessionMsg` in `broadcast.js` uses `BigInt(sessionId)` while `buildMsgCancelSession` in `messages.js` uses `Number(id)` — inconsistency; prefer `buildMsgCancelSession`
1262
+
1263
+ **C# gaps:**
1264
+ - No `buildMsgGrantAllowance` shorthand at the `buildMsg` layer — but `MessageBuilder.GrantFeeAllowance()` covers this at the encode layer
1265
+
1266
+ ---
1267
+
1268
+ ## Known Bugs and Edge Cases
1269
+
1270
+ ### Price Field Encoding (JS `buildMsgStartSession`)
1271
+
1272
+ The `messages.js` layer passes `maxPrice` directly to CosmJS as a plain object. The `v3protocol.js` / `encoding.js` layer manually encodes it using `encodePrice()`. The `encodePrice()` function calls `decToScaledInt()` on `base_value`, scaling it by 10^18.
1273
+
1274
+ **Bug:** If `base_value` is already scaled (e.g., from a node that returns the stored chain value directly), double-scaling corrupts the price. Always pass the raw LCD `quote_value` and `base_value` from the node's price array without pre-processing.
1275
+
1276
+ ### `id` Field Type Inconsistency (JS)
1277
+
1278
+ `buildMsgCancelSession` converts `id` to `Number(id)`. `buildEndSessionMsg` converts to `BigInt(id)`. For session IDs that exceed `Number.MAX_SAFE_INTEGER` (9 quadrillion+), `Number()` truncates silently. Use `BigInt` in the encode layer; the `buildMsg` layer is safe for current chain session ID ranges.
1279
+
1280
+ ### Session LCD Path
1281
+
1282
+ `querySessions(address, lcdUrl)` uses `/sentinel/session/v3/sessions?address={addr}`. The C# SDK uses `/sentinel/session/v3/accounts/{addr}/sessions`. Both paths work but may differ in filtering behavior. Use the `/accounts/{addr}/sessions` form when filtering by status for more reliable results.
1283
+
1284
+ ### Plan Pagination Bug (LCD)
1285
+
1286
+ `/sentinel/node/v3/plans/{id}/nodes` returns incorrect `count_total` and always returns `null` for `next_key`. Use `pagination.limit=5000` in a single request and count the returned array. Both JS and C# SDKs implement this workaround.
1287
+
1288
+ ### Plan Detail Endpoint (501)
1289
+
1290
+ `/sentinel/plan/v3/plans/{id}` returns 501 Not Implemented on all LCD endpoints. Use `discoverPlans()` / `DiscoverPlansAsync()` which probe via the subscriptions endpoint instead.
1291
+
1292
+ ### Provider Endpoint Stays v2
1293
+
1294
+ `/sentinel/provider/v2/providers/{sentprov1...}` — the provider query endpoint was not migrated to v3. All other Sentinel queries use v3 paths.
1295
+
1296
+ ### `client.simulate()` with Fee Grants (CosmJS)
1297
+
1298
+ `client.simulate()` does not include the `granter` field, so it bills gas to the grantee. If the grantee has low balance, simulation fails with "insufficient funds" even though the actual TX would succeed via fee grant. Use a fixed gas estimate (300,000 per single-message TX) instead of simulation when using fee grants.
1299
+
1300
+ ### Plan Creation Requires Separate Activation
1301
+
1302
+ `MsgCreatePlanRequest` creates a plan in **INACTIVE** status. A second TX (`MsgUpdatePlanStatusRequest` with `status=1`) is required to activate it. Forgetting this step leaves the plan permanently inactive.
1303
+
1304
+ ### Sequence Mismatch Recovery (Code 32)
1305
+
1306
+ Both JS (`createSafeBroadcaster`) and C# (`TransactionBuilder.BroadcastAsync`) handle Cosmos error code 32 (wrong sequence). Both implementations check if the previous TX was already committed before retrying, preventing double-spend. Both retry up to 5–6 times with exponential backoff.
1307
+
1308
+ ### Subscription Sharing — Bytes Only
1309
+
1310
+ `MsgShareSubscriptionRequest` has no time/duration field. The chain only tracks bytes. For time-based plans (e.g., monthly access), the operator must track expiry externally and cancel or not renew the subscription when the period expires.