@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.
- package/LICENSE +202 -0
- package/NOTICE +74 -0
- package/README.md +194 -0
- package/crate/pkg/package.json +17 -0
- package/crate/pkg/verus_sapling_prover.d.ts +71 -0
- package/crate/pkg/verus_sapling_prover.js +451 -0
- package/crate/pkg/verus_sapling_prover_bg.wasm +0 -0
- package/crate/pkg/verus_sapling_prover_bg.wasm.d.ts +15 -0
- package/dist/browser/grpcweb.d.ts +29 -0
- package/dist/browser/grpcweb.js +149 -0
- package/dist/browser/index.d.ts +23 -0
- package/dist/browser/index.js +23 -0
- package/dist/browser/lightwalletd-web.d.ts +36 -0
- package/dist/browser/lightwalletd-web.js +186 -0
- package/dist/browser/protobuf.d.ts +38 -0
- package/dist/browser/protobuf.js +115 -0
- package/dist/browser/prover-worker.d.ts +18 -0
- package/dist/browser/prover-worker.js +61 -0
- package/dist/browser/worker-prover.d.ts +38 -0
- package/dist/browser/worker-prover.js +51 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +25 -0
- package/dist/hex.d.ts +11 -0
- package/dist/hex.js +32 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +35 -0
- package/dist/lightwalletd.d.ts +113 -0
- package/dist/lightwalletd.js +99 -0
- package/dist/money.d.ts +23 -0
- package/dist/money.js +40 -0
- package/dist/parse.d.ts +45 -0
- package/dist/parse.js +129 -0
- package/dist/types.d.ts +29 -0
- package/dist/types.js +13 -0
- package/dist/wallet.d.ts +124 -0
- package/dist/wallet.js +179 -0
- package/dist/wasm.d.ts +91 -0
- package/dist/wasm.js +90 -0
- package/dist/zaddr.d.ts +19 -0
- package/dist/zaddr.js +107 -0
- package/package.json +90 -0
- package/proto/compact_formats.proto +62 -0
- package/proto/darkside.proto +124 -0
- package/proto/service.proto +172 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser lightwalletd client — a `LightwalletdTransport` over gRPC-web (`fetch`)
|
|
3
|
+
* instead of Node `@grpc/grpc-js`. Point it at a gRPC-web proxy (grpcwebproxy /
|
|
4
|
+
* Envoy) in front of lightwalletd; over an SSH forward that is
|
|
5
|
+
* `http://localhost:8080`, in production a TLS tunnel hostname.
|
|
6
|
+
*
|
|
7
|
+
* Same interface as the Node `LightwalletdClient`, so `../wallet`'s
|
|
8
|
+
* `detectNotes` / `buildShieldedSpend` work unchanged in the extension. Message
|
|
9
|
+
* (de)serialization is hand-coded against the CompactTxStreamer protobuf field
|
|
10
|
+
* numbers with the minimal `./protobuf` codec — no protobuf runtime dependency.
|
|
11
|
+
*/
|
|
12
|
+
import type { CompactBlock, CompactOutput, CompactTx, LightwalletdTransport, TreeState } from '../lightwalletd.js';
|
|
13
|
+
/** Browser (gRPC-web) implementation of the lightwalletd transport. */
|
|
14
|
+
export declare class LightwalletdWebClient implements LightwalletdTransport {
|
|
15
|
+
private readonly baseUrl;
|
|
16
|
+
/** @param baseUrl gRPC-web proxy origin, e.g. "http://localhost:8080". */
|
|
17
|
+
constructor(baseUrl: string);
|
|
18
|
+
getLatestHeight(): Promise<number>;
|
|
19
|
+
getTreeState(height: number): Promise<TreeState>;
|
|
20
|
+
getTransaction(txidDisplayHex: string): Promise<{
|
|
21
|
+
data: Uint8Array;
|
|
22
|
+
height: string;
|
|
23
|
+
}>;
|
|
24
|
+
getBlockRange(start: number, end: number): AsyncGenerator<CompactBlock>;
|
|
25
|
+
sendTransaction(txHex: string): Promise<{
|
|
26
|
+
errorCode: number;
|
|
27
|
+
errorMessage: string;
|
|
28
|
+
}>;
|
|
29
|
+
/** gRPC-web has no channel to close; provided for interface parity. */
|
|
30
|
+
close(): void;
|
|
31
|
+
}
|
|
32
|
+
export declare function decodeCompactOutput(msg: Uint8Array): CompactOutput;
|
|
33
|
+
export declare function decodeCompactTx(msg: Uint8Array): CompactTx;
|
|
34
|
+
export declare function decodeCompactBlock(msg: Uint8Array): CompactBlock;
|
|
35
|
+
/** Convenience for hex txids from a decoded block (display/big-endian order). */
|
|
36
|
+
export declare function blockTxid(tx: CompactTx): string;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser lightwalletd client — a `LightwalletdTransport` over gRPC-web (`fetch`)
|
|
3
|
+
* instead of Node `@grpc/grpc-js`. Point it at a gRPC-web proxy (grpcwebproxy /
|
|
4
|
+
* Envoy) in front of lightwalletd; over an SSH forward that is
|
|
5
|
+
* `http://localhost:8080`, in production a TLS tunnel hostname.
|
|
6
|
+
*
|
|
7
|
+
* Same interface as the Node `LightwalletdClient`, so `../wallet`'s
|
|
8
|
+
* `detectNotes` / `buildShieldedSpend` work unchanged in the extension. Message
|
|
9
|
+
* (de)serialization is hand-coded against the CompactTxStreamer protobuf field
|
|
10
|
+
* numbers with the minimal `./protobuf` codec — no protobuf runtime dependency.
|
|
11
|
+
*/
|
|
12
|
+
import { bytesToHex, hexToBytes, reverseBytes } from '../hex.js';
|
|
13
|
+
import { ProtoReader, ProtoWriter } from './protobuf.js';
|
|
14
|
+
import { serverStream, unary } from './grpcweb.js';
|
|
15
|
+
const EMPTY = new Uint8Array(0);
|
|
16
|
+
/** BlockID { height=1 } — request for GetTreeState / nested in BlockRange. */
|
|
17
|
+
function blockId(height) {
|
|
18
|
+
return new ProtoWriter().varintField(1, height).finish();
|
|
19
|
+
}
|
|
20
|
+
/** Browser (gRPC-web) implementation of the lightwalletd transport. */
|
|
21
|
+
export class LightwalletdWebClient {
|
|
22
|
+
baseUrl;
|
|
23
|
+
/** @param baseUrl gRPC-web proxy origin, e.g. "http://localhost:8080". */
|
|
24
|
+
constructor(baseUrl) {
|
|
25
|
+
this.baseUrl = baseUrl;
|
|
26
|
+
}
|
|
27
|
+
async getLatestHeight() {
|
|
28
|
+
// GetLatestBlock(ChainSpec{}) -> BlockID{ height=1 }
|
|
29
|
+
const res = await unary(this.baseUrl, 'GetLatestBlock', EMPTY);
|
|
30
|
+
const r = new ProtoReader(res);
|
|
31
|
+
let height = 0;
|
|
32
|
+
while (!r.done) {
|
|
33
|
+
const { field, wire } = r.tag();
|
|
34
|
+
if (field === 1 && wire === 0)
|
|
35
|
+
height = r.varint();
|
|
36
|
+
else
|
|
37
|
+
r.skip(wire);
|
|
38
|
+
}
|
|
39
|
+
return height;
|
|
40
|
+
}
|
|
41
|
+
async getTreeState(height) {
|
|
42
|
+
// GetTreeState(BlockID{height}) -> TreeState{ network=1,height=2,hash=3,time=4,tree=5 }
|
|
43
|
+
const res = await unary(this.baseUrl, 'GetTreeState', blockId(height));
|
|
44
|
+
const r = new ProtoReader(res);
|
|
45
|
+
const out = { network: '', height: '0', hash: '', time: 0, tree: '' };
|
|
46
|
+
while (!r.done) {
|
|
47
|
+
const { field, wire } = r.tag();
|
|
48
|
+
if (field === 1 && wire === 2)
|
|
49
|
+
out.network = r.string();
|
|
50
|
+
else if (field === 2 && wire === 0)
|
|
51
|
+
out.height = String(r.varint());
|
|
52
|
+
else if (field === 3 && wire === 2)
|
|
53
|
+
out.hash = r.string();
|
|
54
|
+
else if (field === 4 && wire === 0)
|
|
55
|
+
out.time = r.varint();
|
|
56
|
+
else if (field === 5 && wire === 2)
|
|
57
|
+
out.tree = r.string();
|
|
58
|
+
else
|
|
59
|
+
r.skip(wire);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
async getTransaction(txidDisplayHex) {
|
|
64
|
+
// GetTransaction(TxFilter{ hash=3 }) -> RawTransaction{ data=1, height=2 }
|
|
65
|
+
const hashInternal = reverseBytes(hexToBytes(txidDisplayHex));
|
|
66
|
+
const req = new ProtoWriter().bytesField(3, hashInternal).finish();
|
|
67
|
+
const res = await unary(this.baseUrl, 'GetTransaction', req);
|
|
68
|
+
const r = new ProtoReader(res);
|
|
69
|
+
let data = EMPTY;
|
|
70
|
+
let height = '0';
|
|
71
|
+
while (!r.done) {
|
|
72
|
+
const { field, wire } = r.tag();
|
|
73
|
+
if (field === 1 && wire === 2)
|
|
74
|
+
data = Uint8Array.from(r.bytes());
|
|
75
|
+
else if (field === 2 && wire === 0)
|
|
76
|
+
height = String(r.varint());
|
|
77
|
+
else
|
|
78
|
+
r.skip(wire);
|
|
79
|
+
}
|
|
80
|
+
return { data, height };
|
|
81
|
+
}
|
|
82
|
+
async *getBlockRange(start, end) {
|
|
83
|
+
// GetBlockRange(BlockRange{ start=1:BlockID, end=2:BlockID }) -> stream CompactBlock
|
|
84
|
+
const req = new ProtoWriter().messageField(1, blockId(start)).messageField(2, blockId(end)).finish();
|
|
85
|
+
for await (const msg of serverStream(this.baseUrl, 'GetBlockRange', req)) {
|
|
86
|
+
yield decodeCompactBlock(msg);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async sendTransaction(txHex) {
|
|
90
|
+
// SendTransaction(RawTransaction{ data=1, height=2 }) -> SendResponse{ errorCode=1, errorMessage=2 }
|
|
91
|
+
const req = new ProtoWriter().bytesField(1, hexToBytes(txHex)).varintField(2, 0).finish();
|
|
92
|
+
const res = await unary(this.baseUrl, 'SendTransaction', req);
|
|
93
|
+
const r = new ProtoReader(res);
|
|
94
|
+
let errorCode = 0;
|
|
95
|
+
let errorMessage = '';
|
|
96
|
+
while (!r.done) {
|
|
97
|
+
const { field, wire } = r.tag();
|
|
98
|
+
if (field === 1 && wire === 0)
|
|
99
|
+
errorCode = r.varint();
|
|
100
|
+
else if (field === 2 && wire === 2)
|
|
101
|
+
errorMessage = r.string();
|
|
102
|
+
else
|
|
103
|
+
r.skip(wire);
|
|
104
|
+
}
|
|
105
|
+
// The transport retries a broadcast once on a dropped connection (see
|
|
106
|
+
// grpcweb `post`), which can re-submit a tx that already reached the daemon.
|
|
107
|
+
// A "transaction already known / in mempool" reply means the FIRST attempt
|
|
108
|
+
// succeeded — report success rather than a spurious failure.
|
|
109
|
+
if (errorCode !== 0 && /already[ -](in[ -]?mempool|known|in the mem)|txn-already|in the memory pool/i.test(errorMessage)) {
|
|
110
|
+
return { errorCode: 0, errorMessage: `already accepted: ${errorMessage}` };
|
|
111
|
+
}
|
|
112
|
+
return { errorCode, errorMessage };
|
|
113
|
+
}
|
|
114
|
+
/** gRPC-web has no channel to close; provided for interface parity. */
|
|
115
|
+
close() {
|
|
116
|
+
/* no-op */
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// The decoders below are exported for unit testing (see test/lightwalletd-web.test.ts).
|
|
120
|
+
// They are not part of the public `./browser` barrel.
|
|
121
|
+
export function decodeCompactOutput(msg) {
|
|
122
|
+
const r = new ProtoReader(msg);
|
|
123
|
+
const out = { cmu: EMPTY, epk: EMPTY, ciphertext: EMPTY };
|
|
124
|
+
while (!r.done) {
|
|
125
|
+
const { field, wire } = r.tag();
|
|
126
|
+
if (field === 1 && wire === 2)
|
|
127
|
+
out.cmu = Uint8Array.from(r.bytes());
|
|
128
|
+
else if (field === 2 && wire === 2)
|
|
129
|
+
out.epk = Uint8Array.from(r.bytes());
|
|
130
|
+
else if (field === 3 && wire === 2)
|
|
131
|
+
out.ciphertext = Uint8Array.from(r.bytes());
|
|
132
|
+
else
|
|
133
|
+
r.skip(wire);
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
export function decodeCompactTx(msg) {
|
|
138
|
+
const r = new ProtoReader(msg);
|
|
139
|
+
const tx = { index: '0', hash: EMPTY, spends: [], outputs: [] };
|
|
140
|
+
while (!r.done) {
|
|
141
|
+
const { field, wire } = r.tag();
|
|
142
|
+
if (field === 1 && wire === 0)
|
|
143
|
+
tx.index = String(r.varint());
|
|
144
|
+
else if (field === 2 && wire === 2)
|
|
145
|
+
tx.hash = Uint8Array.from(r.bytes());
|
|
146
|
+
else if (field === 4 && wire === 2) {
|
|
147
|
+
// CompactSpend{ nf=1 }
|
|
148
|
+
const sr = new ProtoReader(r.bytes());
|
|
149
|
+
let nf = EMPTY;
|
|
150
|
+
while (!sr.done) {
|
|
151
|
+
const t = sr.tag();
|
|
152
|
+
if (t.field === 1 && t.wire === 2)
|
|
153
|
+
nf = Uint8Array.from(sr.bytes());
|
|
154
|
+
else
|
|
155
|
+
sr.skip(t.wire);
|
|
156
|
+
}
|
|
157
|
+
tx.spends.push({ nf });
|
|
158
|
+
}
|
|
159
|
+
else if (field === 5 && wire === 2) {
|
|
160
|
+
tx.outputs.push(decodeCompactOutput(r.bytes()));
|
|
161
|
+
}
|
|
162
|
+
else
|
|
163
|
+
r.skip(wire);
|
|
164
|
+
}
|
|
165
|
+
return tx;
|
|
166
|
+
}
|
|
167
|
+
export function decodeCompactBlock(msg) {
|
|
168
|
+
const r = new ProtoReader(msg);
|
|
169
|
+
const block = { height: '0', hash: EMPTY, vtx: [] };
|
|
170
|
+
while (!r.done) {
|
|
171
|
+
const { field, wire } = r.tag();
|
|
172
|
+
if (field === 2 && wire === 0)
|
|
173
|
+
block.height = String(r.varint());
|
|
174
|
+
else if (field === 3 && wire === 2)
|
|
175
|
+
block.hash = Uint8Array.from(r.bytes());
|
|
176
|
+
else if (field === 7 && wire === 2)
|
|
177
|
+
block.vtx.push(decodeCompactTx(r.bytes()));
|
|
178
|
+
else
|
|
179
|
+
r.skip(wire);
|
|
180
|
+
}
|
|
181
|
+
return block;
|
|
182
|
+
}
|
|
183
|
+
/** Convenience for hex txids from a decoded block (display/big-endian order). */
|
|
184
|
+
export function blockTxid(tx) {
|
|
185
|
+
return bytesToHex(reverseBytes(tx.hash));
|
|
186
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal protobuf3 reader/writer — just the wire-format primitives the
|
|
3
|
+
* lightwalletd message codecs need (varint, length-delimited bytes/strings,
|
|
4
|
+
* embedded messages). Zero dependency, to keep the browser transport as light as
|
|
5
|
+
* the rest of the package.
|
|
6
|
+
*
|
|
7
|
+
* Only wire types 0 (varint) and 2 (length-delimited) are used by the
|
|
8
|
+
* CompactTxStreamer messages we touch; unknown fields are skipped. Field values
|
|
9
|
+
* stay within 2^53 (heights, zatoshi), so plain `number` varints are safe here.
|
|
10
|
+
*/
|
|
11
|
+
/** Sequential reader over a protobuf message body. */
|
|
12
|
+
export declare class ProtoReader {
|
|
13
|
+
private readonly buf;
|
|
14
|
+
private off;
|
|
15
|
+
constructor(buf: Uint8Array);
|
|
16
|
+
get done(): boolean;
|
|
17
|
+
varint(): number;
|
|
18
|
+
bytes(): Uint8Array;
|
|
19
|
+
string(): string;
|
|
20
|
+
/** Read a field tag → { field, wire }. */
|
|
21
|
+
tag(): {
|
|
22
|
+
field: number;
|
|
23
|
+
wire: number;
|
|
24
|
+
};
|
|
25
|
+
/** Skip a field of the given wire type (unknown fields). */
|
|
26
|
+
skip(wire: number): void;
|
|
27
|
+
}
|
|
28
|
+
/** Accumulating writer for a protobuf message body. */
|
|
29
|
+
export declare class ProtoWriter {
|
|
30
|
+
private readonly chunks;
|
|
31
|
+
private pushVarint;
|
|
32
|
+
private tag;
|
|
33
|
+
varintField(field: number, value: number): this;
|
|
34
|
+
bytesField(field: number, value: Uint8Array): this;
|
|
35
|
+
/** Embed a nested message (its already-serialized bytes) as a length-delimited field. */
|
|
36
|
+
messageField(field: number, value: Uint8Array): this;
|
|
37
|
+
finish(): Uint8Array;
|
|
38
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal protobuf3 reader/writer — just the wire-format primitives the
|
|
3
|
+
* lightwalletd message codecs need (varint, length-delimited bytes/strings,
|
|
4
|
+
* embedded messages). Zero dependency, to keep the browser transport as light as
|
|
5
|
+
* the rest of the package.
|
|
6
|
+
*
|
|
7
|
+
* Only wire types 0 (varint) and 2 (length-delimited) are used by the
|
|
8
|
+
* CompactTxStreamer messages we touch; unknown fields are skipped. Field values
|
|
9
|
+
* stay within 2^53 (heights, zatoshi), so plain `number` varints are safe here.
|
|
10
|
+
*/
|
|
11
|
+
const WIRE_VARINT = 0;
|
|
12
|
+
const WIRE_I64 = 1;
|
|
13
|
+
const WIRE_LEN = 2;
|
|
14
|
+
const WIRE_I32 = 5;
|
|
15
|
+
/** Sequential reader over a protobuf message body. */
|
|
16
|
+
export class ProtoReader {
|
|
17
|
+
buf;
|
|
18
|
+
off = 0;
|
|
19
|
+
constructor(buf) {
|
|
20
|
+
this.buf = buf;
|
|
21
|
+
}
|
|
22
|
+
get done() {
|
|
23
|
+
return this.off >= this.buf.length;
|
|
24
|
+
}
|
|
25
|
+
varint() {
|
|
26
|
+
let shift = 0;
|
|
27
|
+
let result = 0;
|
|
28
|
+
for (;;) {
|
|
29
|
+
if (this.off >= this.buf.length)
|
|
30
|
+
throw new RangeError('protobuf: varint past end');
|
|
31
|
+
const b = this.buf[this.off++];
|
|
32
|
+
result += (b & 0x7f) * 2 ** shift;
|
|
33
|
+
if ((b & 0x80) === 0)
|
|
34
|
+
break;
|
|
35
|
+
shift += 7;
|
|
36
|
+
if (shift > 49)
|
|
37
|
+
throw new RangeError('protobuf: varint too long');
|
|
38
|
+
}
|
|
39
|
+
// Values are server-controlled; above 2^53 float64 rounds silently. Reject
|
|
40
|
+
// rather than return an imprecise number (guards the money/height crossing).
|
|
41
|
+
if (result > Number.MAX_SAFE_INTEGER) {
|
|
42
|
+
throw new RangeError('protobuf: varint exceeds safe integer range');
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
bytes() {
|
|
47
|
+
const len = this.varint();
|
|
48
|
+
if (this.off + len > this.buf.length)
|
|
49
|
+
throw new RangeError('protobuf: bytes past end');
|
|
50
|
+
const out = this.buf.subarray(this.off, this.off + len);
|
|
51
|
+
this.off += len;
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
string() {
|
|
55
|
+
return new TextDecoder().decode(this.bytes());
|
|
56
|
+
}
|
|
57
|
+
/** Read a field tag → { field, wire }. */
|
|
58
|
+
tag() {
|
|
59
|
+
const key = this.varint();
|
|
60
|
+
return { field: key >>> 3, wire: key & 0x7 };
|
|
61
|
+
}
|
|
62
|
+
/** Skip a field of the given wire type (unknown fields). */
|
|
63
|
+
skip(wire) {
|
|
64
|
+
switch (wire) {
|
|
65
|
+
case WIRE_VARINT:
|
|
66
|
+
this.varint();
|
|
67
|
+
break;
|
|
68
|
+
case WIRE_LEN:
|
|
69
|
+
this.bytes();
|
|
70
|
+
break;
|
|
71
|
+
case WIRE_I64:
|
|
72
|
+
this.off += 8;
|
|
73
|
+
break;
|
|
74
|
+
case WIRE_I32:
|
|
75
|
+
this.off += 4;
|
|
76
|
+
break;
|
|
77
|
+
default:
|
|
78
|
+
throw new RangeError(`protobuf: unknown wire type ${wire}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Accumulating writer for a protobuf message body. */
|
|
83
|
+
export class ProtoWriter {
|
|
84
|
+
chunks = [];
|
|
85
|
+
pushVarint(n) {
|
|
86
|
+
let v = n;
|
|
87
|
+
while (v > 0x7f) {
|
|
88
|
+
this.chunks.push((v & 0x7f) | 0x80);
|
|
89
|
+
v = Math.floor(v / 128);
|
|
90
|
+
}
|
|
91
|
+
this.chunks.push(v & 0x7f);
|
|
92
|
+
}
|
|
93
|
+
tag(field, wire) {
|
|
94
|
+
this.pushVarint(field * 8 + wire);
|
|
95
|
+
}
|
|
96
|
+
varintField(field, value) {
|
|
97
|
+
this.tag(field, WIRE_VARINT);
|
|
98
|
+
this.pushVarint(value);
|
|
99
|
+
return this;
|
|
100
|
+
}
|
|
101
|
+
bytesField(field, value) {
|
|
102
|
+
this.tag(field, WIRE_LEN);
|
|
103
|
+
this.pushVarint(value.length);
|
|
104
|
+
for (const b of value)
|
|
105
|
+
this.chunks.push(b);
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
/** Embed a nested message (its already-serialized bytes) as a length-delimited field. */
|
|
109
|
+
messageField(field, value) {
|
|
110
|
+
return this.bytesField(field, value);
|
|
111
|
+
}
|
|
112
|
+
finish() {
|
|
113
|
+
return Uint8Array.from(this.chunks);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Worker entry for Sapling proving. The spend prove is ~20 s single-threaded
|
|
3
|
+
* — running it on the main thread would freeze the extension UI, so it lives
|
|
4
|
+
* here. Detection is cheap but is offered too, so a caller can keep ALL wasm off
|
|
5
|
+
* the main thread if it wants.
|
|
6
|
+
*
|
|
7
|
+
* Protocol (postMessage):
|
|
8
|
+
* → { type:'init', wasmBytes, spendParams, outputParams } ⇒ { type:'ready' }
|
|
9
|
+
* → { type:'spend', id, spec } ⇒ { type:'result', id, value } | { type:'error', id, message }
|
|
10
|
+
* → { type:'detect', id, spec } ⇒ { type:'result', id, value } | { type:'error', id, message }
|
|
11
|
+
*
|
|
12
|
+
* Bundle this as a module worker:
|
|
13
|
+
* new Worker(new URL('./prover-worker.js', import.meta.url), { type: 'module' })
|
|
14
|
+
*
|
|
15
|
+
* The params (~50 MB) are transferred in once at init and held for every prove,
|
|
16
|
+
* so they cross the worker boundary only once.
|
|
17
|
+
*/
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Worker entry for Sapling proving. The spend prove is ~20 s single-threaded
|
|
3
|
+
* — running it on the main thread would freeze the extension UI, so it lives
|
|
4
|
+
* here. Detection is cheap but is offered too, so a caller can keep ALL wasm off
|
|
5
|
+
* the main thread if it wants.
|
|
6
|
+
*
|
|
7
|
+
* Protocol (postMessage):
|
|
8
|
+
* → { type:'init', wasmBytes, spendParams, outputParams } ⇒ { type:'ready' }
|
|
9
|
+
* → { type:'spend', id, spec } ⇒ { type:'result', id, value } | { type:'error', id, message }
|
|
10
|
+
* → { type:'detect', id, spec } ⇒ { type:'result', id, value } | { type:'error', id, message }
|
|
11
|
+
*
|
|
12
|
+
* Bundle this as a module worker:
|
|
13
|
+
* new Worker(new URL('./prover-worker.js', import.meta.url), { type: 'module' })
|
|
14
|
+
*
|
|
15
|
+
* The params (~50 MB) are transferred in once at init and held for every prove,
|
|
16
|
+
* so they cross the worker boundary only once.
|
|
17
|
+
*/
|
|
18
|
+
import { detectNotes, initSapling, readNote, spendShielded, verifyCanonicalParams, } from '../wasm.js';
|
|
19
|
+
// Minimal worker-global shape — avoids pulling in the `webworker` lib (which
|
|
20
|
+
// conflicts with `dom`'s `self`) while staying type-safe about what we use.
|
|
21
|
+
const ctx = globalThis;
|
|
22
|
+
let params;
|
|
23
|
+
ctx.addEventListener('message', (e) => {
|
|
24
|
+
const msg = e.data;
|
|
25
|
+
void handle(msg);
|
|
26
|
+
});
|
|
27
|
+
async function handle(msg) {
|
|
28
|
+
try {
|
|
29
|
+
if (msg.type === 'init') {
|
|
30
|
+
await initSapling(msg.wasmBytes);
|
|
31
|
+
const loaded = { spend: msg.spendParams, output: msg.outputParams };
|
|
32
|
+
// Refuse to prove with non-canonical params (the prover skips group-element
|
|
33
|
+
// checks for speed, so unverified params are attack surface).
|
|
34
|
+
await verifyCanonicalParams(loaded);
|
|
35
|
+
params = loaded;
|
|
36
|
+
ctx.postMessage({ type: 'ready' });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (msg.type === 'spend') {
|
|
40
|
+
if (!params)
|
|
41
|
+
throw new Error('worker not initialized (send { type: "init" } first)');
|
|
42
|
+
const hex = spendShielded(msg.spec, params);
|
|
43
|
+
ctx.postMessage({ type: 'result', id: msg.id, value: hex });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (msg.type === 'detect') {
|
|
47
|
+
const notes = detectNotes(msg.spec);
|
|
48
|
+
ctx.postMessage({ type: 'result', id: msg.id, value: notes });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (msg.type === 'read') {
|
|
52
|
+
const note = readNote(msg.spec);
|
|
53
|
+
ctx.postMessage({ type: 'result', id: msg.id, value: note });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
const id = 'id' in msg ? msg.id : -1;
|
|
59
|
+
ctx.postMessage({ type: 'error', id, message: err instanceof Error ? err.message : String(err) });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main-thread handle to the proving Web Worker. Wraps the postMessage protocol
|
|
3
|
+
* of `./prover-worker` into promise-returning `SpendProver` / `DetectProver`
|
|
4
|
+
* callbacks that plug straight into `../wallet`'s `detectNotes` /
|
|
5
|
+
* `buildShieldedSpend`.
|
|
6
|
+
*
|
|
7
|
+
* The caller constructs the `Worker` (its URL is bundler-specific) and passes
|
|
8
|
+
* the wasm bytes + the two Sapling params; this initializes the worker once and
|
|
9
|
+
* hands back the callbacks. Keeps the ~20 s spend prove off the UI thread.
|
|
10
|
+
*
|
|
11
|
+
* const worker = new Worker(new URL('./prover-worker.js', import.meta.url), { type: 'module' });
|
|
12
|
+
* const prover = await createWorkerProver(worker, { wasmBytes, spendParams, outputParams });
|
|
13
|
+
* const { hex } = await buildShieldedSpend(client, prover.spend, { ... });
|
|
14
|
+
*/
|
|
15
|
+
import type { DetectProver, SpendProver } from '../wallet.js';
|
|
16
|
+
import type { ReadNoteResult } from '../wasm.js';
|
|
17
|
+
/** Fully decrypt one incoming output (value + recipient + memo) in the worker. */
|
|
18
|
+
export type ReadProver = (specJson: string) => Promise<ReadNoteResult | null>;
|
|
19
|
+
export interface WorkerProverInit {
|
|
20
|
+
/** The `.wasm` bytes (fetch `crate/pkg/verus_sapling_prover_bg.wasm`). */
|
|
21
|
+
readonly wasmBytes: ArrayBuffer;
|
|
22
|
+
/** sapling-spend.params bytes. */
|
|
23
|
+
readonly spendParams: Uint8Array;
|
|
24
|
+
/** sapling-output.params bytes. */
|
|
25
|
+
readonly outputParams: Uint8Array;
|
|
26
|
+
}
|
|
27
|
+
export interface WorkerProver {
|
|
28
|
+
/** Prove + sign a shielded spend in the worker (~20 s). */
|
|
29
|
+
readonly spend: SpendProver;
|
|
30
|
+
/** Trial-decrypt notes in the worker (cheap; use only to keep wasm off-thread). */
|
|
31
|
+
readonly detect: DetectProver;
|
|
32
|
+
/** Fully decrypt one incoming output → value + recipient + memo (cheap). */
|
|
33
|
+
readonly read: ReadProver;
|
|
34
|
+
/** Terminate the worker. */
|
|
35
|
+
readonly terminate: () => void;
|
|
36
|
+
}
|
|
37
|
+
/** Initialize the worker (loads wasm + params once) and return the prover callbacks. */
|
|
38
|
+
export declare function createWorkerProver(worker: Worker, init: WorkerProverInit): Promise<WorkerProver>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main-thread handle to the proving Web Worker. Wraps the postMessage protocol
|
|
3
|
+
* of `./prover-worker` into promise-returning `SpendProver` / `DetectProver`
|
|
4
|
+
* callbacks that plug straight into `../wallet`'s `detectNotes` /
|
|
5
|
+
* `buildShieldedSpend`.
|
|
6
|
+
*
|
|
7
|
+
* The caller constructs the `Worker` (its URL is bundler-specific) and passes
|
|
8
|
+
* the wasm bytes + the two Sapling params; this initializes the worker once and
|
|
9
|
+
* hands back the callbacks. Keeps the ~20 s spend prove off the UI thread.
|
|
10
|
+
*
|
|
11
|
+
* const worker = new Worker(new URL('./prover-worker.js', import.meta.url), { type: 'module' });
|
|
12
|
+
* const prover = await createWorkerProver(worker, { wasmBytes, spendParams, outputParams });
|
|
13
|
+
* const { hex } = await buildShieldedSpend(client, prover.spend, { ... });
|
|
14
|
+
*/
|
|
15
|
+
/** Initialize the worker (loads wasm + params once) and return the prover callbacks. */
|
|
16
|
+
export function createWorkerProver(worker, init) {
|
|
17
|
+
const pending = new Map();
|
|
18
|
+
let nextId = 1;
|
|
19
|
+
let readyResolve;
|
|
20
|
+
const ready = new Promise((r) => (readyResolve = r));
|
|
21
|
+
worker.addEventListener('message', (e) => {
|
|
22
|
+
const msg = e.data;
|
|
23
|
+
if (msg.type === 'ready') {
|
|
24
|
+
readyResolve?.();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const entry = pending.get(msg.id);
|
|
28
|
+
if (!entry)
|
|
29
|
+
return;
|
|
30
|
+
pending.delete(msg.id);
|
|
31
|
+
if (msg.type === 'error')
|
|
32
|
+
entry.reject(new Error(msg.message));
|
|
33
|
+
else
|
|
34
|
+
entry.resolve(msg.value);
|
|
35
|
+
});
|
|
36
|
+
const call = (type, spec) => {
|
|
37
|
+
const id = nextId++;
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
pending.set(id, { resolve, reject });
|
|
40
|
+
worker.postMessage({ type, id, spec });
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
// Transfer the big params buffers into the worker (no copy).
|
|
44
|
+
worker.postMessage({ type: 'init', wasmBytes: init.wasmBytes, spendParams: init.spendParams, outputParams: init.outputParams }, [init.wasmBytes, init.spendParams.buffer, init.outputParams.buffer]);
|
|
45
|
+
return ready.then(() => ({
|
|
46
|
+
spend: (spec) => call('spend', spec),
|
|
47
|
+
detect: (spec) => call('detect', spec),
|
|
48
|
+
read: (spec) => call('read', spec),
|
|
49
|
+
terminate: () => worker.terminate(),
|
|
50
|
+
}));
|
|
51
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errors for the shielded package.
|
|
3
|
+
*
|
|
4
|
+
* These extend a LOCAL base (`Error`), NOT @chainvue/verus-sdk's `VerusError`:
|
|
5
|
+
* importing anything from the SDK pulls its Node-bundled `dist/bundle.js`
|
|
6
|
+
* (@bitgo/utxo-lib + `crypto`/`buffer`/…) into the browser bundle. To stay
|
|
7
|
+
* browser-safe the base is local, but it mirrors `VerusError`'s shape — a
|
|
8
|
+
* machine-readable `.code` plus `.name` — so callers can branch on those.
|
|
9
|
+
*/
|
|
10
|
+
/** Base class for all shielded-package errors. */
|
|
11
|
+
export declare class ShieldedError extends Error {
|
|
12
|
+
readonly code: string;
|
|
13
|
+
constructor(code: string, message: string);
|
|
14
|
+
}
|
|
15
|
+
/** Thrown when the shielded input contract is violated (bad amount, memo, address). */
|
|
16
|
+
export declare class ShieldedInputError extends ShieldedError {
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errors for the shielded package.
|
|
3
|
+
*
|
|
4
|
+
* These extend a LOCAL base (`Error`), NOT @chainvue/verus-sdk's `VerusError`:
|
|
5
|
+
* importing anything from the SDK pulls its Node-bundled `dist/bundle.js`
|
|
6
|
+
* (@bitgo/utxo-lib + `crypto`/`buffer`/…) into the browser bundle. To stay
|
|
7
|
+
* browser-safe the base is local, but it mirrors `VerusError`'s shape — a
|
|
8
|
+
* machine-readable `.code` plus `.name` — so callers can branch on those.
|
|
9
|
+
*/
|
|
10
|
+
/** Base class for all shielded-package errors. */
|
|
11
|
+
export class ShieldedError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
constructor(code, message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.name = 'ShieldedError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Thrown when the shielded input contract is violated (bad amount, memo, address). */
|
|
20
|
+
export class ShieldedInputError extends ShieldedError {
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super('ERR_SHIELDED_INPUT', message);
|
|
23
|
+
this.name = 'ShieldedInputError';
|
|
24
|
+
}
|
|
25
|
+
}
|
package/dist/hex.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buffer-free hex ↔ bytes helpers, so the core (parsers, orchestration, address
|
|
3
|
+
* decode) runs unchanged in the browser as well as Node. Node's `Buffer` is not
|
|
4
|
+
* available in a browser/extension context; these use only `Uint8Array`.
|
|
5
|
+
*/
|
|
6
|
+
/** Lowercase hex of a byte array. */
|
|
7
|
+
export declare function bytesToHex(bytes: Uint8Array): string;
|
|
8
|
+
/** Parse a hex string (even length) to bytes. Throws on odd length / non-hex. */
|
|
9
|
+
export declare function hexToBytes(hex: string): Uint8Array;
|
|
10
|
+
/** Reverse byte order (e.g. txid display ↔ internal). */
|
|
11
|
+
export declare function reverseBytes(bytes: Uint8Array): Uint8Array;
|
package/dist/hex.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Buffer-free hex ↔ bytes helpers, so the core (parsers, orchestration, address
|
|
3
|
+
* decode) runs unchanged in the browser as well as Node. Node's `Buffer` is not
|
|
4
|
+
* available in a browser/extension context; these use only `Uint8Array`.
|
|
5
|
+
*/
|
|
6
|
+
const HEX = '0123456789abcdef';
|
|
7
|
+
/** Lowercase hex of a byte array. */
|
|
8
|
+
export function bytesToHex(bytes) {
|
|
9
|
+
let out = '';
|
|
10
|
+
for (const b of bytes)
|
|
11
|
+
out += HEX[b >> 4] + HEX[b & 0x0f];
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
/** Parse a hex string (even length) to bytes. Throws on odd length / non-hex. */
|
|
15
|
+
export function hexToBytes(hex) {
|
|
16
|
+
if (hex.length % 2 !== 0)
|
|
17
|
+
throw new Error(`hex: odd length ${hex.length}`);
|
|
18
|
+
// Strict: reject any non-hex char up front. `Number.parseInt` otherwise accepts
|
|
19
|
+
// "+1"/" 1" and stops at the first bad char in a pair ("1z" -> 1), silently
|
|
20
|
+
// producing wrong bytes.
|
|
21
|
+
if (!/^[0-9a-fA-F]*$/.test(hex))
|
|
22
|
+
throw new Error('hex: invalid character');
|
|
23
|
+
const out = new Uint8Array(hex.length / 2);
|
|
24
|
+
for (let i = 0; i < out.length; i++) {
|
|
25
|
+
out[i] = Number.parseInt(hex.substr(i * 2, 2), 16);
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
/** Reverse byte order (e.g. txid display ↔ internal). */
|
|
30
|
+
export function reverseBytes(bytes) {
|
|
31
|
+
return Uint8Array.from(bytes).reverse();
|
|
32
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
export * from './parse.js';
|
|
28
|
+
export { CONSENSUS_BRANCH_ID, parseSats, toSafeNumber } from './money.js';
|
|
29
|
+
export { bytesToHex, hexToBytes, reverseBytes } from './hex.js';
|
|
30
|
+
export { decodeSaplingAddress, saplingAddressToHex } from './zaddr.js';
|
|
31
|
+
export { initSapling, shieldT2z, spendShielded, detectNotes as detectNotesWasm, readNote, verifyCanonicalParams, PARAM_SHA256, type SaplingParams, type DetectedNoteRaw, type ReadNoteResult, } from './wasm.js';
|
|
32
|
+
export { detectNotes, buildShieldedSpend, type DetectKey, type DetectProver, type SpendProver, type DetectNotesParams, type SpendableNote, type BuildShieldedSpendParams, type ShieldedRecipient, type TransparentRecipient, } from './wallet.js';
|
|
33
|
+
export type { LightwalletdTransport, TreeState, CompactOutput, CompactTx, CompactBlock, LightdInfo, } from './lightwalletd.js';
|