@chainvue/verus-sapling 0.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.
Files changed (44) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +74 -0
  3. package/README.md +194 -0
  4. package/crate/pkg/package.json +17 -0
  5. package/crate/pkg/verus_sapling_prover.d.ts +71 -0
  6. package/crate/pkg/verus_sapling_prover.js +451 -0
  7. package/crate/pkg/verus_sapling_prover_bg.wasm +0 -0
  8. package/crate/pkg/verus_sapling_prover_bg.wasm.d.ts +15 -0
  9. package/dist/browser/grpcweb.d.ts +29 -0
  10. package/dist/browser/grpcweb.js +149 -0
  11. package/dist/browser/index.d.ts +23 -0
  12. package/dist/browser/index.js +23 -0
  13. package/dist/browser/lightwalletd-web.d.ts +36 -0
  14. package/dist/browser/lightwalletd-web.js +186 -0
  15. package/dist/browser/protobuf.d.ts +38 -0
  16. package/dist/browser/protobuf.js +115 -0
  17. package/dist/browser/prover-worker.d.ts +18 -0
  18. package/dist/browser/prover-worker.js +61 -0
  19. package/dist/browser/worker-prover.d.ts +38 -0
  20. package/dist/browser/worker-prover.js +51 -0
  21. package/dist/errors.d.ts +18 -0
  22. package/dist/errors.js +25 -0
  23. package/dist/hex.d.ts +11 -0
  24. package/dist/hex.js +32 -0
  25. package/dist/index.d.ts +33 -0
  26. package/dist/index.js +35 -0
  27. package/dist/lightwalletd.d.ts +113 -0
  28. package/dist/lightwalletd.js +99 -0
  29. package/dist/money.d.ts +23 -0
  30. package/dist/money.js +40 -0
  31. package/dist/parse.d.ts +45 -0
  32. package/dist/parse.js +129 -0
  33. package/dist/types.d.ts +29 -0
  34. package/dist/types.js +13 -0
  35. package/dist/wallet.d.ts +124 -0
  36. package/dist/wallet.js +179 -0
  37. package/dist/wasm.d.ts +91 -0
  38. package/dist/wasm.js +90 -0
  39. package/dist/zaddr.d.ts +19 -0
  40. package/dist/zaddr.js +107 -0
  41. package/package.json +90 -0
  42. package/proto/compact_formats.proto +62 -0
  43. package/proto/darkside.proto +124 -0
  44. package/proto/service.proto +172 -0
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @chainvue/verus-sapling
3
+ *
4
+ * Offline signing of Verus shielded (Sapling) transactions — t→z, z→z, z→t,
5
+ * with memo — plus client-side note detection and memo reading. Companion to
6
+ * @chainvue/verus-sdk: it builds and signs bytes; the consumer broadcasts. No
7
+ * full node on the signing host (z→z / z→t need witness data from a Verus
8
+ * lightwalletd, reached through a `LightwalletdTransport`).
9
+ *
10
+ * Verus shielded = stock Zcash Sapling (stock circuit, byte-identical MPC
11
+ * params, consensus branch id 0x76b809bb).
12
+ *
13
+ * The API:
14
+ * - `initSapling` + the wasm builders (`shieldT2z` / `spendShielded` /
15
+ * `detectNotesWasm` / `readNote`) — the prover boundary (`wasm.ts`).
16
+ * - the lightwalletd-driven orchestration (`detectNotes`, `buildShieldedSpend`)
17
+ * over a `LightwalletdTransport` (`wallet.ts`).
18
+ * - pure helpers: v4/tree parsers, `parseSats`/`toSafeNumber`, hex, `zs` decode.
19
+ *
20
+ * Entry points: the package root is browser-safe and pulls in no `@grpc/grpc-js`.
21
+ * Browser consumers also import `@chainvue/verus-sapling/browser` (gRPC-web
22
+ * client + Web Worker prover); Node consumers import
23
+ * `@chainvue/verus-sapling/lightwalletd` (the gRPC client).
24
+ */
25
+ export * from './types.js';
26
+ export * from './errors.js';
27
+ // Pure helpers (dependency-free, browser-safe).
28
+ export * from './parse.js';
29
+ export { CONSENSUS_BRANCH_ID, parseSats, toSafeNumber } from './money.js';
30
+ export { bytesToHex, hexToBytes, reverseBytes } from './hex.js';
31
+ export { decodeSaplingAddress, saplingAddressToHex } from './zaddr.js';
32
+ // wasm prover loaders + low-level builders (init once, then call).
33
+ export { initSapling, shieldT2z, spendShielded, detectNotes as detectNotesWasm, readNote, verifyCanonicalParams, PARAM_SHA256, } from './wasm.js';
34
+ // Shielded wallet orchestration (transport- and prover-agnostic).
35
+ export { detectNotes, buildShieldedSpend, } from './wallet.js';
@@ -0,0 +1,113 @@
1
+ /**
2
+ * lightwalletd gRPC adapter — the production data source for shielded spends.
3
+ *
4
+ * Replaces the daemon-RPC "gather" shortcut used in testing. Talks to a
5
+ * `lightwalletd` (VerusCoin/lightwalletd) over gRPC and supplies everything the
6
+ * prover needs without a full node on the signing host:
7
+ * - getTreeState(h) → the note-commitment tree at height h (witness/anchor base)
8
+ * - getBlockRange → compact blocks (cmus for witness position + note scanning)
9
+ * - getTransaction → full raw tx (to extract the full output of a note to spend)
10
+ * - sendTransaction → broadcast the signed tx
11
+ *
12
+ * Verified against a live Verus testnet lightwalletd: GetLightdInfo, GetTreeState
13
+ * (238-byte tree = the finalState our witness builder parses), GetBlockRange
14
+ * (cmus + nullifiers), and GetTransaction (full raw tx) all return correctly.
15
+ *
16
+ * gRPC over Node: uses @grpc/grpc-js + @grpc/proto-loader with the `.proto`
17
+ * files in `../proto`. A *browser* consumer needs a gRPC-web proxy in front of
18
+ * lightwalletd; a Node/server consumer connects directly (optionally over an
19
+ * SSH tunnel in dev).
20
+ */
21
+ import * as grpc from '@grpc/grpc-js';
22
+ /** TreeState (from lightwalletd, derived from z_gettreestate). `tree` is the
23
+ * commitment-tree finalState hex our witness builder parses. */
24
+ export interface TreeState {
25
+ network: string;
26
+ height: string;
27
+ hash: string;
28
+ time: number;
29
+ tree: string;
30
+ }
31
+ /** One Sapling output in a compact block. */
32
+ export interface CompactOutput {
33
+ cmu: Uint8Array;
34
+ epk: Uint8Array;
35
+ ciphertext: Uint8Array;
36
+ }
37
+ export interface CompactTx {
38
+ index: string;
39
+ hash: Uint8Array;
40
+ spends: {
41
+ nf: Uint8Array;
42
+ }[];
43
+ outputs: CompactOutput[];
44
+ }
45
+ export interface CompactBlock {
46
+ height: string;
47
+ hash: Uint8Array;
48
+ vtx: CompactTx[];
49
+ }
50
+ export interface LightdInfo {
51
+ chainName: string;
52
+ consensusBranchId: string;
53
+ blockHeight: string;
54
+ saplingActivationHeight: string;
55
+ estimatedHeight: string;
56
+ }
57
+ /**
58
+ * The minimal lightwalletd RPC surface the shielded orchestration (`../wallet`)
59
+ * depends on. Abstracting it keeps the wallet logic free of any concrete
60
+ * transport: `LightwalletdClient` implements it over Node `@grpc/grpc-js`; a
61
+ * browser consumer implements the same interface over a gRPC-web proxy.
62
+ */
63
+ export interface LightwalletdTransport {
64
+ getLatestHeight(): Promise<number>;
65
+ getTreeState(height: number): Promise<TreeState>;
66
+ getTransaction(txidDisplayHex: string): Promise<{
67
+ data: Uint8Array;
68
+ height: string;
69
+ }>;
70
+ getBlockRange(start: number, end: number): AsyncGenerator<CompactBlock>;
71
+ sendTransaction(txHex: string): Promise<{
72
+ errorCode: number;
73
+ errorMessage: string;
74
+ }>;
75
+ }
76
+ /** Thin, typed client over the lightwalletd CompactTxStreamer service. */
77
+ export declare class LightwalletdClient implements LightwalletdTransport {
78
+ private readonly svc;
79
+ /**
80
+ * @param target host:port of lightwalletd (e.g. "lightwalletd:9067").
81
+ * @param opts `credentials` to supply channel credentials explicitly, or
82
+ * `insecure: true` for a plaintext channel (dev tunnels only). The default is
83
+ * **TLS** (`createSsl()`): an unencrypted channel lets a network observer see
84
+ * which notes/txids the wallet fetches and lets a MITM feed fabricated tree
85
+ * state. The spending key never crosses this channel, but privacy and
86
+ * integrity do.
87
+ */
88
+ constructor(target: string, opts?: {
89
+ credentials?: grpc.ChannelCredentials;
90
+ insecure?: boolean;
91
+ });
92
+ private unary;
93
+ getLightdInfo(): Promise<LightdInfo>;
94
+ getLatestHeight(): Promise<number>;
95
+ /** Tree state at `height` — the witness/anchor base. */
96
+ getTreeState(height: number): Promise<TreeState>;
97
+ /** Full raw transaction by txid (display-order hex string). Needed to extract
98
+ * the full Sapling output (cv/cmu/epk/enc/out/proof) of a note being spent. */
99
+ getTransaction(txidDisplayHex: string): Promise<{
100
+ data: Uint8Array;
101
+ height: string;
102
+ }>;
103
+ /** Stream compact blocks in [start, end]. Used for note detection
104
+ * (trial-decrypt outputs) and to collect a block's ordered cmus for witness
105
+ * construction. */
106
+ getBlockRange(start: number, end: number): AsyncGenerator<CompactBlock>;
107
+ /** Broadcast a signed transaction (hex). Returns {errorCode, errorMessage}. */
108
+ sendTransaction(txHex: string): Promise<{
109
+ errorCode: number;
110
+ errorMessage: string;
111
+ }>;
112
+ close(): void;
113
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * lightwalletd gRPC adapter — the production data source for shielded spends.
3
+ *
4
+ * Replaces the daemon-RPC "gather" shortcut used in testing. Talks to a
5
+ * `lightwalletd` (VerusCoin/lightwalletd) over gRPC and supplies everything the
6
+ * prover needs without a full node on the signing host:
7
+ * - getTreeState(h) → the note-commitment tree at height h (witness/anchor base)
8
+ * - getBlockRange → compact blocks (cmus for witness position + note scanning)
9
+ * - getTransaction → full raw tx (to extract the full output of a note to spend)
10
+ * - sendTransaction → broadcast the signed tx
11
+ *
12
+ * Verified against a live Verus testnet lightwalletd: GetLightdInfo, GetTreeState
13
+ * (238-byte tree = the finalState our witness builder parses), GetBlockRange
14
+ * (cmus + nullifiers), and GetTransaction (full raw tx) all return correctly.
15
+ *
16
+ * gRPC over Node: uses @grpc/grpc-js + @grpc/proto-loader with the `.proto`
17
+ * files in `../proto`. A *browser* consumer needs a gRPC-web proxy in front of
18
+ * lightwalletd; a Node/server consumer connects directly (optionally over an
19
+ * SSH tunnel in dev).
20
+ */
21
+ import { fileURLToPath } from 'node:url';
22
+ import * as path from 'node:path';
23
+ import * as grpc from '@grpc/grpc-js';
24
+ import * as protoLoader from '@grpc/proto-loader';
25
+ import { ShieldedInputError } from './errors.js';
26
+ const PROTO_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'proto');
27
+ /** Thin, typed client over the lightwalletd CompactTxStreamer service. */
28
+ export class LightwalletdClient {
29
+ svc;
30
+ /**
31
+ * @param target host:port of lightwalletd (e.g. "lightwalletd:9067").
32
+ * @param opts `credentials` to supply channel credentials explicitly, or
33
+ * `insecure: true` for a plaintext channel (dev tunnels only). The default is
34
+ * **TLS** (`createSsl()`): an unencrypted channel lets a network observer see
35
+ * which notes/txids the wallet fetches and lets a MITM feed fabricated tree
36
+ * state. The spending key never crosses this channel, but privacy and
37
+ * integrity do.
38
+ */
39
+ constructor(target, opts) {
40
+ const def = protoLoader.loadSync('service.proto', {
41
+ keepCase: true,
42
+ longs: String,
43
+ enums: String,
44
+ defaults: true,
45
+ oneofs: true,
46
+ includeDirs: [PROTO_DIR],
47
+ });
48
+ const pkg = grpc.loadPackageDefinition(def);
49
+ const Ctor = pkg.cash.z.wallet.sdk.rpc.CompactTxStreamer;
50
+ const credentials = opts?.credentials ??
51
+ (opts?.insecure ? grpc.credentials.createInsecure() : grpc.credentials.createSsl());
52
+ this.svc = new Ctor(target, credentials);
53
+ }
54
+ unary(method, arg) {
55
+ return new Promise((resolve, reject) => this.svc[method](arg, (err, res) => err ? reject(err) : resolve(res)));
56
+ }
57
+ getLightdInfo() {
58
+ return this.unary('GetLightdInfo', {});
59
+ }
60
+ getLatestHeight() {
61
+ return this.unary('GetLatestBlock', {}).then((b) => Number(b.height));
62
+ }
63
+ /** Tree state at `height` — the witness/anchor base. */
64
+ getTreeState(height) {
65
+ return this.unary('GetTreeState', { height });
66
+ }
67
+ /** Full raw transaction by txid (display-order hex string). Needed to extract
68
+ * the full Sapling output (cv/cmu/epk/enc/out/proof) of a note being spent. */
69
+ getTransaction(txidDisplayHex) {
70
+ // Validate before Buffer.from, which silently truncates at the first non-hex
71
+ // char and would query a wrong (shorter) hash.
72
+ if (!/^[0-9a-fA-F]{64}$/.test(txidDisplayHex)) {
73
+ throw new ShieldedInputError(`getTransaction: txid must be 64 hex chars, got ${txidDisplayHex.length}`);
74
+ }
75
+ const hash = Buffer.from(txidDisplayHex, 'hex').reverse(); // internal byte order
76
+ return this.unary('GetTransaction', { hash });
77
+ }
78
+ /** Stream compact blocks in [start, end]. Used for note detection
79
+ * (trial-decrypt outputs) and to collect a block's ordered cmus for witness
80
+ * construction. */
81
+ async *getBlockRange(start, end) {
82
+ const stream = this.svc.GetBlockRange({ start: { height: start }, end: { height: end } });
83
+ try {
84
+ for await (const block of stream) {
85
+ yield block;
86
+ }
87
+ }
88
+ finally {
89
+ stream.cancel?.();
90
+ }
91
+ }
92
+ /** Broadcast a signed transaction (hex). Returns {errorCode, errorMessage}. */
93
+ sendTransaction(txHex) {
94
+ return this.unary('SendTransaction', { data: Buffer.from(txHex, 'hex'), height: 0 });
95
+ }
96
+ close() {
97
+ this.svc.close();
98
+ }
99
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Money helpers, kept LOCAL and dependency-free so the browser-reachable core
3
+ * (wallet orchestration) bundles without pulling @chainvue/verus-sdk — whose
4
+ * published `dist/bundle.js` bundles @bitgo/utxo-lib and Node builtins
5
+ * (`crypto`/`buffer`/`os`), which don't belong in a browser extension.
6
+ *
7
+ * These mirror the SDK's semantics exactly (same money invariant): satoshi
8
+ * amounts are `bigint` end to end; `toSafeNumber` is the ONLY checked crossing
9
+ * into a float64 boundary. Node consumers who already have the SDK can keep
10
+ * using its versions — these exist so the shielded package stays self-contained.
11
+ */
12
+ /** Verus consensus branch id for the ZIP-243 sighash / binding sig (0x76b809bb). */
13
+ export declare const CONSENSUS_BRANCH_ID = 1991772603;
14
+ /**
15
+ * Exact decimal-string → satoshi `bigint`. No float math (no `coins * 1e8`).
16
+ * Rejects malformed input and more than 8 fractional digits.
17
+ */
18
+ export declare function parseSats(amount: string): bigint;
19
+ /**
20
+ * Checked crossing from `bigint` satoshis into a JS `number` (the only place a
21
+ * satoshi value becomes float64). Throws outside `[0, 2^53)`.
22
+ */
23
+ export declare function toSafeNumber(sats: bigint): number;
package/dist/money.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Money helpers, kept LOCAL and dependency-free so the browser-reachable core
3
+ * (wallet orchestration) bundles without pulling @chainvue/verus-sdk — whose
4
+ * published `dist/bundle.js` bundles @bitgo/utxo-lib and Node builtins
5
+ * (`crypto`/`buffer`/`os`), which don't belong in a browser extension.
6
+ *
7
+ * These mirror the SDK's semantics exactly (same money invariant): satoshi
8
+ * amounts are `bigint` end to end; `toSafeNumber` is the ONLY checked crossing
9
+ * into a float64 boundary. Node consumers who already have the SDK can keep
10
+ * using its versions — these exist so the shielded package stays self-contained.
11
+ */
12
+ /** Verus consensus branch id for the ZIP-243 sighash / binding sig (0x76b809bb). */
13
+ export const CONSENSUS_BRANCH_ID = 0x76b809bb; // 1991772603
14
+ const SATS_PER_COIN = 100000000n;
15
+ const MAX_DECIMALS = 8;
16
+ /**
17
+ * Exact decimal-string → satoshi `bigint`. No float math (no `coins * 1e8`).
18
+ * Rejects malformed input and more than 8 fractional digits.
19
+ */
20
+ export function parseSats(amount) {
21
+ const s = amount.trim();
22
+ if (!/^\d+(\.\d+)?$/.test(s))
23
+ throw new Error(`invalid amount: "${amount}"`);
24
+ const [intPart, fracPart = ''] = s.split('.');
25
+ if (fracPart.length > MAX_DECIMALS) {
26
+ throw new Error(`amount "${amount}" has more than ${MAX_DECIMALS} decimals`);
27
+ }
28
+ const frac = (fracPart + '00000000').slice(0, MAX_DECIMALS);
29
+ return BigInt(intPart) * SATS_PER_COIN + BigInt(frac);
30
+ }
31
+ /**
32
+ * Checked crossing from `bigint` satoshis into a JS `number` (the only place a
33
+ * satoshi value becomes float64). Throws outside `[0, 2^53)`.
34
+ */
35
+ export function toSafeNumber(sats) {
36
+ if (sats < 0n || sats > BigInt(Number.MAX_SAFE_INTEGER)) {
37
+ throw new Error(`satoshi value ${sats} outside safe range [0, 2^53)`);
38
+ }
39
+ return Number(sats);
40
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Pure byte parsers for the two pieces of chain data a shielded spend needs from
3
+ * lightwalletd, with no external dependency:
4
+ *
5
+ * - `parseTreeState` — the note-commitment tree `finalState` (zcashd/Verus
6
+ * serialization) → { left, right, parents }, the witness base the prover
7
+ * reconstructs into a `CommitmentTree`.
8
+ * - `parseSaplingOutput` — the full 948-byte Sapling output description at a
9
+ * given index of a raw v4 transaction (cv/cmu/epk/enc/ct/proof), needed to
10
+ * re-derive and spend a note.
11
+ *
12
+ * These were validated end to end: the parsed outputs and tree fed a wasm
13
+ * `spend_shielded` that produced an on-chain-accepted z→z (txid 07e3b38e…f996).
14
+ * Kept dependency-free and unit-tested so the byte layout is pinned.
15
+ */
16
+ /** The note-commitment tree state, parts as raw-wire hex (nulls for empty slots). */
17
+ export interface ParsedTreeState {
18
+ left: string | null;
19
+ right: string | null;
20
+ parents: (string | null)[];
21
+ }
22
+ /**
23
+ * Parse a zcashd/Verus commitment-tree `finalState` (hex) — as returned by
24
+ * `z_gettreestate` / lightwalletd `GetTreeState` — into { left, right, parents }.
25
+ * Layout: `EmptyOrValue<Node> left; EmptyOrValue<Node> right;
26
+ * vector<EmptyOrValue<Node>> parents`, each `EmptyOrValue` = 1 tag byte then
27
+ * (if 1) 32 bytes.
28
+ */
29
+ export declare function parseTreeState(finalStateHex: string): ParsedTreeState;
30
+ /** A full Sapling output description, fields as raw-wire hex. */
31
+ export interface SaplingOutputBytes {
32
+ cv: string;
33
+ cmu: string;
34
+ epk: string;
35
+ enc: string;
36
+ ct: string;
37
+ proof: string;
38
+ }
39
+ /**
40
+ * Extract the Sapling output description at `index` from a raw Sapling **v4**
41
+ * transaction. Walks the transparent inputs/outputs and shielded spends to reach
42
+ * `vShieldedOutput`, then returns the 948-byte description split into fields.
43
+ * Throws if the tx has fewer than `index + 1` shielded outputs.
44
+ */
45
+ export declare function parseSaplingOutput(rawTx: Uint8Array, index: number): SaplingOutputBytes;
package/dist/parse.js ADDED
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Pure byte parsers for the two pieces of chain data a shielded spend needs from
3
+ * lightwalletd, with no external dependency:
4
+ *
5
+ * - `parseTreeState` — the note-commitment tree `finalState` (zcashd/Verus
6
+ * serialization) → { left, right, parents }, the witness base the prover
7
+ * reconstructs into a `CommitmentTree`.
8
+ * - `parseSaplingOutput` — the full 948-byte Sapling output description at a
9
+ * given index of a raw v4 transaction (cv/cmu/epk/enc/ct/proof), needed to
10
+ * re-derive and spend a note.
11
+ *
12
+ * These were validated end to end: the parsed outputs and tree fed a wasm
13
+ * `spend_shielded` that produced an on-chain-accepted z→z (txid 07e3b38e…f996).
14
+ * Kept dependency-free and unit-tested so the byte layout is pinned.
15
+ */
16
+ import { bytesToHex, hexToBytes } from './hex.js';
17
+ /** Minimal sequential byte reader over a `Uint8Array` (little-endian ints). */
18
+ class ByteReader {
19
+ buf;
20
+ off = 0;
21
+ constructor(buf) {
22
+ this.buf = buf;
23
+ }
24
+ view() {
25
+ return new DataView(this.buf.buffer, this.buf.byteOffset, this.buf.byteLength);
26
+ }
27
+ u8() {
28
+ if (this.off >= this.buf.length)
29
+ throw new RangeError('ByteReader: read past end');
30
+ return this.buf[this.off++];
31
+ }
32
+ slice(n) {
33
+ if (this.off + n > this.buf.length)
34
+ throw new RangeError('ByteReader: slice past end');
35
+ const s = this.buf.subarray(this.off, this.off + n);
36
+ this.off += n;
37
+ return s;
38
+ }
39
+ skip(n) {
40
+ if (this.off + n > this.buf.length)
41
+ throw new RangeError('ByteReader: skip past end');
42
+ this.off += n;
43
+ }
44
+ /** Bitcoin/Zcash CompactSize varint. */
45
+ varint() {
46
+ const b = this.u8();
47
+ if (b < 0xfd)
48
+ return b;
49
+ if (b === 0xfd) {
50
+ const v = this.view().getUint16(this.off, true);
51
+ this.off += 2;
52
+ return v;
53
+ }
54
+ if (b === 0xfe) {
55
+ const v = this.view().getUint32(this.off, true);
56
+ this.off += 4;
57
+ return v;
58
+ }
59
+ const v = this.view().getBigUint64(this.off, true);
60
+ this.off += 8;
61
+ if (v > BigInt(Number.MAX_SAFE_INTEGER))
62
+ throw new RangeError('varint exceeds MAX_SAFE_INTEGER');
63
+ return Number(v);
64
+ }
65
+ }
66
+ const toHex = (b) => bytesToHex(b);
67
+ /**
68
+ * Parse a zcashd/Verus commitment-tree `finalState` (hex) — as returned by
69
+ * `z_gettreestate` / lightwalletd `GetTreeState` — into { left, right, parents }.
70
+ * Layout: `EmptyOrValue<Node> left; EmptyOrValue<Node> right;
71
+ * vector<EmptyOrValue<Node>> parents`, each `EmptyOrValue` = 1 tag byte then
72
+ * (if 1) 32 bytes.
73
+ */
74
+ export function parseTreeState(finalStateHex) {
75
+ const r = new ByteReader(hexToBytes(finalStateHex));
76
+ const opt = () => (r.u8() === 1 ? toHex(r.slice(32)) : null);
77
+ const left = opt();
78
+ const right = opt();
79
+ const n = r.varint();
80
+ const parents = [];
81
+ for (let i = 0; i < n; i++)
82
+ parents.push(opt());
83
+ return { left, right, parents };
84
+ }
85
+ /**
86
+ * Extract the Sapling output description at `index` from a raw Sapling **v4**
87
+ * transaction. Walks the transparent inputs/outputs and shielded spends to reach
88
+ * `vShieldedOutput`, then returns the 948-byte description split into fields.
89
+ * Throws if the tx has fewer than `index + 1` shielded outputs.
90
+ */
91
+ export function parseSaplingOutput(rawTx, index) {
92
+ const r = new ByteReader(rawTx);
93
+ r.skip(8); // header (4) + versionGroupId (4)
94
+ const nIn = r.varint();
95
+ for (let i = 0; i < nIn; i++) {
96
+ r.skip(36); // prevout: txid(32) + vout(4)
97
+ const scriptLen = r.varint();
98
+ r.skip(scriptLen);
99
+ r.skip(4); // sequence
100
+ }
101
+ const nOut = r.varint();
102
+ for (let i = 0; i < nOut; i++) {
103
+ r.skip(8); // value
104
+ const scriptLen = r.varint();
105
+ r.skip(scriptLen);
106
+ }
107
+ r.skip(4 + 4 + 8); // lockTime + expiryHeight + valueBalance(i64)
108
+ const nSpend = r.varint();
109
+ r.skip(nSpend * 384); // each SpendDescription is 384 bytes in a full tx
110
+ const nShieldedOut = r.varint();
111
+ if (index >= nShieldedOut) {
112
+ throw new RangeError(`shielded output index ${index} out of range (have ${nShieldedOut})`);
113
+ }
114
+ let found;
115
+ for (let i = 0; i < nShieldedOut; i++) {
116
+ const d = r.slice(948);
117
+ if (i === index)
118
+ found = d;
119
+ }
120
+ const d = found;
121
+ return {
122
+ cv: toHex(d.subarray(0, 32)),
123
+ cmu: toHex(d.subarray(32, 64)),
124
+ epk: toHex(d.subarray(64, 96)),
125
+ enc: toHex(d.subarray(96, 676)),
126
+ ct: toHex(d.subarray(676, 756)),
127
+ proof: toHex(d.subarray(756, 948)),
128
+ };
129
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared domain vocabulary for the shielded package.
3
+ *
4
+ * Money invariant (inherited from @chainvue/verus-sdk): ALL satoshi amounts are
5
+ * `bigint`, end to end — never `number`. The only checked crossing into a
6
+ * float64 boundary is `toSafeNumber` (see `money.ts`).
7
+ *
8
+ * The concrete request/result shapes live with the functions that use them:
9
+ * `SpendableNote` / `ShieldedRecipient` / `BuildShieldedSpendParams` in
10
+ * `wallet.ts`, `SaplingParams` / `ReadNoteResult` in `wasm.ts`.
11
+ */
12
+ /** Sapling payment address, Bech32: `zs`-prefixed (mainnet) / `ztestsapling` (testnet). */
13
+ export type SaplingAddress = string;
14
+ /** Transparent Verus address (R-address, P2PKH). */
15
+ export type TransparentAddress = string;
16
+ /**
17
+ * Sapling extended spending key (ZIP-32) as 169-byte hex — the full spend
18
+ * authority. Same trust surface as a transparent WIF: it is present on the
19
+ * signing host; callers must protect it accordingly.
20
+ */
21
+ export type SaplingSpendingKey = string;
22
+ /**
23
+ * A Sapling memo. A `string` is UTF-8-encoded then zero-padded to 512 bytes; a
24
+ * hex string via a recipient's `memoHex` carries raw bytes verbatim (for
25
+ * structured/binary memos). Max 512 bytes either way.
26
+ */
27
+ export type Memo = string;
28
+ /** Max memo size in bytes (Zcash/Verus Sapling note-plaintext memo field). */
29
+ export declare const MEMO_MAX_BYTES = 512;
package/dist/types.js ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Shared domain vocabulary for the shielded package.
3
+ *
4
+ * Money invariant (inherited from @chainvue/verus-sdk): ALL satoshi amounts are
5
+ * `bigint`, end to end — never `number`. The only checked crossing into a
6
+ * float64 boundary is `toSafeNumber` (see `money.ts`).
7
+ *
8
+ * The concrete request/result shapes live with the functions that use them:
9
+ * `SpendableNote` / `ShieldedRecipient` / `BuildShieldedSpendParams` in
10
+ * `wallet.ts`, `SaplingParams` / `ReadNoteResult` in `wasm.ts`.
11
+ */
12
+ /** Max memo size in bytes (Zcash/Verus Sapling note-plaintext memo field). */
13
+ export const MEMO_MAX_BYTES = 512;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Shielded wallet orchestration — the typed home for the flows proven end to end
3
+ * on Verus testnet, sourcing ALL chain data from lightwalletd (no full node):
4
+ *
5
+ * - `detectNotes` — find the wallet's own notes by trial-decrypting the
6
+ * compact blocks of a height range, and drop any already
7
+ * spent within that range. Replaces `z_listunspent`.
8
+ * - `buildShieldedSpend`— assemble and prove a z→z / z→t spend of one detected
9
+ * note (fetches the note's creating output, tree state,
10
+ * and block cmus, then calls the prover).
11
+ *
12
+ * This module is transport- and prover-agnostic: it depends only on the
13
+ * `LightwalletdTransport` interface and a prover callback. That keeps the money
14
+ * invariant and marshalling here (bigint sats, checked crossings) while letting
15
+ * the caller decide where the wasm runs — directly in Node, or in a Web Worker
16
+ * in the browser (spend proving is ~5–20 s; detection is milliseconds).
17
+ *
18
+ * Proven reference: the fully-lightwalletd-sourced z→z (txid 07e3b38e…f996) and
19
+ * the note-detection cross-check whose predicted nullifier matched that spend's
20
+ * on-chain nullifier byte-for-byte.
21
+ */
22
+ import type { LightwalletdTransport } from './lightwalletd.js';
23
+ import type { DetectedNoteRaw } from './wasm.js';
24
+ /** A viewing/spending key for detection: an extended spending key (169-byte hex)
25
+ * or a diversifiable full viewing key (128-byte hex, read-only). */
26
+ export type DetectKey = {
27
+ extskHex: string;
28
+ } | {
29
+ dfvkHex: string;
30
+ };
31
+ /** Trial-decrypt the compact outputs (read path). Returns raw detected notes. */
32
+ export type DetectProver = (specJson: string) => DetectedNoteRaw[] | Promise<DetectedNoteRaw[]>;
33
+ /** Prove + sign a shielded spend. Returns the signed transaction hex. The caller
34
+ * binds the proving parameters (and may run this in a Web Worker). */
35
+ export type SpendProver = (specJson: string) => string | Promise<string>;
36
+ export interface DetectNotesParams {
37
+ readonly key: DetectKey;
38
+ /** First block to scan (inclusive). Use the wallet's birthday height. */
39
+ readonly fromHeight: number;
40
+ /** Last block to scan (inclusive). Defaults to the chain tip. */
41
+ readonly toHeight?: number;
42
+ }
43
+ /** A spendable note the wallet owns, with everything a later spend needs. */
44
+ export interface SpendableNote {
45
+ /** Creating transaction id (display/big-endian hex). */
46
+ readonly txid: string;
47
+ readonly outputIndex: number;
48
+ readonly height: number;
49
+ /** 0-based leaf position in the note-commitment tree. */
50
+ readonly position: number;
51
+ readonly valueSats: bigint;
52
+ /** 43-byte Sapling payment address the note pays to (hex). */
53
+ readonly recipientHex: string;
54
+ /** Note nullifier (hex) — its appearance in a block marks the note spent. */
55
+ readonly nullifierHex: string;
56
+ }
57
+ /**
58
+ * Find the wallet's own notes in `[fromHeight, toHeight]` by trial-decryption,
59
+ * excluding notes whose nullifier is spent within the same range.
60
+ *
61
+ * Sources everything from `transport`: the tree state before the range (for
62
+ * absolute positions) plus every compact block in the range. `prover` is the
63
+ * wasm `detectNotes` (cheap; may run on the main thread).
64
+ *
65
+ * Note: spends are only observed within the scanned range. Scan from the wallet
66
+ * birthday to the tip for a complete unspent set.
67
+ */
68
+ export declare function detectNotes(transport: LightwalletdTransport, prover: DetectProver, params: DetectNotesParams): Promise<SpendableNote[]>;
69
+ /** A shielded (z) recipient of a spend. */
70
+ export interface ShieldedRecipient {
71
+ readonly address: string;
72
+ readonly valueSats: bigint;
73
+ /** UTF-8 text memo (≤ 512 bytes). Ignored if `memoHex` is set. */
74
+ readonly memo?: string;
75
+ /** Raw memo bytes as hex (≤ 512 bytes) — for structured/binary memos (e.g. a
76
+ * messenger frame). Takes precedence over `memo`. */
77
+ readonly memoHex?: string;
78
+ }
79
+ /** A transparent recipient of a spend, by raw output script. */
80
+ export interface TransparentRecipient {
81
+ readonly scriptHex: string;
82
+ readonly valueSats: bigint;
83
+ }
84
+ export interface BuildShieldedSpendParams {
85
+ /** The note to spend, from `detectNotes` (its value + spending key). */
86
+ readonly note: {
87
+ readonly txid: string;
88
+ readonly outputIndex: number;
89
+ readonly extskHex: string;
90
+ /** The note's value in satoshis (from the detected `SpendableNote`). Used to
91
+ * enforce value conservation before signing. */
92
+ readonly valueSats: bigint;
93
+ };
94
+ readonly shieldedOutputs?: readonly ShieldedRecipient[];
95
+ readonly transparentOutputs?: readonly TransparentRecipient[];
96
+ /** The miner fee in satoshis. **Required and explicit**: the fee is the note
97
+ * value minus every output, so an unstated fee is silently whatever is left
98
+ * over — a forgotten change output would donate it to miners. This value must
99
+ * equal `note.valueSats − Σ outputs`, and is bounded by `maxFeeSats`. */
100
+ readonly feeSats: bigint;
101
+ /** Sanity ceiling on `feeSats` to catch a mis-computed fee (default 0.1 coin). */
102
+ readonly maxFeeSats?: bigint;
103
+ /** nExpiryHeight. Defaults to chain tip + 40. */
104
+ readonly expiryHeight?: number;
105
+ /** Consensus branch id. Defaults to Verus `CONSENSUS_BRANCH_ID` (0x76b809bb). */
106
+ readonly branchId?: number;
107
+ }
108
+ /**
109
+ * Build + sign a z→z / z→t spend of a single detected note. Fetches the note's
110
+ * creating output (via `getTransaction`), the tree state at its block − 1, and
111
+ * that block's ordered cmus (to place the note), assembles the prover spec, and
112
+ * runs `prover` (wasm `spendShielded`, typically in a Web Worker). Returns the
113
+ * broadcastable transaction hex.
114
+ *
115
+ * Value conservation is enforced HERE, before signing: `note.valueSats` must
116
+ * equal Σ shielded-output + Σ transparent-output + `feeSats`. The daemon only
117
+ * rejects a *negative* fee at broadcast; a positive overshoot (e.g. a forgotten
118
+ * change output) is a valid, eagerly-mined transaction that donates the excess
119
+ * to miners. Requiring an explicit `feeSats` and checking the balance turns that
120
+ * silent loss into a caught error.
121
+ */
122
+ export declare function buildShieldedSpend(transport: LightwalletdTransport, prover: SpendProver, params: BuildShieldedSpendParams): Promise<{
123
+ hex: string;
124
+ }>;