@blockula/sdk 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tx.js CHANGED
@@ -1,32 +1,196 @@
1
1
  // Canonical transaction encoding. Must stay byte-identical to node/src/types.rs `Tx::signing_bytes`.
2
+ import { blake3 } from "@noble/hashes/blake3.js";
3
+ import { bytesToHex, concatBytes, hexToBytes } from "@noble/hashes/utils.js";
4
+ import { bech32m } from "@scure/base";
2
5
  export const PUBLIC_KEY_LEN = 1952;
3
6
  export const SIGNATURE_LEN = 3309;
7
+ /** Domain of the unsigned ghost wire form (`private_transfer`, `unshield`). */
8
+ export const GHOST_TX_DOMAIN = "BLOCKULA_GHOST_V1";
9
+ export const GHOST_ENC_BYTES = 1401;
10
+ export const GHOST_OUT_BYTES = 80;
11
+ export const GHOST_ADDRESS_BYTES = 1216;
12
+ /** The Action circuit the node verifies proofs against (blockula-ghost `CIRCUIT_ID`). */
13
+ export const GHOST_CIRCUIT_ID = 1;
4
14
  export const DROPS_PER_FANG = 1000000000n;
5
15
  const utf8 = new TextEncoder();
16
+ const U64_LIMIT = 1n << 64n;
17
+ /**
18
+ * The largest value of a whole-number field a tx carries as a JSON number, not a string (ids,
19
+ * heights, PR numbers, the nonce): 2^53 − 1, what a JavaScript number holds exactly
20
+ * (node/src/types.rs `MAX_JSON_INT`). The chain refuses larger ones, so every stored proposal
21
+ * kind and multisig action reads back exactly here, and its digest can be computed.
22
+ */
23
+ export const MAX_JSON_INT = (1n << 53n) - 1n;
24
+ /** An optional field's JSON default (`#[serde(default)]` in node/src/types.rs): only when it's absent, never for `null`. */
25
+ const orDefault = (v, d) => (v === undefined ? d : v);
26
+ /**
27
+ * An unsigned integer field as the node reads it from JSON (node/src/types.rs: a JSON number,
28
+ * or for amounts a decimal string), checked to be in `0 ..= max`. Anything the node's JSON
29
+ * parser would refuse is refused here, so `signingBytes`/`txBytes` never encode a value other
30
+ * than the one in the tx object (no wrapping of negative or oversized numbers).
31
+ */
32
+ export function uintField(v, max, what = "integer field") {
33
+ let n;
34
+ if (typeof v === "bigint")
35
+ n = v;
36
+ else if (typeof v === "number") {
37
+ if (!Number.isInteger(v))
38
+ throw new RangeError(`${what} must be a whole number, got ${v}`);
39
+ // Above 2^53 − 1 a JavaScript number may not be the value that was meant: pass a string of
40
+ // digits (where the field can be that large).
41
+ if (!Number.isSafeInteger(v)) {
42
+ if (max <= MAX_JSON_INT)
43
+ throw new RangeError(`${what} must be 0 to ${max}, got ${v}${v > Number.MAX_SAFE_INTEGER ? " (past 2^53 − 1: not even exact as a JavaScript number)" : ""}`);
44
+ throw new RangeError(`${what} must be 0 to ${Number.MAX_SAFE_INTEGER} as a number (larger values as a string of digits), got ${v}`);
45
+ }
46
+ n = BigInt(v);
47
+ }
48
+ else if (typeof v === "string") {
49
+ if (!/^\d+$/.test(v))
50
+ throw new RangeError(`${what} must be a decimal string of digits, got ${JSON.stringify(v)}`);
51
+ n = BigInt(v);
52
+ }
53
+ else
54
+ throw new RangeError(`${what} must be a number or a decimal string, got ${typeof v}`);
55
+ if (n < 0n || n > max)
56
+ throw new RangeError(`${what} must be 0 to ${max}, got ${n}`);
57
+ return n;
58
+ }
59
+ /** A u64 field (`uintField` up to 2^64 − 1). */
60
+ export const u64Field = (v, what) => uintField(v, U64_LIMIT - 1n, what);
61
+ /**
62
+ * A bytes field as the node's JSON carries it (node/src/types.rs `HexBytes`): lowercase hex, two
63
+ * digits a byte. The node's parser refuses uppercase and odd lengths, so this does too, instead of
64
+ * signing bytes the node can't read back from the JSON.
65
+ */
66
+ function lowerHexBytes(v, what) {
67
+ if (typeof v !== "string")
68
+ throw new TypeError(`${what} must be a string of lowercase hex`);
69
+ if (!/^(?:[0-9a-f]{2})*$/.test(v))
70
+ throw new Error(`${what} must be lowercase hex, two digits a byte`);
71
+ return hexToBytes(v);
72
+ }
73
+ /** An entry of a list field (a program call, a binding, an initializer): a JSON object. */
74
+ function fields(v, what) {
75
+ if (!v || typeof v !== "object" || Array.isArray(v))
76
+ throw new TypeError(`${what} must be an object`);
77
+ return v;
78
+ }
6
79
  class Writer {
7
80
  parts = [];
8
81
  bytes(b) { for (const x of b)
9
82
  this.parts.push(x); }
10
- u8(n) { this.parts.push(n & 0xff); }
11
- u16(n) { this.u8(n); this.u8(n >> 8); }
12
- u64(n) { for (let i = 0n; i < 8n; i++)
13
- this.u8(Number((n >> (8n * i)) & 0xffn)); }
14
- str(s) { const b = utf8.encode(s); this.u16(b.length); this.bytes(b); }
83
+ /** A byte the encoder chose (tags, bools, lengths' bytes): always in range. */
84
+ byte(n) { this.parts.push(n & 0xff); }
85
+ u8(n, what) { this.byte(Number(uintField(n, 0xffn, what))); }
86
+ u16(n, what) { const v = Number(uintField(n, 0xffffn, what)); this.byte(v); this.byte(v >> 8); }
87
+ u32(n, what) { const v = Number(uintField(n, 0xffffffffn, what)); this.byte(v); this.byte(v >> 8); this.byte(v >> 16); this.byte(v >>> 24); }
88
+ u64(n, what) { this.le64(u64Field(n, what)); }
89
+ /** A u64 the tx carries as a JSON number: at most `MAX_JSON_INT`, as the node's format rules require. */
90
+ int(n, what) { this.le64(uintField(n, MAX_JSON_INT, what)); }
91
+ le64(v) { for (let i = 0n; i < 8n; i++)
92
+ this.byte(Number((v >> (8n * i)) & 0xffn)); }
93
+ bool(b, what = "flag") {
94
+ if (typeof b !== "boolean")
95
+ throw new TypeError(`${what} must be true or false`);
96
+ this.byte(b ? 1 : 0);
97
+ }
98
+ str(s, what = "string field") {
99
+ if (typeof s !== "string")
100
+ throw new TypeError(`${what} must be a string`);
101
+ const b = utf8.encode(s);
102
+ if (b.length > 0xffff)
103
+ throw new Error(`${what} too long to encode`);
104
+ this.u16(b.length);
105
+ this.bytes(b);
106
+ }
107
+ ids(xs, what = "id list") {
108
+ if (!Array.isArray(xs))
109
+ throw new TypeError(`${what} must be an array`);
110
+ this.u16(xs.length, `${what} length`);
111
+ for (const x of xs)
112
+ this.int(x, what);
113
+ }
114
+ /** Raw bytes of exactly `len` bytes, from hex. */
115
+ fixed(hex, len) {
116
+ const b = hexToBytes(hex);
117
+ if (b.length !== len)
118
+ throw new Error(`expected ${len} bytes, got ${b.length}`);
119
+ this.bytes(b);
120
+ }
121
+ strs(xs, what = "string list") {
122
+ if (!Array.isArray(xs))
123
+ throw new TypeError(`${what} must be an array`);
124
+ this.u16(xs.length, `${what} length`);
125
+ for (const x of xs)
126
+ this.str(x, what);
127
+ }
128
+ /** Bytes with a u32 length (node/src/types.rs `put_bytes`: a program's code, interface and inputs), from lowercase hex. */
129
+ putBytes(hex, what = "bytes field") {
130
+ const b = lowerHexBytes(hex, what);
131
+ this.u32(b.length, `${what} length`);
132
+ this.bytes(b);
133
+ }
134
+ /** A u8 count, then each entry (a program action's calls and bindings). More than 255 are refused, never wrapped. */
135
+ entries(xs, what, each) {
136
+ if (!Array.isArray(xs))
137
+ throw new TypeError(`${what} must be an array`);
138
+ this.u8(xs.length, `${what} count`);
139
+ for (const x of xs)
140
+ each(fields(x, `each of ${what}`));
141
+ }
15
142
  done() { return Uint8Array.from(this.parts); }
16
143
  }
144
+ /** A chain's genesis hash as its 32 bytes (`/api/status` `genesis_hash`, lowercase hex). */
145
+ export function genesisHashBytes(genesisHash) {
146
+ if (typeof genesisHash !== "string" || !/^[0-9a-f]{64}$/.test(genesisHash)) {
147
+ throw new Error("genesis_hash must be the chain's genesis hash: 32 bytes of lowercase hex (genesis_hash in /api/status)");
148
+ }
149
+ return hexToBytes(genesisHash);
150
+ }
151
+ /** Unsigned ghost txs (`private_transfer`, `unshield`): no key signs them, their proof authorizes them. */
152
+ export function isUnsignedAction(a) {
153
+ return a.type === "private_transfer" || a.type === "unshield";
154
+ }
155
+ /** A program action (tags 62–64 and 69, node/src/types.rs `Action::is_program`). */
156
+ export function isProgramAction(a) {
157
+ return a.type === "upload_code" || a.type === "create_program" || a.type === "call_program" || a.type === "programs_pause";
158
+ }
159
+ /**
160
+ * The bytes a key signs (node/src/types.rs `Tx::signing_bytes`). For an unsigned ghost tx this is
161
+ * its whole wire form instead: `BLOCKULA_GHOST_V1 ‖ chain_id ‖ genesis hash ‖ fee ‖ tip ‖ valid_until ‖ action`.
162
+ */
17
163
  export function signingBytes(tx) {
18
164
  const w = new Writer();
19
- w.bytes(utf8.encode("BLOCKULA_TX_V2"));
165
+ if (isUnsignedAction(tx.action)) {
166
+ w.bytes(utf8.encode(GHOST_TX_DOMAIN));
167
+ w.str(tx.chain_id);
168
+ w.bytes(genesisHashBytes(tx.genesis_hash));
169
+ w.u64(tx.fee, "fee");
170
+ w.u64(orDefault(tx.tip, "0"), "tip");
171
+ w.int(tx.valid_until, "valid_until");
172
+ encodeAction(w, tx.action);
173
+ return w.done();
174
+ }
175
+ w.bytes(utf8.encode("BLOCKULA_TX_V3"));
20
176
  w.str(tx.chain_id);
177
+ w.bytes(genesisHashBytes(tx.genesis_hash));
21
178
  w.str(tx.from);
22
- w.u64(BigInt(tx.nonce));
23
- w.u64(BigInt(tx.fee));
24
- const a = tx.action;
179
+ w.int(tx.nonce, "nonce");
180
+ w.u64(tx.fee, "fee");
181
+ w.u64(orDefault(tx.tip, "0"), "tip");
182
+ w.int(tx.valid_until, "valid_until");
183
+ encodeAction(w, tx.action);
184
+ w.str(orDefault(tx.memo, ""), "memo");
185
+ return w.done();
186
+ }
187
+ /** An action's tag and fields, as inside the signing bytes (node/src/types.rs `Action::encode`). */
188
+ function encodeAction(w, a) {
25
189
  switch (a.type) {
26
190
  case "transfer":
27
191
  w.u8(0);
28
192
  w.str(a.to);
29
- w.u64(BigInt(a.amount));
193
+ w.u64(a.amount);
30
194
  break;
31
195
  case "register_name":
32
196
  w.u8(1);
@@ -40,10 +204,12 @@ export function signingBytes(tx) {
40
204
  w.u8(0);
41
205
  else {
42
206
  w.u8(1);
43
- w.u64(BigInt(a.role.limit));
44
- w.u64(BigInt(a.role.period_blocks));
45
- w.u64(BigInt(a.role.expires_at));
207
+ w.u64(a.role.limit);
208
+ w.int(a.role.period_blocks, "period_blocks");
209
+ w.int(a.role.expires_at, "expires_at");
46
210
  }
211
+ w.str(a.public_key);
212
+ w.str(a.pop);
47
213
  break;
48
214
  case "remove_key":
49
215
  w.u8(3);
@@ -54,7 +220,7 @@ export function signingBytes(tx) {
54
220
  w.str(a.consensus_key);
55
221
  w.str(a.p2p_addr);
56
222
  w.str(a.http_addr);
57
- w.u64(BigInt(a.bond));
223
+ w.u64(a.bond);
58
224
  w.str(a.pop);
59
225
  break;
60
226
  case "approve_validator":
@@ -81,34 +247,661 @@ export function signingBytes(tx) {
81
247
  case "create_hunt":
82
248
  w.u8(10);
83
249
  w.str(a.repo);
84
- w.u64(BigInt(a.issue));
85
- w.u64(BigInt(a.amount));
86
- w.u64(BigInt(a.expires_at));
250
+ w.int(a.issue, "issue");
251
+ w.u64(a.amount);
252
+ w.int(a.expires_at, "expires_at");
87
253
  w.str(a.title);
88
254
  break;
89
255
  case "fund_hunt":
90
256
  w.u8(11);
91
- w.u64(BigInt(a.hunt_id));
92
- w.u64(BigInt(a.amount));
257
+ w.int(a.hunt_id, "hunt_id");
258
+ w.u64(a.amount);
93
259
  break;
94
260
  case "award_hunt":
95
261
  w.u8(12);
96
- w.u64(BigInt(a.hunt_id));
262
+ w.int(a.hunt_id, "hunt_id");
97
263
  w.str(a.to);
98
- w.u64(BigInt(a.pr));
264
+ w.int(a.pr, "pr");
99
265
  w.str(a.familiar_key ?? "");
100
266
  break;
101
267
  case "cancel_hunt":
102
268
  w.u8(13);
103
- w.u64(BigInt(a.hunt_id));
269
+ w.int(a.hunt_id, "hunt_id");
104
270
  break;
105
271
  case "register_familiar":
106
272
  w.u8(14);
107
273
  w.str(a.key_id);
108
274
  w.str(a.label);
109
275
  break;
276
+ case "set_repo":
277
+ w.u8(15);
278
+ w.str(a.repo);
279
+ w.int(a.weight, "weight");
280
+ break;
281
+ case "grant_soul":
282
+ w.u8(16);
283
+ w.str(a.to);
284
+ w.int(a.amount, "amount");
285
+ w.str(a.reason);
286
+ break;
287
+ case "vouch":
288
+ w.u8(17);
289
+ w.str(a.account);
290
+ break;
291
+ case "unvouch":
292
+ w.u8(18);
293
+ w.str(a.account);
294
+ break;
295
+ case "flag_sybil":
296
+ w.u8(19);
297
+ w.str(a.account);
298
+ w.str(a.reason);
299
+ break;
300
+ case "submit_claim":
301
+ w.u8(20);
302
+ w.str(a.repo);
303
+ w.int(a.pr, "pr");
304
+ w.str(a.familiar_key ?? "");
305
+ break;
306
+ case "attest_claim":
307
+ w.u8(21);
308
+ w.int(a.claim_id, "claim_id");
309
+ w.bool(a.ok, "ok");
310
+ w.str(a.merge_sha);
311
+ w.str(orDefault(a.note, ""));
312
+ break;
313
+ case "reject_claim":
314
+ w.u8(22);
315
+ w.int(a.claim_id, "claim_id");
316
+ w.str(a.reason);
317
+ break;
318
+ case "join_jury":
319
+ w.u8(23);
320
+ w.u64(a.bond);
321
+ break;
322
+ case "leave_jury":
323
+ w.u8(24);
324
+ break;
325
+ case "commit_votes":
326
+ w.u8(25);
327
+ w.int(a.seance_id, "seance_id");
328
+ w.str(a.commitment);
329
+ w.str(a.pairs_digest);
330
+ break;
331
+ case "reveal_votes":
332
+ w.u8(26);
333
+ w.int(a.seance_id, "seance_id");
334
+ w.str(a.votes);
335
+ w.ids(a.spam, "spam");
336
+ w.str(a.salt);
337
+ break;
338
+ case "fund_pool":
339
+ w.u8(27);
340
+ w.u64(a.amount);
341
+ break;
342
+ case "withdraw_vested":
343
+ w.u8(28);
344
+ break;
345
+ case "clawback":
346
+ w.u8(29);
347
+ w.str(a.account);
348
+ w.str(a.reason);
349
+ break;
350
+ case "delegate":
351
+ w.u8(30);
352
+ w.str(a.validator);
353
+ w.u64(a.amount);
354
+ break;
355
+ case "undelegate":
356
+ w.u8(31);
357
+ w.str(a.validator);
358
+ w.u64(a.shares);
359
+ w.u64(orDefault(a.amount, "0"));
360
+ break;
361
+ case "redelegate":
362
+ w.u8(32);
363
+ w.str(a.from_validator);
364
+ w.str(a.to_validator);
365
+ w.u64(a.shares);
366
+ break;
367
+ case "withdraw_unbonded":
368
+ w.u8(33);
369
+ break;
370
+ case "update_validator":
371
+ w.u8(34);
372
+ w.int(a.commission_bps, "commission_bps");
373
+ w.str(a.p2p_addr);
374
+ w.str(a.http_addr);
375
+ break;
376
+ case "unjail":
377
+ w.u8(35);
378
+ w.str(a.validator);
379
+ break;
380
+ case "report_equivocation":
381
+ w.u8(36);
382
+ w.str(a.evidence);
383
+ break;
384
+ case "beacon_commit":
385
+ w.u8(37);
386
+ w.int(a.epoch, "epoch");
387
+ w.str(a.commitment);
388
+ break;
389
+ case "beacon_reveal":
390
+ w.u8(38);
391
+ w.int(a.epoch, "epoch");
392
+ w.str(a.secret);
393
+ break;
394
+ case "propose":
395
+ w.u8(39);
396
+ encodeProposalKind(w, a.kind);
397
+ w.u64(a.deposit);
398
+ w.str(a.title);
399
+ w.str(orDefault(a.url, ""));
400
+ break;
401
+ case "vote":
402
+ w.u8(40);
403
+ w.int(a.proposal_id, "proposal_id");
404
+ w.bool(a.aye, "aye");
405
+ w.u64(a.amount);
406
+ w.u8(a.conviction, "conviction");
407
+ w.str(a.proposal_digest);
408
+ break;
409
+ case "unlock":
410
+ w.u8(41);
411
+ break;
412
+ case "multisig_create":
413
+ w.u8(42);
414
+ w.strs(a.owners);
415
+ w.u8(a.threshold, "threshold");
416
+ break;
417
+ case "multisig_propose":
418
+ if (a.action.type === "multisig_propose")
419
+ throw new Error("a multisig proposal can't contain another");
420
+ // The node won't decode one (docs/PROGRAMS-V0.md §2.1, "No nesting").
421
+ if (isProgramAction(a.action))
422
+ throw new Error("a multisig proposal can't hold a program action");
423
+ w.u8(43);
424
+ encodeAction(w, a.action);
425
+ break;
426
+ case "multisig_approve":
427
+ w.u8(44);
428
+ w.int(a.id, "id");
429
+ w.str(a.action_digest);
430
+ break;
431
+ case "renew_name":
432
+ w.u8(45);
433
+ w.str(a.name);
434
+ w.u8(a.years, "years");
435
+ break;
436
+ case "transfer_name":
437
+ w.u8(46);
438
+ w.str(a.name);
439
+ w.str(a.to);
440
+ break;
441
+ case "release_name":
442
+ w.u8(47);
443
+ w.str(a.name);
444
+ break;
445
+ case "claim_distribution":
446
+ w.u8(48);
447
+ w.int(a.distribution_id, "distribution_id");
448
+ w.u64(a.amount);
449
+ w.strs(a.proof);
450
+ break;
451
+ case "attest_revert":
452
+ w.u8(49);
453
+ w.int(a.claim_id, "claim_id");
454
+ w.str(a.revert_sha);
455
+ break;
456
+ case "shield":
457
+ w.u8(50);
458
+ w.u64(a.amount);
459
+ encodeBundle(w, a.bundle);
460
+ break;
461
+ case "private_transfer":
462
+ w.u8(51);
463
+ encodeBundle(w, a.bundle);
464
+ break;
465
+ case "unshield":
466
+ w.u8(52);
467
+ w.str(a.to);
468
+ w.u64(a.amount);
469
+ encodeBundle(w, a.bundle);
470
+ break;
471
+ case "set_ghost_address":
472
+ w.u8(53);
473
+ w.fixed(a.address, GHOST_ADDRESS_BYTES);
474
+ break;
475
+ case "ghost_pause":
476
+ w.u8(54);
477
+ break;
478
+ case "validator_ready":
479
+ w.u8(60);
480
+ w.int(a.epoch, "epoch");
481
+ break;
482
+ case "admit_validator":
483
+ w.u8(61);
484
+ w.str(a.validator);
485
+ w.int(a.epoch, "epoch");
486
+ break;
487
+ // Programs (docs/PROGRAMS-V0.md §2.2–2.5). Counts are a u8; byte fields a u32 length (`putBytes`).
488
+ case "upload_code":
489
+ w.u8(62);
490
+ w.putBytes(a.wasm, "wasm");
491
+ w.putBytes(a.interface, "interface");
492
+ w.u64(a.storage_limit, "storage_limit");
493
+ break;
494
+ case "create_program": {
495
+ w.u8(63);
496
+ w.str(a.wasm_hash, "wasm_hash");
497
+ w.str(a.interface_hash, "interface_hash");
498
+ w.str(authorityField(a.authority), "authority");
499
+ w.int(a.upgrade_delay_blocks, "upgrade_delay_blocks");
500
+ w.u8(policyByte(a.policy));
501
+ w.bool(a.accepts_transfers, "accepts_transfers");
502
+ // `init`: a presence byte, then the method, its input and value (absent or null: none, as the node reads the JSON).
503
+ if (a.init === undefined || a.init === null)
504
+ w.u8(0);
505
+ else {
506
+ const init = fields(a.init, "init");
507
+ w.u8(1);
508
+ w.str(init.method, "init.method");
509
+ w.putBytes(init.input, "init.input");
510
+ w.u64(init.value, "init.value");
511
+ }
512
+ w.int(a.fuel_limit, "fuel_limit");
513
+ w.u64(a.storage_limit, "storage_limit");
514
+ encodeBindings(w, a.bindings);
515
+ break;
516
+ }
517
+ case "call_program":
518
+ w.u8(64);
519
+ w.entries(a.calls, "calls", (c) => { w.str(c.program, "calls.program"); w.str(c.version_digest, "calls.version_digest"); w.str(c.method, "calls.method"); w.putBytes(c.input, "calls.input"); w.u64(c.value, "calls.value"); });
520
+ w.int(a.fuel_limit, "fuel_limit");
521
+ w.u64(a.storage_limit, "storage_limit");
522
+ encodeBindings(w, a.bindings);
523
+ break;
524
+ case "programs_pause":
525
+ w.u8(69);
526
+ break;
527
+ default:
528
+ throw new Error(`unknown action type ${a.type}`);
529
+ }
530
+ }
531
+ /** A program action's bindings (node/src/types.rs `put_bindings`): a u8 count, then each program and version digest. */
532
+ function encodeBindings(w, bindings) {
533
+ w.entries(bindings, "bindings", (b) => { w.str(b.program, "bindings.program"); w.str(b.version_digest, "bindings.version_digest"); });
534
+ }
535
+ /**
536
+ * A create's `authority` as the codec writes it (an optional string: `""` for none). The node
537
+ * reads JSON `""` as an authority that isn't an address, and refuses it, but its bytes would be
538
+ * an immutable program's: so `""` is refused here, never signed as the other.
539
+ */
540
+ function authorityField(authority) {
541
+ if (authority === undefined || authority === null)
542
+ return "";
543
+ if (authority === "")
544
+ throw new Error('authority must be an address, or null for an immutable program (not "")');
545
+ if (typeof authority !== "string")
546
+ throw new TypeError("authority must be a string (an address) or null");
547
+ return authority;
548
+ }
549
+ /** A program policy's byte (node/src/types.rs `ProgramPolicy::byte`). */
550
+ function policyByte(policy) {
551
+ if (policy === "open")
552
+ return 0;
553
+ if (policy === "additive")
554
+ return 1;
555
+ throw new Error(`policy must be "open" or "additive", not ${typeof policy === "string" ? JSON.stringify(policy) : typeof policy}`);
556
+ }
557
+ /** A bundle without its proof, raw and in wire order: what the sighash covers. */
558
+ function encodeBundlePublic(w, b) {
559
+ w.fixed(b.instance, 32);
560
+ w.fixed(b.anchor, 32);
561
+ for (const x of [...b.nullifiers, ...b.commitments])
562
+ w.fixed(x, 32);
563
+ for (const x of b.enc)
564
+ w.fixed(x, GHOST_ENC_BYTES);
565
+ for (const x of b.out)
566
+ w.fixed(x, GHOST_OUT_BYTES);
567
+ }
568
+ /** node/src/types.rs `GhostBundle::encode`: the fixed fields, then the proof with a u32 length. */
569
+ function encodeBundle(w, b) {
570
+ encodeBundlePublic(w, b);
571
+ const proof = hexToBytes(b.proof);
572
+ w.u32(proof.length);
573
+ w.bytes(proof);
574
+ }
575
+ /**
576
+ * What a ghost tx's Action proof is bound to (node/src/types.rs `Tx::ghost_sighash`):
577
+ * BLAKE3-KDF("blockula ghost v1 tx sighash", chain_id ‖ genesis hash ‖ tag ‖ valid_until ‖ fee ‖
578
+ * tip ‖ the tag's own fields (shield: from, nonce, amount; unshield: to, amount) ‖ the bundle
579
+ * without its proof ‖ circuit id). A wallet proves with this as the witness's sighash, then fills
580
+ * in the proof.
581
+ */
582
+ export function ghostSighash(tx) {
583
+ const a = tx.action;
584
+ if (a.type !== "shield" && a.type !== "private_transfer" && a.type !== "unshield")
585
+ throw new Error("not a ghost transfer");
586
+ const w = new Writer();
587
+ w.str(tx.chain_id);
588
+ w.bytes(genesisHashBytes(tx.genesis_hash));
589
+ w.u8(a.type === "shield" ? 50 : a.type === "private_transfer" ? 51 : 52);
590
+ w.int(tx.valid_until, "valid_until");
591
+ w.u64(tx.fee, "fee");
592
+ w.u64(orDefault(tx.tip, "0"), "tip");
593
+ if (a.type === "shield") {
594
+ w.str(tx.from);
595
+ w.int(tx.nonce, "nonce");
596
+ w.u64(a.amount);
597
+ }
598
+ if (a.type === "unshield") {
599
+ w.str(a.to);
600
+ w.u64(a.amount);
601
+ }
602
+ encodeBundlePublic(w, a.bundle);
603
+ w.u32(GHOST_CIRCUIT_ID);
604
+ return blake3(w.done(), { context: utf8.encode("blockula ghost v1 tx sighash") });
605
+ }
606
+ /** A proposal kind's own tag and fields (node/src/types.rs `ProposalKind::encode`). */
607
+ function encodeProposalKind(w, k) {
608
+ switch (k.type) {
609
+ case "param_change":
610
+ w.u8(0);
611
+ w.str(k.key);
612
+ w.u64(k.value);
613
+ break;
614
+ case "treasury_spend":
615
+ w.u8(1);
616
+ w.str(k.to);
617
+ w.u64(k.amount);
618
+ break;
619
+ case "distribution":
620
+ w.u8(2);
621
+ w.int(k.distribution_id, "distribution_id");
622
+ w.str(k.merkle_root);
623
+ w.u64(k.total);
624
+ w.int(k.claim_until, "claim_until");
625
+ break;
626
+ case "set_repo":
627
+ w.u8(3);
628
+ w.str(k.repo);
629
+ w.int(k.weight, "weight");
630
+ break;
631
+ case "grant_soul":
632
+ w.u8(4);
633
+ w.str(k.to);
634
+ w.int(k.amount, "amount");
635
+ w.str(k.reason);
636
+ break;
637
+ case "flag_sybil":
638
+ w.u8(5);
639
+ w.str(k.account);
640
+ w.str(k.reason);
641
+ break;
642
+ case "reject_claim":
643
+ w.u8(6);
644
+ w.int(k.claim_id, "claim_id");
645
+ w.str(k.reason);
646
+ break;
647
+ case "clawback":
648
+ w.u8(7);
649
+ w.str(k.account);
650
+ w.str(k.reason);
651
+ break;
652
+ case "unlink_github":
653
+ w.u8(8);
654
+ w.str(k.handle);
655
+ break;
656
+ case "refund_hunt":
657
+ w.u8(9);
658
+ w.int(k.hunt_id, "hunt_id");
659
+ break;
660
+ case "schedule_upgrade":
661
+ w.u8(10);
662
+ w.u32(k.version, "version");
663
+ w.int(k.activation_height, "activation_height");
664
+ break;
665
+ case "activate_feature":
666
+ w.u8(11);
667
+ w.str(k.feature);
668
+ break;
669
+ case "deactivate_feature":
670
+ w.u8(12);
671
+ w.str(k.feature);
672
+ break;
673
+ case "text":
674
+ w.u8(13);
675
+ break;
676
+ case "unflag_sybil":
677
+ w.u8(14);
678
+ w.str(k.account);
679
+ w.str(k.reason);
680
+ break;
681
+ case "cancel_upgrade":
682
+ w.u8(15);
683
+ w.u32(k.version, "version");
684
+ w.int(k.activation_height, "activation_height");
685
+ break;
686
+ default:
687
+ throw new Error(`unknown proposal kind ${k.type}`);
110
688
  }
111
- w.str(tx.memo);
689
+ }
690
+ /**
691
+ * What a Séance juror seals before revealing (node/src/types.rs `seance_commitment`): hex
692
+ * BLAKE3 of the Séance id, the juror's account, the votes, spam flags and a secret salt.
693
+ */
694
+ export function seanceCommitment(seanceId, juror, votes, spam, salt) {
695
+ const w = new Writer();
696
+ w.bytes(utf8.encode("BLOCKULA_SEANCE_V1"));
697
+ w.int(seanceId, "seance_id");
698
+ w.str(juror);
699
+ w.str(votes);
700
+ w.ids(spam, "spam");
701
+ w.str(salt);
702
+ return bytesToHex(blake3(w.done()));
703
+ }
704
+ /**
705
+ * A validator's randomness-beacon commitment (node/src/beacon.rs `commitment`): hex BLAKE3 in
706
+ * derive-key mode ("blockula beacon commitment v1") over the chain id, the target epoch, the
707
+ * validator's address and its 32-byte hex secret. Strings are u64-length-prefixed, integers
708
+ * u64 little-endian.
709
+ */
710
+ export function beaconCommitment(chainId, epoch, validator, secret) {
711
+ const u64 = (n) => {
712
+ const b = new Uint8Array(8);
713
+ // setBigUint64 would wrap a negative or oversized value: check it first.
714
+ new DataView(b.buffer).setBigUint64(0, u64Field(n, "epoch"), true);
715
+ return b;
716
+ };
717
+ const str = (s) => {
718
+ const b = utf8.encode(s);
719
+ return concatBytes(u64(b.length), b);
720
+ };
721
+ const key = hexToBytes(secret);
722
+ if (key.length !== 32)
723
+ throw new Error("a beacon secret is 32 bytes");
724
+ return bytesToHex(blake3(concatBytes(str(chainId), u64(epoch), str(validator), key), { context: utf8.encode("blockula beacon commitment v1") }));
725
+ }
726
+ /** "2.5" Soul → 2500 milli-Soul. */
727
+ export function parseSoul(soul) {
728
+ const s = String(soul).trim();
729
+ if (!/^\d+(\.\d{1,3})?$/.test(s))
730
+ throw new Error(`invalid Soul amount: ${s}`);
731
+ const [whole, frac = ""] = s.split(".");
732
+ return Number(whole) * 1000 + Number(frac.padEnd(3, "0"));
733
+ }
734
+ /**
735
+ * What a `multisig_approve` binds (node/src/types.rs `action_digest`): BLAKE3 (derive-key mode,
736
+ * context "blockula multisig action v1") of the action's encoding, as inside the signing bytes.
737
+ * Compute it from the action you show the approver (`client.multisig(account)` `pending[].action`).
738
+ */
739
+ export function actionDigest(action) {
740
+ return digestOf("multisig action", "blockula multisig action v1", (w) => encodeAction(w, action));
741
+ }
742
+ /**
743
+ * What a `vote` binds (node/src/types.rs `proposal_digest`): the proposal's kind (accounts as
744
+ * stored, resolved), title and link. Compute it from the proposal you show the voter.
745
+ */
746
+ export function proposalDigest(kind, title, url = "") {
747
+ return digestOf("proposal", "blockula proposal v1", (w) => {
748
+ encodeProposalKind(w, kind);
749
+ w.str(title);
750
+ w.str(url);
751
+ });
752
+ }
753
+ /**
754
+ * BLAKE3 (derive-key mode, `context`) of what `encode` writes. A field out of range (a JSON
755
+ * number past 2^53 − 1 arrives rounded, and can't be the value the chain stored) fails with
756
+ * what's wrong and where it likely came from, not a bare encoder error (review-r2-clients-1).
757
+ */
758
+ function digestOf(what, context, encode) {
759
+ const w = new Writer();
760
+ try {
761
+ encode(w);
762
+ }
763
+ catch (e) {
764
+ if (e instanceof RangeError)
765
+ throw new RangeError(`can't compute this ${what}'s digest: ${e.message}. The chain stores no ${what} with a whole number past 2^53 − 1 (the most a JSON number carries exactly), so the node it was read from may be misreporting it: read it from another node`);
766
+ throw e;
767
+ }
768
+ return bytesToHex(blake3(w.done(), { context: utf8.encode(context) }));
769
+ }
770
+ // ---- Programs' digests (node/src/programs/digest.rs, docs/PROGRAMS-V0.md §2.1), checked offline
771
+ // against test/program-vectors.json, which the node writes. Frozen with protocol v2. ----
772
+ /**
773
+ * The contexts of programs' five digests, each BLAKE3 in derive-key mode: node/src/programs/digest.rs
774
+ * `CODE_CONTEXT`, `INTERFACE_CONTEXT`, `VERSION_CONTEXT` and `ADDRESS_CONTEXT`, and the VM's
775
+ * `SELECTOR_CONTEXT` (node/vm/src/interface.rs), the selector's only home on the node.
776
+ */
777
+ export const PROGRAM_DIGEST_CONTEXTS = Object.freeze({
778
+ code: "blockula program code v1",
779
+ interface: "blockula program interface v1",
780
+ version: "blockula program version v1",
781
+ address: "blockula program address v1",
782
+ method: "blockula program method v1",
783
+ });
784
+ /** Every v0 program's version (node/src/programs/digest.rs `FIRST_VERSION`): only a program upgrade, in a later protocol version, raises it. */
785
+ export const FIRST_PROGRAM_VERSION = 1;
786
+ /** Address prefixes (index.ts `KNOWN_PREFIXES`, node/src/crypto.rs `KNOWN_PREFIXES`), kept here so this module imports nothing of the SDK's. */
787
+ const ADDRESS_PREFIXES = ["boo", "tboo"];
788
+ /**
789
+ * A canonical address's prefix and 32 bytes (node/src/crypto.rs `parse_address`): a known prefix,
790
+ * and only the exact rendering, so neither uppercase nor another checksum aliases the account.
791
+ * Programs are named by address, never by name.
792
+ */
793
+ function parseAddress(s, what) {
794
+ if (typeof s === "string" && s.length <= 90) {
795
+ try {
796
+ const { prefix, words } = bech32m.decode(s);
797
+ const bytes = bech32m.fromWords(words);
798
+ if (ADDRESS_PREFIXES.includes(prefix) && bytes.length === 32 && bech32m.encode(prefix, words) === s)
799
+ return { prefix, bytes };
800
+ }
801
+ catch {
802
+ // Not bech32m: refused below.
803
+ }
804
+ }
805
+ throw new Error(`${what} must be an address (boo1… or tboo1…), not ${typeof s === "string" ? JSON.stringify(s) : typeof s}`);
806
+ }
807
+ /** A 32-byte digest as a program action's JSON carries it: 64 lowercase hex (node/src/types.rs `check_hex_32`). */
808
+ function hex32(v, what) {
809
+ if (typeof v !== "string" || !/^[0-9a-f]{64}$/.test(v))
810
+ throw new Error(`${what} must be 32 bytes of lowercase hex`);
811
+ return hexToBytes(v);
812
+ }
813
+ /** BLAKE3 in derive-key mode (the node's `blake3::derive_key`). */
814
+ const deriveKey = (context, input) => blake3(input, { context: utf8.encode(context) });
815
+ /** Bytes, or lowercase hex as `upload_code` carries them. */
816
+ const bytesOf = (v, what) => (v instanceof Uint8Array ? v : lowerHexBytes(v, what));
817
+ /**
818
+ * The hash that names a program's code (node/src/programs/digest.rs `wasm_hash`): BLAKE3-KDF("blockula
819
+ * program code v1", wasm), as 64 lowercase hex. A `create_program` names its code by it, once an
820
+ * `upload_code` of the same bytes (as bytes, or lowercase hex) has applied.
821
+ */
822
+ export function wasmHash(wasm) {
823
+ return bytesToHex(deriveKey(PROGRAM_DIGEST_CONTEXTS.code, bytesOf(wasm, "wasm")));
824
+ }
825
+ /** The hash that names an interface, its format-1 bytes (node/src/programs/digest.rs `interface_hash`): as `wasmHash`, under "blockula program interface v1". */
826
+ export function interfaceHash(iface) {
827
+ return bytesToHex(deriveKey(PROGRAM_DIGEST_CONTEXTS.interface, bytesOf(iface, "interface")));
828
+ }
829
+ /**
830
+ * What a call to `program` binds (node/src/programs/digest.rs `version_digest`): BLAKE3-KDF("blockula
831
+ * program version v1", the program's address as a string field (u16 LE length, its bytes) ‖ its
832
+ * code's hash ‖ its interface's hash ‖ its version, u32 LE), as 64 lowercase hex. A call runs only
833
+ * while this is the program's digest in force, so it runs the code its signer was shown: compute it
834
+ * from the record you show (its hashes and version), rather than sign a digest a node reported.
835
+ */
836
+ export function versionDigest(program, wasmHashHex, interfaceHashHex, version = FIRST_PROGRAM_VERSION) {
837
+ parseAddress(program, "program");
838
+ const w = new Writer();
839
+ w.str(program, "program");
840
+ w.bytes(hex32(wasmHashHex, "wasm_hash"));
841
+ w.bytes(hex32(interfaceHashHex, "interface_hash"));
842
+ w.u32(version, "version");
843
+ return bytesToHex(deriveKey(PROGRAM_DIGEST_CONTEXTS.version, w.done()));
844
+ }
845
+ /**
846
+ * The address of the program `creator` creates with its transaction of `nonce` (node/src/programs/digest.rs
847
+ * `program_address`): bech32m(prefix, BLAKE3-KDF("blockula program address v1", the creator's 32
848
+ * address bytes ‖ nonce u64 LE)). `prefix` defaults to the creator's own (a chain admits only its
849
+ * prefix, so for any create it runs the two are the same).
850
+ *
851
+ * Don't send FANG to it before the create has applied: a create that fails spends its nonce, no
852
+ * later create reaches the address, and what was sent there is stranded for good (docs/PROGRAMS-V0.md
853
+ * §2.3). Fund a program through its initializer's value, or by transfer once it exists.
854
+ */
855
+ export function programAddress(creator, nonce, prefix) {
856
+ const c = parseAddress(creator, "creator");
857
+ const p = prefix ?? c.prefix;
858
+ if (!ADDRESS_PREFIXES.includes(p))
859
+ throw new Error(`unknown address prefix ${p}`);
860
+ const w = new Writer();
861
+ w.bytes(c.bytes);
862
+ w.u64(nonce, "nonce");
863
+ return bech32m.encode(p, bech32m.toWords(deriveKey(PROGRAM_DIGEST_CONTEXTS.address, w.done())));
864
+ }
865
+ /** A method name: an identifier, `[a-z][a-z0-9_]{0,31}` (node/vm `abi::is_ident`). */
866
+ const METHOD_NAME = /^[a-z][a-z0-9_]{0,31}$/;
867
+ /**
868
+ * A method's selector (node/vm/src/interface.rs `selector`): the first 8 bytes of
869
+ * BLAKE3-KDF("blockula program method v1", its name), as 16 lowercase hex. An interface's methods
870
+ * have distinct selectors. Only identifiers are method names, so anything else is refused.
871
+ */
872
+ export function methodSelector(name) {
873
+ if (typeof name !== "string" || !METHOD_NAME.test(name))
874
+ throw new Error(`a method name is an identifier ([a-z][a-z0-9_]{0,31}), not ${typeof name === "string" ? JSON.stringify(name) : typeof name}`);
875
+ return bytesToHex(deriveKey(PROGRAM_DIGEST_CONTEXTS.method, utf8.encode(name)).subarray(0, 8));
876
+ }
877
+ /**
878
+ * What a `commit_votes` binds (node/src/types.rs `seance_pairs_digest`): the Séance, the juror and,
879
+ * per assigned pair in order, both claims' ids, repos and PR numbers.
880
+ */
881
+ export function seancePairsDigest(seanceId, juror, pairs) {
882
+ const w = new Writer();
883
+ w.int(seanceId, "seance_id");
884
+ w.str(juror);
885
+ w.u16(pairs.length);
886
+ for (const pair of pairs)
887
+ for (const c of pair) {
888
+ w.int(c.id, "claim id");
889
+ w.str(c.repo);
890
+ w.int(c.pr, "pr");
891
+ }
892
+ return bytesToHex(blake3(w.done(), { context: utf8.encode("blockula seance pairs v1") }));
893
+ }
894
+ /** FIPS 204 context string for a key's proof of possession in `add_key` (node/src/types.rs `KEY_POP_CONTEXT`). */
895
+ export const KEY_POP_CONTEXT = utf8.encode("blockula/key-pop/v1");
896
+ /**
897
+ * What a key signs to consent to being added to `account` (node/src/types.rs `key_pop_message`):
898
+ * the chain instance, the account and the key's own id (audit gap-7-2).
899
+ */
900
+ export function keyPopMessage(chainId, genesisHash, account, keyId) {
901
+ const w = new Writer();
902
+ w.bytes(utf8.encode("BLOCKULA_KEY_POP_V1"));
903
+ for (const s of [chainId, genesisHash, account, keyId])
904
+ w.str(s);
112
905
  return w.done();
113
906
  }
114
907
  /** FIPS 204 context string for validator proofs of possession (node/src/types.rs `POP_CONTEXT`). */
@@ -121,10 +914,57 @@ export function popMessage(chainId, owner, p2pAddr, httpAddr) {
121
914
  w.str(s);
122
915
  return w.done();
123
916
  }
124
- /** Mirrors `min_fee` in node/src/types.rs: base fee is priced per byte and burned. */
917
+ /** Wire size in bytes (node/src/types.rs `Tx::size`): signing bytes plus the key and signature (none for an unsigned ghost tx). Fees are priced on it. */
918
+ export function txSize(tx) {
919
+ return signingBytes(tx).length + (isUnsignedAction(tx.action) ? 0 : PUBLIC_KEY_LEN + SIGNATURE_LEN);
920
+ }
921
+ /** The base fee at its genesis floor: 10,000 drops + 10 per byte (`min_fee`). */
922
+ export const FLOOR_BASE_FEE = { flat: 10000n, rate: 10000n };
923
+ /** Mirrors `FeeSlot::fee` in node/src/econ.rs: `flat + rate × size / 1000`, rounded down. Burned. */
924
+ export function baseFee(tx, fee = FLOOR_BASE_FEE) {
925
+ return BigInt(fee.flat) + (BigInt(fee.rate) * BigInt(txSize(tx))) / 1000n;
926
+ }
927
+ /** Mirrors `min_fee` in node/src/types.rs: the base fee at its floor (an idle chain's). */
125
928
  export function minFee(tx) {
126
- const size = signingBytes(tx).length + PUBLIC_KEY_LEN + SIGNATURE_LEN;
127
- return 10000n + BigInt(size) * 10n;
929
+ return baseFee(tx, FLOOR_BASE_FEE);
930
+ }
931
+ /** The chain's default `tip_cap_multiple` (node/src/params.rs): a tip may be at most this many times the base fee of the block that includes it. */
932
+ export const DEFAULT_TIP_CAP_MULTIPLE = 20;
933
+ /**
934
+ * The most this tx may tip in a block with this base fee (node/src/econ.rs `max_tip`): `multiple` × its base fee. Nodes refuse
935
+ * a bigger tip. `/api/fees` serves the chain's `tip_cap_multiple` and `lowest`, the base fee a tip fits under in any upcoming block.
936
+ * Tips are also paid only from transferable FANG (locks hold them), and a multisig's owner keys or a validator's consensus key
937
+ * acting for its owner can't tip at all.
938
+ */
939
+ export function maxTip(tx, fee = FLOOR_BASE_FEE, multiple = DEFAULT_TIP_CAP_MULTIPLE) {
940
+ return BigInt(multiple) * baseFee(tx, fee);
941
+ }
942
+ /** What the tx is charged in a block with this base fee (node/src/econ.rs `charge`): base fee + tip. Its `fee` must cover it; nothing more is debited. */
943
+ export function txCharge(tx, fee = FLOOR_BASE_FEE) {
944
+ return baseFee(tx, fee) + u64Field(orDefault(tx.tip, "0"), "tip");
945
+ }
946
+ /**
947
+ * Wire form (node/src/types.rs `Tx::to_bytes`): signing bytes ‖ public key (1,952 B) ‖ signature (3,309 B), or just the
948
+ * unsigned form for an unsigned ghost tx. Blocks and gossip carry this; `POST /api/tx` takes it as `application/octet-stream`.
949
+ */
950
+ export function txBytes(tx) {
951
+ const sb = signingBytes(tx);
952
+ if (isUnsignedAction(tx.action))
953
+ return sb;
954
+ const out = new Uint8Array(sb.length + PUBLIC_KEY_LEN + SIGNATURE_LEN);
955
+ out.set(sb);
956
+ out.set(hexToBytes(tx.public_key), sb.length);
957
+ out.set(hexToBytes(tx.signature), sb.length + PUBLIC_KEY_LEN);
958
+ return out;
959
+ }
960
+ /** A tx's id (node/src/types.rs `Tx::id`): hex BLAKE3 of the signing bytes and the signature. Known before submitting. */
961
+ export function txId(tx) {
962
+ const sb = signingBytes(tx);
963
+ const sig = hexToBytes(tx.signature);
964
+ const buf = new Uint8Array(sb.length + sig.length);
965
+ buf.set(sb);
966
+ buf.set(sig, sb.length);
967
+ return bytesToHex(blake3(buf));
128
968
  }
129
969
  /** "1.5" → 1500000000n drops. Accepts numbers, decimal strings or bigint drops. */
130
970
  export function parseFang(amount) {