@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/README.md +143 -13
- package/dist/describe.d.ts +3 -1
- package/dist/describe.js +93 -2
- package/dist/index.d.ts +602 -11
- package/dist/index.js +1080 -58
- package/dist/networks.d.ts +25 -0
- package/dist/networks.js +27 -0
- package/dist/payments.d.ts +83 -0
- package/dist/payments.js +240 -0
- package/dist/programs.d.ts +471 -0
- package/dist/programs.js +779 -0
- package/dist/proof.d.ts +44 -0
- package/dist/proof.js +62 -0
- package/dist/tx.d.ts +135 -0
- package/dist/tx.js +213 -0
- package/package.json +6 -5
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.
|
|
4
|
-
"description": "\ud83e\udddb TypeScript SDK for Blockula
|
|
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": [
|