@chainvue/verus-sapling 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +43 -10
- package/crate/pkg/verus_sapling_prover.d.ts +9 -0
- package/crate/pkg/verus_sapling_prover.js +29 -0
- package/crate/pkg/verus_sapling_prover_bg.wasm +0 -0
- package/crate/pkg/verus_sapling_prover_bg.wasm.d.ts +1 -0
- package/dist/derive.d.ts +88 -0
- package/dist/derive.js +99 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +5 -2
- package/dist/wasm.d.ts +21 -2
- package/dist/wasm.js +11 -1
- package/dist/zaddr.d.ts +11 -0
- package/dist/zaddr.js +24 -0
- package/package.json +9 -10
package/LICENSE
CHANGED
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
same "printed page" as the copyright notice for easier
|
|
188
188
|
identification within third-party archives.
|
|
189
189
|
|
|
190
|
-
Copyright
|
|
190
|
+
Copyright 2026 Robert Lech
|
|
191
191
|
|
|
192
192
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
193
|
you may not use this file except in compliance with the License.
|
package/README.md
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
# @chainvue/verus-sapling
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<img alt="types" src="https://img.shields.io/badge/types-included-3178c6?logo=typescript&logoColor=white">
|
|
8
|
-
<a href="./LICENSE"><img alt="license" src="https://img.shields.io/badge/license-Apache--2.0-blue"></a>
|
|
9
|
-
<img alt="offline" src="https://img.shields.io/badge/network-none%20in%20signer-success">
|
|
10
|
-
</p>
|
|
3
|
+
[](https://www.npmjs.com/package/@chainvue/verus-sapling)
|
|
4
|
+
[](https://github.com/chainvue/verus-sapling/actions/workflows/ci.yml)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
[](https://nodejs.org)
|
|
11
7
|
|
|
12
8
|
> **Offline signing of Verus _shielded_ (Sapling) transactions** — `t→z`, `z→z`,
|
|
13
9
|
> `z→t`, with encrypted memos. It builds and signs bytes; your consumer
|
|
@@ -101,6 +97,44 @@ ZIP-212 enforcement is `Off`. The only Verus-specific value in the entire path i
|
|
|
101
97
|
that branch id, injected into the sighash. Even the lightwalletd wire protocol is
|
|
102
98
|
stock — `VerusCoin/lightwalletd`'s protos are byte-identical to Zcash's.
|
|
103
99
|
|
|
100
|
+
### Recovery phrases: one phrase, two unrelated key schedules
|
|
101
|
+
|
|
102
|
+
A Verus Mobile wallet derives **both** of its keys from a single recovery
|
|
103
|
+
phrase — by two derivations that share nothing:
|
|
104
|
+
|
|
105
|
+
| | transparent (`R…`) | shielded (`zs…`) |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| derivation | `sha256(utf8(phrase))` + Agama/Iguana clamp | BIP-39 → ZIP-32 |
|
|
108
|
+
| BIP-39 | ignored — the phrase is hashed as text | required, real PBKDF2 |
|
|
109
|
+
| path | none — one key per phrase, no HD | `m/32'/coin'/account'` |
|
|
110
|
+
| network | mainnet ≡ testnet | coin type 133 on **both** (see below) |
|
|
111
|
+
| lives in | [`@chainvue/verus-sdk`](https://www.npmjs.com/package/@chainvue/verus-sdk) `keys.seedToWif` | `deriveSaplingAccount` (here) |
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { deriveSaplingAccount, initSapling } from '@chainvue/verus-sapling';
|
|
115
|
+
|
|
116
|
+
await initSapling(wasmBytes);
|
|
117
|
+
const account = await deriveSaplingAccount({ mnemonic: phrase }); // coinType 133, account 0
|
|
118
|
+
account.address; // zs… — compare against your wallet before relying on it
|
|
119
|
+
account.extskHex; // spending key, the form every builder here takes
|
|
120
|
+
account.dfvkHex; // viewing key — enough to scan, cannot spend
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Consequences worth internalizing: the phrase must be a **valid BIP-39 mnemonic**
|
|
124
|
+
for a z-address to exist at all (Verus Mobile gates its shielded account on
|
|
125
|
+
`validateMnemonic()` + ≥12 words, while the transparent side accepts any string),
|
|
126
|
+
so the same words produce unrelated keys on the two sides.
|
|
127
|
+
|
|
128
|
+
**Coin type 133 on both networks — including VRSCTEST.** Verus Mobile's
|
|
129
|
+
`parseDlightSeed` calls `Tools.deriveSaplingSpendingKey(seed)` with no network
|
|
130
|
+
argument, so the Kotlin bridge's `networks.getOrDefault(network,
|
|
131
|
+
ZcashNetwork.Mainnet)` falls back to mainnet and a testnet wallet holds a
|
|
132
|
+
mainnet-path key. Verified 2026-07-28 against a live VRSCTEST wallet: of five
|
|
133
|
+
candidate paths, only `m/32'/133'/0'` reproduced the address the app shows. The
|
|
134
|
+
default here matches that; `COIN_TYPE_VRSCTEST` (1) exists for stock-zcash
|
|
135
|
+
tooling and derives a key no Verus Mobile wallet holds. The address HRP is `zs`
|
|
136
|
+
on both networks either way.
|
|
137
|
+
|
|
104
138
|
### Backend: the one unavoidable dependency
|
|
105
139
|
|
|
106
140
|
- **`t→z` (shielding)** needs **no** commitment-tree witness — only the
|
|
@@ -177,8 +211,7 @@ test/ vitest suite (money, hex, protobuf, gRPC-web, wallet, zaddr, …
|
|
|
177
211
|
```
|
|
178
212
|
|
|
179
213
|
Docs: [SECURITY](SECURITY.md) · [CONTRIBUTING](CONTRIBUTING.md) ·
|
|
180
|
-
[RELEASING](RELEASING.md) · [NOTICE](NOTICE)
|
|
181
|
-
[Code of Conduct](CODE_OF_CONDUCT.md)
|
|
214
|
+
[RELEASING](RELEASING.md) · [NOTICE](NOTICE)
|
|
182
215
|
|
|
183
216
|
## Contributing
|
|
184
217
|
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
/* tslint:disable */
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Derive a Sapling account (spending key, viewing key, default address) from a
|
|
6
|
+
* BIP-39 seed via ZIP-32 `m/32'/coin_type'/account'`. `spec_json` is the
|
|
7
|
+
* derivation request (see `json_api::derive_account_from_json`). No params, no
|
|
8
|
+
* proving — cheap, safe on the main thread.
|
|
9
|
+
*/
|
|
10
|
+
export function derive_account(spec_json: string): string;
|
|
11
|
+
|
|
4
12
|
/**
|
|
5
13
|
* Detect the wallet's own notes by trial-decrypting compact outputs (read path
|
|
6
14
|
* — no params, no proving). `spec_json` is the note-detection request (see
|
|
@@ -34,6 +42,7 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
|
|
|
34
42
|
|
|
35
43
|
export interface InitOutput {
|
|
36
44
|
readonly memory: WebAssembly.Memory;
|
|
45
|
+
readonly derive_account: (a: number, b: number) => [number, number, number, number];
|
|
37
46
|
readonly detect_notes: (a: number, b: number) => [number, number, number, number];
|
|
38
47
|
readonly read_note: (a: number, b: number) => [number, number, number, number];
|
|
39
48
|
readonly shield_t2z: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number];
|
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
/* @ts-self-types="./verus_sapling_prover.d.ts" */
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Derive a Sapling account (spending key, viewing key, default address) from a
|
|
5
|
+
* BIP-39 seed via ZIP-32 `m/32'/coin_type'/account'`. `spec_json` is the
|
|
6
|
+
* derivation request (see `json_api::derive_account_from_json`). No params, no
|
|
7
|
+
* proving — cheap, safe on the main thread.
|
|
8
|
+
* @param {string} spec_json
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
export function derive_account(spec_json) {
|
|
12
|
+
let deferred3_0;
|
|
13
|
+
let deferred3_1;
|
|
14
|
+
try {
|
|
15
|
+
const ptr0 = passStringToWasm0(spec_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
16
|
+
const len0 = WASM_VECTOR_LEN;
|
|
17
|
+
const ret = wasm.derive_account(ptr0, len0);
|
|
18
|
+
var ptr2 = ret[0];
|
|
19
|
+
var len2 = ret[1];
|
|
20
|
+
if (ret[3]) {
|
|
21
|
+
ptr2 = 0; len2 = 0;
|
|
22
|
+
throw takeFromExternrefTable0(ret[2]);
|
|
23
|
+
}
|
|
24
|
+
deferred3_0 = ptr2;
|
|
25
|
+
deferred3_1 = len2;
|
|
26
|
+
return getStringFromWasm0(ptr2, len2);
|
|
27
|
+
} finally {
|
|
28
|
+
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
3
32
|
/**
|
|
4
33
|
* Detect the wallet's own notes by trial-decrypting compact outputs (read path
|
|
5
34
|
* — no params, no proving). `spec_json` is the note-detection request (see
|
|
Binary file
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* tslint:disable */
|
|
2
2
|
/* eslint-disable */
|
|
3
3
|
export const memory: WebAssembly.Memory;
|
|
4
|
+
export const derive_account: (a: number, b: number) => [number, number, number, number];
|
|
4
5
|
export const detect_notes: (a: number, b: number) => [number, number, number, number];
|
|
5
6
|
export const read_note: (a: number, b: number) => [number, number, number, number];
|
|
6
7
|
export const shield_t2z: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number, number, number];
|
package/dist/derive.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shielded key derivation from a recovery phrase — BIP-39 → ZIP-32 Sapling.
|
|
3
|
+
*
|
|
4
|
+
* A Verus Mobile wallet derives BOTH of its keys from one phrase, by two
|
|
5
|
+
* completely unrelated schedules:
|
|
6
|
+
*
|
|
7
|
+
* transparent (R-address) sha256(utf8(phrase)) + Agama/Iguana clamp.
|
|
8
|
+
* No BIP-39, no BIP-32, one key per phrase.
|
|
9
|
+
* Lives in `@chainvue/verus-sdk` (`keys.seedToWif`).
|
|
10
|
+
* shielded (z-address) BIP-39 → 64-byte seed → ZIP-32 `m/32'/coin'/account'`.
|
|
11
|
+
* This module.
|
|
12
|
+
*
|
|
13
|
+
* Consequences worth knowing before you wire this into a wallet:
|
|
14
|
+
*
|
|
15
|
+
* - The phrase must be a REAL BIP-39 mnemonic for a z-address to exist. Verus
|
|
16
|
+
* Mobile gates its shielded account on `validateMnemonic()` + ≥12 words; an
|
|
17
|
+
* arbitrary passphrase yields a transparent-only wallet. The transparent
|
|
18
|
+
* side, by contrast, accepts any string at all.
|
|
19
|
+
* - `coinType` is 133 for a Verus Mobile wallet on BOTH networks — including
|
|
20
|
+
* VRSCTEST. That is not a typo: the app's `parseDlightSeed` calls
|
|
21
|
+
* `Tools.deriveSaplingSpendingKey(seed)` with NO network argument, so the
|
|
22
|
+
* Kotlin bridge falls back to `networks.getOrDefault(network,
|
|
23
|
+
* ZcashNetwork.Mainnet)` and a testnet wallet ends up holding a mainnet-path
|
|
24
|
+
* key. Confirmed 2026-07-28 against a live VRSCTEST wallet: only
|
|
25
|
+
* `m/32'/133'/0'` reproduced the address the app displays. The stock-zcash
|
|
26
|
+
* testnet coin type (1) is exported for other tooling, but derives a key no
|
|
27
|
+
* Verus Mobile wallet holds.
|
|
28
|
+
* - The address has no network split either — Verus emits `zs` on both.
|
|
29
|
+
* - This derives keys; it does not validate the BIP-39 checksum (that needs the
|
|
30
|
+
* 2048-word list, which this package deliberately does not carry). A typo'd
|
|
31
|
+
* phrase derives a valid but empty wallet — compare the address against your
|
|
32
|
+
* wallet before relying on it.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* ZIP-32 coin type for Verus (stock Zcash mainnet value; the Verus librustzcash
|
|
36
|
+
* fork leaves it unmodified). This is what Verus Mobile uses on **both**
|
|
37
|
+
* networks — see the note above — so it is the right default for VRSCTEST too.
|
|
38
|
+
*/
|
|
39
|
+
export declare const COIN_TYPE_VRSC = 133;
|
|
40
|
+
/**
|
|
41
|
+
* Stock-zcash testnet coin type. Exported for interoperating with tooling that
|
|
42
|
+
* follows the network split; a Verus Mobile wallet does NOT hold a key at this
|
|
43
|
+
* path, not even on VRSCTEST.
|
|
44
|
+
*/
|
|
45
|
+
export declare const COIN_TYPE_VRSCTEST = 1;
|
|
46
|
+
/**
|
|
47
|
+
* BIP-39 mnemonic → 64-byte seed: PBKDF2-HMAC-SHA512, 2048 iterations, salt
|
|
48
|
+
* `"mnemonic" + passphrase`, both NFKD-normalized. Exactly what Verus Mobile's
|
|
49
|
+
* `SeedPhrase.toByteArray()` (kotlin-bip39 `MnemonicCode.toSeed()`) computes.
|
|
50
|
+
*
|
|
51
|
+
* WebCrypto, so it works unchanged in Node and the browser.
|
|
52
|
+
*/
|
|
53
|
+
export declare function mnemonicToSeed(mnemonic: string, passphrase?: string): Promise<Uint8Array>;
|
|
54
|
+
/** A derived Sapling account. */
|
|
55
|
+
export interface SaplingAccount {
|
|
56
|
+
/** `zs…` payment address — compare this against your wallet. */
|
|
57
|
+
address: string;
|
|
58
|
+
/** Raw 43-byte payment address, hex (what the JSON specs take). */
|
|
59
|
+
addressHex: string;
|
|
60
|
+
/** ZIP-32 extended spending key, hex — `extskHex` everywhere else here. CAN SPEND. */
|
|
61
|
+
extskHex: string;
|
|
62
|
+
/** Diversifiable full viewing key, hex — enough to scan, cannot spend. */
|
|
63
|
+
dfvkHex: string;
|
|
64
|
+
/** Diversifier index the default address was found at. */
|
|
65
|
+
diversifierIndexHex: string;
|
|
66
|
+
}
|
|
67
|
+
export interface DeriveSaplingAccountParams {
|
|
68
|
+
/** BIP-39 recovery phrase. Mutually exclusive with `seedHex`. */
|
|
69
|
+
mnemonic?: string;
|
|
70
|
+
/** Optional BIP-39 passphrase ("25th word"). Verus Mobile does not use one. */
|
|
71
|
+
passphrase?: string;
|
|
72
|
+
/** A 64-byte BIP-39 seed, hex, if you did the mnemonic step yourself. */
|
|
73
|
+
seedHex?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Defaults to 133 — what Verus Mobile derives with on BOTH VRSC and VRSCTEST.
|
|
76
|
+
* Pass 1 only for stock-zcash tooling that honours the testnet split.
|
|
77
|
+
*/
|
|
78
|
+
coinType?: number;
|
|
79
|
+
/** Defaults to 0 — the account Verus Mobile uses (`Account.DEFAULT`). */
|
|
80
|
+
account?: number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Derive `m/32'/coinType'/account'` and its default payment address.
|
|
84
|
+
*
|
|
85
|
+
* Requires the wasm module: call `initSapling()` first. Cheap — no proving
|
|
86
|
+
* parameters, no Groth16, safe on the main thread.
|
|
87
|
+
*/
|
|
88
|
+
export declare function deriveSaplingAccount(params: DeriveSaplingAccountParams): Promise<SaplingAccount>;
|
package/dist/derive.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shielded key derivation from a recovery phrase — BIP-39 → ZIP-32 Sapling.
|
|
3
|
+
*
|
|
4
|
+
* A Verus Mobile wallet derives BOTH of its keys from one phrase, by two
|
|
5
|
+
* completely unrelated schedules:
|
|
6
|
+
*
|
|
7
|
+
* transparent (R-address) sha256(utf8(phrase)) + Agama/Iguana clamp.
|
|
8
|
+
* No BIP-39, no BIP-32, one key per phrase.
|
|
9
|
+
* Lives in `@chainvue/verus-sdk` (`keys.seedToWif`).
|
|
10
|
+
* shielded (z-address) BIP-39 → 64-byte seed → ZIP-32 `m/32'/coin'/account'`.
|
|
11
|
+
* This module.
|
|
12
|
+
*
|
|
13
|
+
* Consequences worth knowing before you wire this into a wallet:
|
|
14
|
+
*
|
|
15
|
+
* - The phrase must be a REAL BIP-39 mnemonic for a z-address to exist. Verus
|
|
16
|
+
* Mobile gates its shielded account on `validateMnemonic()` + ≥12 words; an
|
|
17
|
+
* arbitrary passphrase yields a transparent-only wallet. The transparent
|
|
18
|
+
* side, by contrast, accepts any string at all.
|
|
19
|
+
* - `coinType` is 133 for a Verus Mobile wallet on BOTH networks — including
|
|
20
|
+
* VRSCTEST. That is not a typo: the app's `parseDlightSeed` calls
|
|
21
|
+
* `Tools.deriveSaplingSpendingKey(seed)` with NO network argument, so the
|
|
22
|
+
* Kotlin bridge falls back to `networks.getOrDefault(network,
|
|
23
|
+
* ZcashNetwork.Mainnet)` and a testnet wallet ends up holding a mainnet-path
|
|
24
|
+
* key. Confirmed 2026-07-28 against a live VRSCTEST wallet: only
|
|
25
|
+
* `m/32'/133'/0'` reproduced the address the app displays. The stock-zcash
|
|
26
|
+
* testnet coin type (1) is exported for other tooling, but derives a key no
|
|
27
|
+
* Verus Mobile wallet holds.
|
|
28
|
+
* - The address has no network split either — Verus emits `zs` on both.
|
|
29
|
+
* - This derives keys; it does not validate the BIP-39 checksum (that needs the
|
|
30
|
+
* 2048-word list, which this package deliberately does not carry). A typo'd
|
|
31
|
+
* phrase derives a valid but empty wallet — compare the address against your
|
|
32
|
+
* wallet before relying on it.
|
|
33
|
+
*/
|
|
34
|
+
import { ShieldedInputError } from './errors.js';
|
|
35
|
+
import { bytesToHex, hexToBytes } from './hex.js';
|
|
36
|
+
import { deriveAccountWasm } from './wasm.js';
|
|
37
|
+
import { encodeSaplingAddress } from './zaddr.js';
|
|
38
|
+
/**
|
|
39
|
+
* ZIP-32 coin type for Verus (stock Zcash mainnet value; the Verus librustzcash
|
|
40
|
+
* fork leaves it unmodified). This is what Verus Mobile uses on **both**
|
|
41
|
+
* networks — see the note above — so it is the right default for VRSCTEST too.
|
|
42
|
+
*/
|
|
43
|
+
export const COIN_TYPE_VRSC = 133;
|
|
44
|
+
/**
|
|
45
|
+
* Stock-zcash testnet coin type. Exported for interoperating with tooling that
|
|
46
|
+
* follows the network split; a Verus Mobile wallet does NOT hold a key at this
|
|
47
|
+
* path, not even on VRSCTEST.
|
|
48
|
+
*/
|
|
49
|
+
export const COIN_TYPE_VRSCTEST = 1;
|
|
50
|
+
/**
|
|
51
|
+
* BIP-39 mnemonic → 64-byte seed: PBKDF2-HMAC-SHA512, 2048 iterations, salt
|
|
52
|
+
* `"mnemonic" + passphrase`, both NFKD-normalized. Exactly what Verus Mobile's
|
|
53
|
+
* `SeedPhrase.toByteArray()` (kotlin-bip39 `MnemonicCode.toSeed()`) computes.
|
|
54
|
+
*
|
|
55
|
+
* WebCrypto, so it works unchanged in Node and the browser.
|
|
56
|
+
*/
|
|
57
|
+
export async function mnemonicToSeed(mnemonic, passphrase = '') {
|
|
58
|
+
if (typeof mnemonic !== 'string' || mnemonic.trim().length === 0) {
|
|
59
|
+
throw new ShieldedInputError('mnemonic is empty');
|
|
60
|
+
}
|
|
61
|
+
const enc = new TextEncoder();
|
|
62
|
+
const key = await globalThis.crypto.subtle.importKey('raw', enc.encode(mnemonic.normalize('NFKD')), 'PBKDF2', false, ['deriveBits']);
|
|
63
|
+
const bits = await globalThis.crypto.subtle.deriveBits({
|
|
64
|
+
name: 'PBKDF2',
|
|
65
|
+
salt: enc.encode(`mnemonic${passphrase.normalize('NFKD')}`),
|
|
66
|
+
iterations: 2048,
|
|
67
|
+
hash: 'SHA-512',
|
|
68
|
+
}, key, 512);
|
|
69
|
+
return new Uint8Array(bits);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Derive `m/32'/coinType'/account'` and its default payment address.
|
|
73
|
+
*
|
|
74
|
+
* Requires the wasm module: call `initSapling()` first. Cheap — no proving
|
|
75
|
+
* parameters, no Groth16, safe on the main thread.
|
|
76
|
+
*/
|
|
77
|
+
export async function deriveSaplingAccount(params) {
|
|
78
|
+
const { mnemonic, seedHex, passphrase = '', coinType = COIN_TYPE_VRSC, account = 0 } = params;
|
|
79
|
+
// Narrowed by the branches themselves rather than by a non-null assertion:
|
|
80
|
+
// exactly one input is allowed, and each branch reads the one it checked.
|
|
81
|
+
let seed;
|
|
82
|
+
if (mnemonic != null && seedHex == null) {
|
|
83
|
+
seed = await mnemonicToSeed(mnemonic, passphrase);
|
|
84
|
+
}
|
|
85
|
+
else if (seedHex != null && mnemonic == null) {
|
|
86
|
+
seed = hexToBytes(seedHex);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
throw new ShieldedInputError('pass exactly one of mnemonic or seedHex');
|
|
90
|
+
}
|
|
91
|
+
const derived = deriveAccountWasm(JSON.stringify({ seed_hex: bytesToHex(seed), coin_type: coinType, account }));
|
|
92
|
+
return {
|
|
93
|
+
address: encodeSaplingAddress(hexToBytes(derived.address_hex)),
|
|
94
|
+
addressHex: derived.address_hex,
|
|
95
|
+
extskHex: derived.extsk_hex,
|
|
96
|
+
dfvkHex: derived.dfvk_hex,
|
|
97
|
+
diversifierIndexHex: derived.diversifier_index_hex,
|
|
98
|
+
};
|
|
99
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -27,7 +27,8 @@ export * from './errors.js';
|
|
|
27
27
|
export * from './parse.js';
|
|
28
28
|
export { CONSENSUS_BRANCH_ID, parseSats, toSafeNumber } from './money.js';
|
|
29
29
|
export { bytesToHex, hexToBytes, reverseBytes } from './hex.js';
|
|
30
|
-
export { decodeSaplingAddress, saplingAddressToHex } from './zaddr.js';
|
|
31
|
-
export {
|
|
30
|
+
export { decodeSaplingAddress, saplingAddressToHex, encodeSaplingAddress } from './zaddr.js';
|
|
31
|
+
export { mnemonicToSeed, deriveSaplingAccount, COIN_TYPE_VRSC, COIN_TYPE_VRSCTEST, type SaplingAccount, type DeriveSaplingAccountParams, } from './derive.js';
|
|
32
|
+
export { initSapling, shieldT2z, spendShielded, detectNotes as detectNotesWasm, readNote, deriveAccountWasm, verifyCanonicalParams, PARAM_SHA256, type SaplingParams, type DetectedNoteRaw, type ReadNoteResult, type DerivedAccountRaw, } from './wasm.js';
|
|
32
33
|
export { detectNotes, buildShieldedSpend, type DetectKey, type DetectProver, type SpendProver, type DetectNotesParams, type SpendableNote, type BuildShieldedSpendParams, type ShieldedRecipient, type TransparentRecipient, } from './wallet.js';
|
|
33
34
|
export type { LightwalletdTransport, TreeState, CompactOutput, CompactTx, CompactBlock, LightdInfo, } from './lightwalletd.js';
|
package/dist/index.js
CHANGED
|
@@ -28,8 +28,11 @@ export * from './errors.js';
|
|
|
28
28
|
export * from './parse.js';
|
|
29
29
|
export { CONSENSUS_BRANCH_ID, parseSats, toSafeNumber } from './money.js';
|
|
30
30
|
export { bytesToHex, hexToBytes, reverseBytes } from './hex.js';
|
|
31
|
-
export { decodeSaplingAddress, saplingAddressToHex } from './zaddr.js';
|
|
31
|
+
export { decodeSaplingAddress, saplingAddressToHex, encodeSaplingAddress } from './zaddr.js';
|
|
32
|
+
// Shielded key derivation from a BIP-39 recovery phrase (ZIP-32 m/32'/coin'/account').
|
|
33
|
+
// `mnemonicToSeed` is pure; `deriveSaplingAccount` needs the wasm module.
|
|
34
|
+
export { mnemonicToSeed, deriveSaplingAccount, COIN_TYPE_VRSC, COIN_TYPE_VRSCTEST, } from './derive.js';
|
|
32
35
|
// 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';
|
|
36
|
+
export { initSapling, shieldT2z, spendShielded, detectNotes as detectNotesWasm, readNote, deriveAccountWasm, verifyCanonicalParams, PARAM_SHA256, } from './wasm.js';
|
|
34
37
|
// Shielded wallet orchestration (transport- and prover-agnostic).
|
|
35
38
|
export { detectNotes, buildShieldedSpend, } from './wallet.js';
|
package/dist/wasm.d.ts
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
/** SHA-256 of the canonical Zcash Sapling proving parameters (byte-identical for
|
|
16
16
|
* Verus). See NOTICE / README. */
|
|
17
17
|
export declare const PARAM_SHA256: {
|
|
18
|
-
readonly spend:
|
|
19
|
-
readonly output:
|
|
18
|
+
readonly spend: '8e48ffd23abb3a5fd9c5589204f32d9c31285a04b78096ba40a79b75677efc13';
|
|
19
|
+
readonly output: '2f0ebbcbb9bb0bcffe95a397e7eba89c29eb4dde6191c339db88570e3f3fb0e4';
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
22
|
* Verify caller-supplied proving parameters are the canonical Sapling params
|
|
@@ -89,3 +89,22 @@ export interface ReadNoteResult {
|
|
|
89
89
|
* Returns `null` if the output is not for this key. Cheap (no params, no proving).
|
|
90
90
|
*/
|
|
91
91
|
export declare function readNote(specJson: string): ReadNoteResult | null;
|
|
92
|
+
/** A ZIP-32-derived Sapling account, raw from the wasm boundary (all hex). */
|
|
93
|
+
export interface DerivedAccountRaw {
|
|
94
|
+
/** Extended spending key, 169 bytes. CAN SPEND. */
|
|
95
|
+
extsk_hex: string;
|
|
96
|
+
/** Diversifiable full viewing key, 128 bytes — scanning only. */
|
|
97
|
+
dfvk_hex: string;
|
|
98
|
+
/** Raw 43-byte payment address. */
|
|
99
|
+
address_hex: string;
|
|
100
|
+
/** 11-byte diversifier index the default address was found at. */
|
|
101
|
+
diversifier_index_hex: string;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Derive a Sapling account from a BIP-39 seed via ZIP-32
|
|
105
|
+
* `m/32'/coin_type'/account'` (see the Rust `json_api::derive_account_from_json`).
|
|
106
|
+
* No params, no proving — but the wasm module must be initialized. Prefer
|
|
107
|
+
* `deriveSaplingAccount` in `derive.ts`, which also does the BIP-39 step and
|
|
108
|
+
* bech32-encodes the address.
|
|
109
|
+
*/
|
|
110
|
+
export declare function deriveAccountWasm(specJson: string): DerivedAccountRaw;
|
package/dist/wasm.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
// The generated glue is ESM (wasm-pack, crate/pkg). In a bundler/browser, import
|
|
16
16
|
// the pkg directly.
|
|
17
|
-
import initWasm, { shield_t2z, spend_shielded, detect_notes, read_note, } from '../crate/pkg/verus_sapling_prover.js';
|
|
17
|
+
import initWasm, { shield_t2z, spend_shielded, detect_notes, read_note, derive_account, } from '../crate/pkg/verus_sapling_prover.js';
|
|
18
18
|
import { ShieldedError } from './errors.js';
|
|
19
19
|
let ready;
|
|
20
20
|
/** SHA-256 of the canonical Zcash Sapling proving parameters (byte-identical for
|
|
@@ -88,3 +88,13 @@ export function detectNotes(specJson) {
|
|
|
88
88
|
export function readNote(specJson) {
|
|
89
89
|
return JSON.parse(read_note(specJson));
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Derive a Sapling account from a BIP-39 seed via ZIP-32
|
|
93
|
+
* `m/32'/coin_type'/account'` (see the Rust `json_api::derive_account_from_json`).
|
|
94
|
+
* No params, no proving — but the wasm module must be initialized. Prefer
|
|
95
|
+
* `deriveSaplingAccount` in `derive.ts`, which also does the BIP-39 step and
|
|
96
|
+
* bech32-encodes the address.
|
|
97
|
+
*/
|
|
98
|
+
export function deriveAccountWasm(specJson) {
|
|
99
|
+
return JSON.parse(derive_account(specJson));
|
|
100
|
+
}
|
package/dist/zaddr.d.ts
CHANGED
|
@@ -17,3 +17,14 @@
|
|
|
17
17
|
export declare function decodeSaplingAddress(addr: string): Uint8Array;
|
|
18
18
|
/** Hex of the 43-byte payload (convenience for JSON specs). */
|
|
19
19
|
export declare function saplingAddressToHex(addr: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Encode a raw 43-byte Sapling payment address as a `zs…` bech32 address — the
|
|
22
|
+
* inverse of `decodeSaplingAddress`, for addresses that are DERIVED rather than
|
|
23
|
+
* pasted in (see `derive.ts`).
|
|
24
|
+
*
|
|
25
|
+
* The HRP defaults to `zs` for BOTH Verus networks, matching what a live Verus
|
|
26
|
+
* daemon emits (see the note at the top of this file): unlike stock zcash there
|
|
27
|
+
* is no `ztestsapling` on vrsctest. The parameter exists only to interoperate
|
|
28
|
+
* with stock-zcash tooling that still expects that split.
|
|
29
|
+
*/
|
|
30
|
+
export declare function encodeSaplingAddress(payload: Uint8Array, hrp?: 'zs' | 'ztestsapling'): string;
|
package/dist/zaddr.js
CHANGED
|
@@ -105,3 +105,27 @@ export function decodeSaplingAddress(addr) {
|
|
|
105
105
|
export function saplingAddressToHex(addr) {
|
|
106
106
|
return bytesToHex(decodeSaplingAddress(addr));
|
|
107
107
|
}
|
|
108
|
+
function bech32Encode(hrp, data) {
|
|
109
|
+
// Checksum over hrp || data || six zero symbols, xor 1 (bech32, not bech32m).
|
|
110
|
+
const checksum = bech32Polymod([...hrpExpand(hrp), ...data, 0, 0, 0, 0, 0, 0]) ^ 1;
|
|
111
|
+
const out = [...data];
|
|
112
|
+
for (let i = 0; i < 6; i++)
|
|
113
|
+
out.push((checksum >> (5 * (5 - i))) & 31);
|
|
114
|
+
return `${hrp}1${out.map((v) => CHARSET[v]).join('')}`;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Encode a raw 43-byte Sapling payment address as a `zs…` bech32 address — the
|
|
118
|
+
* inverse of `decodeSaplingAddress`, for addresses that are DERIVED rather than
|
|
119
|
+
* pasted in (see `derive.ts`).
|
|
120
|
+
*
|
|
121
|
+
* The HRP defaults to `zs` for BOTH Verus networks, matching what a live Verus
|
|
122
|
+
* daemon emits (see the note at the top of this file): unlike stock zcash there
|
|
123
|
+
* is no `ztestsapling` on vrsctest. The parameter exists only to interoperate
|
|
124
|
+
* with stock-zcash tooling that still expects that split.
|
|
125
|
+
*/
|
|
126
|
+
export function encodeSaplingAddress(payload, hrp = 'zs') {
|
|
127
|
+
if (payload.length !== 43) {
|
|
128
|
+
throw new Error(`expected 43-byte payload, got ${payload.length}`);
|
|
129
|
+
}
|
|
130
|
+
return bech32Encode(hrp, convertBits([...payload], 8, 5, true));
|
|
131
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chainvue/verus-sapling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Offline signing of Verus shielded (Sapling) transactions — t→z, z→z, z→t, with memo. Companion to @chainvue/verus-sdk. Builds and signs bytes; consumers broadcast. WASM prover, no full node on the signing host.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Robert Lech",
|
|
@@ -59,7 +59,6 @@
|
|
|
59
59
|
"build:ext": "tsc && node examples/extension/build.mjs",
|
|
60
60
|
"typecheck": "tsc --noEmit",
|
|
61
61
|
"test": "vitest run",
|
|
62
|
-
"lint": "eslint .",
|
|
63
62
|
"prepack": "npm run build"
|
|
64
63
|
},
|
|
65
64
|
"peerDependencies": {
|
|
@@ -67,20 +66,20 @@
|
|
|
67
66
|
},
|
|
68
67
|
"optionalDependencies": {
|
|
69
68
|
"@grpc/grpc-js": "^1.10.0",
|
|
70
|
-
"@grpc/proto-loader": "^0.
|
|
69
|
+
"@grpc/proto-loader": "^0.8.1"
|
|
71
70
|
},
|
|
72
71
|
"devDependencies": {
|
|
73
72
|
"@chainvue/verus-sdk": ">=0.14.0",
|
|
74
73
|
"@grpc/grpc-js": "^1.10.0",
|
|
75
|
-
"@grpc/proto-loader": "^0.
|
|
76
|
-
"@semantic-release/changelog": "^
|
|
74
|
+
"@grpc/proto-loader": "^0.8.1",
|
|
75
|
+
"@semantic-release/changelog": "^7.0.0",
|
|
77
76
|
"@semantic-release/exec": "^7.1.0",
|
|
78
|
-
"@semantic-release/git": "^
|
|
79
|
-
"@types/node": "^
|
|
80
|
-
"esbuild": "^0.
|
|
77
|
+
"@semantic-release/git": "^11.0.0",
|
|
78
|
+
"@types/node": "^26.1.1",
|
|
79
|
+
"esbuild": "^0.28.1",
|
|
81
80
|
"semantic-release": "^25.0.7",
|
|
82
|
-
"typescript": "^
|
|
83
|
-
"vitest": "^
|
|
81
|
+
"typescript": "^7.0.2",
|
|
82
|
+
"vitest": "^4.1.10"
|
|
84
83
|
},
|
|
85
84
|
"//": "The concrete lightwalletd gRPC client (@chainvue/verus-sapling/lightwalletd) needs @grpc/grpc-js — an OPTIONAL dependency: the browser path implements LightwalletdTransport over gRPC-web instead, and the package root pulls in no gRPC at runtime.",
|
|
86
85
|
"publishConfig": {
|