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 +13 -4
- package/batch.js +2 -2
- package/chain/broadcast.js +113 -3
- package/chain/client.js +54 -6
- package/chain/queries.js +21 -0
- package/connection/connect.js +4 -4
- package/cosmjs-setup.js +52 -7
- package/docs/CHAIN-PROTOCOL-UPGRADE-PROPOSAL.md +662 -0
- package/docs/ON-CHAIN-FUNCTIONS.md +1310 -0
- package/index.js +12 -0
- package/package.json +2 -1
- package/plan-operations.js +18 -11
- package/protocol/encoding.js +38 -24
- package/protocol/messages.js +19 -19
- package/protocol/plans.js +18 -11
- package/protocol/v3.js +11 -7
- package/test-subscription-flows.js +457 -0
- package/v3protocol.js +38 -24
- package/test-all-chain-ops.js +0 -493
- package/test-feegrant-connect.js +0 -98
- package/test-logic.js +0 -148
|
@@ -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.
|