@blockula/sdk 0.5.1 → 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/proof.d.ts CHANGED
@@ -21,6 +21,12 @@ export declare const NAMESPACES: {
21
21
  readonly GRANTS: 25;
22
22
  readonly RESERVED_NAMES: 43;
23
23
  readonly FEATURES: 47;
24
+ /** A program's record, keyed by its address as a string (as accounts are). */
25
+ readonly PROGRAMS: 66;
26
+ readonly PROGRAM_CODE: 67;
27
+ readonly PROGRAM_INTERFACES: 68;
28
+ readonly PROGRAM_STORAGE: 69;
29
+ readonly PROGRAM_BLOB_REFS: 72;
24
30
  };
25
31
  /** Hash of an empty subtree. */
26
32
  export declare const EMPTY_HASH: Uint8Array<ArrayBuffer>;
@@ -124,3 +130,41 @@ export interface NameRecord {
124
130
  grace_blocks?: number;
125
131
  }
126
132
  export declare function decodeName(value: Uint8Array): NameRecord;
133
+ /**
134
+ * A program's record as the state holds it (node/src/programs/state.rs `ProgramRecord`, format 1),
135
+ * decoded from the value `/api/proof/66/{address}` proves. `version_digest` isn't in the record:
136
+ * `Client.provenProgram` computes it from the record's own fields (`versionDigest`).
137
+ */
138
+ export interface ProgramRecord {
139
+ /** The account whose transaction created it. */
140
+ creator: string;
141
+ created_at: number;
142
+ /** Lowercase hex, 64 characters. */
143
+ wasm_hash: string;
144
+ interface_hash: string;
145
+ version: number;
146
+ /** Stored as created, with no power until program upgrades exist. */
147
+ authority: string | null;
148
+ upgrade_delay_blocks: number;
149
+ /** 0 open, 1 additive. */
150
+ policy: "open" | "additive";
151
+ accepts_transfers: boolean;
152
+ /** Its storage entries: how many, Σ their accounted bytes, Σ their deposits (drops, as decimal strings). */
153
+ storage: {
154
+ items: number;
155
+ bytes: number;
156
+ deposit: string;
157
+ };
158
+ /** The part of those deposits the program paid itself. */
159
+ sponsored: string;
160
+ /** What its creator bonded for the record itself, and who. */
161
+ record_deposit: string;
162
+ record_payer: string;
163
+ }
164
+ /**
165
+ * Decodes a program record: the format byte (1: the only format this SDK reads, as the node's
166
+ * `Versioned` does) and the borsh body, field by field in the order the node writes them. Refuses
167
+ * a format it doesn't know, a bool or an option tag that isn't 0 or 1, text that isn't UTF-8 and
168
+ * bytes left over, so what it returns is what the state holds.
169
+ */
170
+ export declare function decodeProgram(value: Uint8Array): ProgramRecord;
package/dist/proof.js CHANGED
@@ -30,6 +30,13 @@ export const NAMESPACES = {
30
30
  // 44–46, upgrade 47); see node/src/state/mod.rs for what each holds.
31
31
  RESERVED_NAMES: 43,
32
32
  FEATURES: 47,
33
+ // Programs (protocol v2, docs/PROGRAMS-V0.md §4.1): empty until programs first switch on.
34
+ /** A program's record, keyed by its address as a string (as accounts are). */
35
+ PROGRAMS: 66,
36
+ PROGRAM_CODE: 67,
37
+ PROGRAM_INTERFACES: 68,
38
+ PROGRAM_STORAGE: 69,
39
+ PROGRAM_BLOB_REFS: 72,
33
40
  };
34
41
  /** Hash of an empty subtree. */
35
42
  export const EMPTY_HASH = new Uint8Array(32);
@@ -196,6 +203,20 @@ class Reader {
196
203
  this.at += 8;
197
204
  return v;
198
205
  }
206
+ /** `n` raw bytes. */
207
+ fixed(n) {
208
+ this.need(n);
209
+ const v = this.b.subarray(this.at, this.at + n);
210
+ this.at += n;
211
+ return v;
212
+ }
213
+ /** A borsh bool: one byte, 0 or 1 and nothing else. */
214
+ bool() {
215
+ const v = this.u8();
216
+ if (v > 1)
217
+ throw new Error(`record holds ${v} where a bool should be`);
218
+ return v === 1;
219
+ }
199
220
  str() {
200
221
  const n = this.u32();
201
222
  this.need(n);
@@ -253,3 +274,44 @@ export function decodeName(value) {
253
274
  rec.grace_blocks = sat(r.u64());
254
275
  return rec;
255
276
  }
277
+ /** A number the record holds as a u64, which must be one JSON and JS carry exactly. */
278
+ const exactCount = (v, what) => {
279
+ if (v > BigInt(Number.MAX_SAFE_INTEGER))
280
+ throw new Error(`a program record's ${what} (${v}) is past 2^53 − 1, which this SDK can't carry exactly`);
281
+ return Number(v);
282
+ };
283
+ /**
284
+ * Decodes a program record: the format byte (1: the only format this SDK reads, as the node's
285
+ * `Versioned` does) and the borsh body, field by field in the order the node writes them. Refuses
286
+ * a format it doesn't know, a bool or an option tag that isn't 0 or 1, text that isn't UTF-8 and
287
+ * bytes left over, so what it returns is what the state holds.
288
+ */
289
+ export function decodeProgram(value) {
290
+ const r = new Reader(value);
291
+ const format = r.u8();
292
+ if (format !== 1)
293
+ throw new Error(`program record format ${format}: this SDK reads format 1`);
294
+ const creator = r.str();
295
+ const created_at = exactCount(r.u64(), "created_at");
296
+ const wasm_hash = bytesToHex(r.fixed(32));
297
+ const interface_hash = bytesToHex(r.fixed(32));
298
+ const version = r.u32();
299
+ const hasAuthority = r.u8();
300
+ if (hasAuthority > 1)
301
+ throw new Error(`a program record's authority has option tag ${hasAuthority}`);
302
+ const authority = hasAuthority === 1 ? r.str() : null;
303
+ const upgrade_delay_blocks = exactCount(r.u64(), "upgrade_delay_blocks");
304
+ const policyByte = r.u8();
305
+ if (policyByte > 1)
306
+ throw new Error(`a program record's policy is ${policyByte}: this SDK knows open (0) and additive (1)`);
307
+ const accepts_transfers = r.bool();
308
+ const items = exactCount(r.u64(), "items");
309
+ const bytes = exactCount(r.u64(), "bytes");
310
+ const deposit = r.u64().toString();
311
+ const sponsored = r.u64().toString();
312
+ const record_deposit = r.u64().toString();
313
+ const record_payer = r.str();
314
+ if (r.at !== value.length)
315
+ throw new Error("a program record has bytes after its last field");
316
+ return { creator, created_at, wasm_hash, interface_hash, version, authority, upgrade_delay_blocks, policy: policyByte === 0 ? "open" : "additive", accepts_transfers, storage: { items, bytes, deposit }, sponsored, record_deposit, record_payer };
317
+ }
package/dist/tx.d.ts CHANGED
@@ -321,7 +321,93 @@ export type Action = {
321
321
  type: "admit_validator";
322
322
  validator: string;
323
323
  epoch: number;
324
+ }
325
+ /**
326
+ * Upload a program's code (WebAssembly 1.0) and its interface (format 1), both lowercase hex,
327
+ * bonding a storage deposit for each that isn't stored yet: at most `storage_limit` drops. The
328
+ * code is usable from the block after the upload's; a create names the two by their hashes
329
+ * (`wasmHash`, `interfaceHash`).
330
+ */
331
+ | {
332
+ type: "upload_code";
333
+ wasm: string;
334
+ interface: string;
335
+ storage_limit: string;
336
+ }
337
+ /**
338
+ * Create a program from stored code and interface, named by their hashes (64 lowercase hex). Its
339
+ * address is `programAddress(from, nonce)`: don't send FANG there before the create has applied
340
+ * (a failed create spends its nonce, stranding whatever was sent). `authority` (an address, or
341
+ * null for an immutable program), `upgrade_delay_blocks` and `policy` are stored with no power
342
+ * until program upgrades exist; an immutable program has delay 0 and policy "open". `init` runs
343
+ * the interface's `Init` method, present exactly when the interface declares one; without it,
344
+ * `fuel_limit` is 0 and there are no bindings.
345
+ */
346
+ | {
347
+ type: "create_program";
348
+ wasm_hash: string;
349
+ interface_hash: string;
350
+ authority?: string | null;
351
+ upgrade_delay_blocks: number;
352
+ policy: ProgramPolicy;
353
+ accepts_transfers: boolean;
354
+ init?: InitCall | null;
355
+ fuel_limit: number;
356
+ storage_limit: string;
357
+ bindings: ProgramBinding[];
358
+ }
359
+ /**
360
+ * Call programs: 1–8 calls run in order, all or nothing, sharing `fuel_limit` (1 to 20,000,000,
361
+ * and at most the chain's `program_max_tx_fuel`) and `storage_limit` (the most their storage
362
+ * deposits may bond, drops). Each call names the `versionDigest` of the program it was shown. The
363
+ * code a call may reach is the calls' own programs' and the `bindings`'.
364
+ */
365
+ | {
366
+ type: "call_program";
367
+ calls: ProgramCall[];
368
+ fuel_limit: number;
369
+ storage_limit: string;
370
+ bindings: ProgramBinding[];
371
+ }
372
+ /** An active validator (from its own account or its owner's) votes to pause programs; they pause once f + 1 of the epoch's validators have. */
373
+ | {
374
+ type: "programs_pause";
324
375
  };
376
+ /**
377
+ * What a `create_program`'s authority may later do to it, once program upgrades exist (inert in
378
+ * v0; node/src/types.rs `ProgramPolicy`): replace its code freely ("open"), or only with code
379
+ * whose interface keeps every method ("additive").
380
+ */
381
+ export type ProgramPolicy = "open" | "additive";
382
+ /**
383
+ * A `create_program`'s initializer (node/src/types.rs `InitCall`): the interface's `Init` method,
384
+ * its input (lowercase hex) and the FANG sent with it (drops; only if the method is payable).
385
+ */
386
+ export interface InitCall {
387
+ method: string;
388
+ input: string;
389
+ value: string;
390
+ }
391
+ /**
392
+ * One call of a `call_program` (node/src/types.rs `ProgramCall`): a program (by address), the
393
+ * `versionDigest` of it the signer was shown (never 64 zeros), a method of kind `Call`, its input
394
+ * (lowercase hex) and the FANG sent with it (drops; only if the method is payable).
395
+ */
396
+ export interface ProgramCall {
397
+ program: string;
398
+ version_digest: string;
399
+ method: string;
400
+ input: string;
401
+ value: string;
402
+ }
403
+ /**
404
+ * A program whose code a program action may reach besides its calls' own (node/src/types.rs
405
+ * `Binding`): by address, at the version digest given, or at any version for 64 zeros.
406
+ */
407
+ export interface ProgramBinding {
408
+ program: string;
409
+ version_digest: string;
410
+ }
325
411
  /**
326
412
  * What a ghost action carries (node/src/types.rs `GhostBundle`), every field lowercase hex: the
327
413
  * chain `instance` (from `/api/ghost`), the `anchor` root, two 32-byte `nullifiers` and
@@ -467,6 +553,8 @@ export declare const u64Field: (v: unknown, what?: string) => bigint;
467
553
  export declare function genesisHashBytes(genesisHash: string): Uint8Array;
468
554
  /** Unsigned ghost txs (`private_transfer`, `unshield`): no key signs them, their proof authorizes them. */
469
555
  export declare function isUnsignedAction(a: Action): boolean;
556
+ /** A program action (tags 62–64 and 69, node/src/types.rs `Action::is_program`). */
557
+ export declare function isProgramAction(a: Action): boolean;
470
558
  /**
471
559
  * The bytes a key signs (node/src/types.rs `Tx::signing_bytes`). For an unsigned ghost tx this is
472
560
  * its whole wire form instead: `BLOCKULA_GHOST_V1 ‖ chain_id ‖ genesis hash ‖ fee ‖ tip ‖ valid_until ‖ action`.
@@ -505,6 +593,53 @@ export declare function actionDigest(action: Action): string;
505
593
  * stored, resolved), title and link. Compute it from the proposal you show the voter.
506
594
  */
507
595
  export declare function proposalDigest(kind: ProposalKind, title: string, url?: string): string;
596
+ /**
597
+ * The contexts of programs' five digests, each BLAKE3 in derive-key mode: node/src/programs/digest.rs
598
+ * `CODE_CONTEXT`, `INTERFACE_CONTEXT`, `VERSION_CONTEXT` and `ADDRESS_CONTEXT`, and the VM's
599
+ * `SELECTOR_CONTEXT` (node/vm/src/interface.rs), the selector's only home on the node.
600
+ */
601
+ export declare const PROGRAM_DIGEST_CONTEXTS: Readonly<{
602
+ code: "blockula program code v1";
603
+ interface: "blockula program interface v1";
604
+ version: "blockula program version v1";
605
+ address: "blockula program address v1";
606
+ method: "blockula program method v1";
607
+ }>;
608
+ /** Every v0 program's version (node/src/programs/digest.rs `FIRST_VERSION`): only a program upgrade, in a later protocol version, raises it. */
609
+ export declare const FIRST_PROGRAM_VERSION = 1;
610
+ /**
611
+ * The hash that names a program's code (node/src/programs/digest.rs `wasm_hash`): BLAKE3-KDF("blockula
612
+ * program code v1", wasm), as 64 lowercase hex. A `create_program` names its code by it, once an
613
+ * `upload_code` of the same bytes (as bytes, or lowercase hex) has applied.
614
+ */
615
+ export declare function wasmHash(wasm: Uint8Array | string): string;
616
+ /** The hash that names an interface, its format-1 bytes (node/src/programs/digest.rs `interface_hash`): as `wasmHash`, under "blockula program interface v1". */
617
+ export declare function interfaceHash(iface: Uint8Array | string): string;
618
+ /**
619
+ * What a call to `program` binds (node/src/programs/digest.rs `version_digest`): BLAKE3-KDF("blockula
620
+ * program version v1", the program's address as a string field (u16 LE length, its bytes) ‖ its
621
+ * code's hash ‖ its interface's hash ‖ its version, u32 LE), as 64 lowercase hex. A call runs only
622
+ * while this is the program's digest in force, so it runs the code its signer was shown: compute it
623
+ * from the record you show (its hashes and version), rather than sign a digest a node reported.
624
+ */
625
+ export declare function versionDigest(program: string, wasmHashHex: string, interfaceHashHex: string, version?: number): string;
626
+ /**
627
+ * The address of the program `creator` creates with its transaction of `nonce` (node/src/programs/digest.rs
628
+ * `program_address`): bech32m(prefix, BLAKE3-KDF("blockula program address v1", the creator's 32
629
+ * address bytes ‖ nonce u64 LE)). `prefix` defaults to the creator's own (a chain admits only its
630
+ * prefix, so for any create it runs the two are the same).
631
+ *
632
+ * Don't send FANG to it before the create has applied: a create that fails spends its nonce, no
633
+ * later create reaches the address, and what was sent there is stranded for good (docs/PROGRAMS-V0.md
634
+ * §2.3). Fund a program through its initializer's value, or by transfer once it exists.
635
+ */
636
+ export declare function programAddress(creator: string, nonce: number | bigint | string, prefix?: string): string;
637
+ /**
638
+ * A method's selector (node/vm/src/interface.rs `selector`): the first 8 bytes of
639
+ * BLAKE3-KDF("blockula program method v1", its name), as 16 lowercase hex. An interface's methods
640
+ * have distinct selectors. Only identifiers are method names, so anything else is refused.
641
+ */
642
+ export declare function methodSelector(name: string): string;
508
643
  /** One claim of a Séance pair, as the juror judged it. */
509
644
  export interface SeanceClaimRef {
510
645
  id: number;
package/dist/tx.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Canonical transaction encoding. Must stay byte-identical to node/src/types.rs `Tx::signing_bytes`.
2
2
  import { blake3 } from "@noble/hashes/blake3.js";
3
3
  import { bytesToHex, concatBytes, hexToBytes } from "@noble/hashes/utils.js";
4
+ import { bech32m } from "@scure/base";
4
5
  export const PUBLIC_KEY_LEN = 1952;
5
6
  export const SIGNATURE_LEN = 3309;
6
7
  /** Domain of the unsigned ghost wire form (`private_transfer`, `unshield`). */
@@ -57,6 +58,24 @@ export function uintField(v, max, what = "integer field") {
57
58
  }
58
59
  /** A u64 field (`uintField` up to 2^64 − 1). */
59
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
+ }
60
79
  class Writer {
61
80
  parts = [];
62
81
  bytes(b) { for (const x of b)
@@ -106,6 +125,20 @@ class Writer {
106
125
  for (const x of xs)
107
126
  this.str(x, what);
108
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
+ }
109
142
  done() { return Uint8Array.from(this.parts); }
110
143
  }
111
144
  /** A chain's genesis hash as its 32 bytes (`/api/status` `genesis_hash`, lowercase hex). */
@@ -119,6 +152,10 @@ export function genesisHashBytes(genesisHash) {
119
152
  export function isUnsignedAction(a) {
120
153
  return a.type === "private_transfer" || a.type === "unshield";
121
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
+ }
122
159
  /**
123
160
  * The bytes a key signs (node/src/types.rs `Tx::signing_bytes`). For an unsigned ghost tx this is
124
161
  * its whole wire form instead: `BLOCKULA_GHOST_V1 ‖ chain_id ‖ genesis hash ‖ fee ‖ tip ‖ valid_until ‖ action`.
@@ -380,6 +417,9 @@ function encodeAction(w, a) {
380
417
  case "multisig_propose":
381
418
  if (a.action.type === "multisig_propose")
382
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");
383
423
  w.u8(43);
384
424
  encodeAction(w, a.action);
385
425
  break;
@@ -444,10 +484,76 @@ function encodeAction(w, a) {
444
484
  w.str(a.validator);
445
485
  w.int(a.epoch, "epoch");
446
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;
447
527
  default:
448
528
  throw new Error(`unknown action type ${a.type}`);
449
529
  }
450
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
+ }
451
557
  /** A bundle without its proof, raw and in wire order: what the sighash covers. */
452
558
  function encodeBundlePublic(w, b) {
453
559
  w.fixed(b.instance, 32);
@@ -661,6 +767,113 @@ function digestOf(what, context, encode) {
661
767
  }
662
768
  return bytesToHex(blake3(w.done(), { context: utf8.encode(context) }));
663
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
+ }
664
877
  /**
665
878
  * What a `commit_votes` binds (node/src/types.rs `seance_pairs_digest`): the Séance, the juror and,
666
879
  * per assigned pair in order, both claims' ids, repos and PR numbers.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@blockula/sdk",
3
- "version": "0.5.1",
4
- "description": "\ud83e\udddb TypeScript SDK for Blockula \u2014 post-quantum wallets (ML-DSA-65), .boo names, transfers",
3
+ "version": "1.0.0",
4
+ "description": "\ud83e\udddb TypeScript SDK for Blockula: wallets with post-quantum signatures (ML-DSA-65), .boo names and transfers. Experimental software, provided as is under MIT.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -16,9 +16,10 @@
16
16
  ],
17
17
  "scripts": {
18
18
  "build": "tsc",
19
- "test": "tsx test/e2e.ts",
19
+ "test": "tsx test/e2e.ts && tsx test/e2e-integrators.ts",
20
+ "test:programs": "tsx test/e2e-programs.ts",
20
21
  "test:kat": "tsx test/kat.ts",
21
- "test:vectors": "tsx test/passkey-vectors.ts && tsx test/proof-vectors.ts && tsx test/tx-vectors.ts && tsx test/unit.ts"
22
+ "test:vectors": "tsx test/passkey-vectors.ts && tsx test/proof-vectors.ts && tsx test/tx-vectors.ts && tsx test/program-vectors.ts && tsx test/programs.ts && tsx test/unit.ts && tsx test/integrators.ts && tsx test/examples.ts"
22
23
  },
23
24
  "license": "MIT",
24
25
  "dependencies": {
@@ -29,7 +30,7 @@
29
30
  "devDependencies": {
30
31
  "@types/node": "*",
31
32
  "tsx": "*",
32
- "typescript": "*"
33
+ "typescript": "^7.0.2"
33
34
  },
34
35
  "author": "Jack Dishman",
35
36
  "keywords": [