@orbinum/sdk 0.25.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +355 -23
- package/dist/adapters/indexeddb/index.d.mts +152 -0
- package/dist/adapters/indexeddb/index.d.ts +152 -0
- package/dist/adapters/indexeddb/index.js +381 -0
- package/dist/adapters/indexeddb/index.mjs +326 -0
- package/dist/chunk-JMYU5QAK.mjs +40 -0
- package/dist/chunk-Y6LNYJAJ.mjs +1242 -0
- package/dist/index-JYVjYJtf.d.mts +353 -0
- package/dist/index-JYVjYJtf.d.ts +353 -0
- package/dist/index.d.mts +4745 -2971
- package/dist/index.d.ts +4745 -2971
- package/dist/index.js +6319 -3431
- package/dist/index.mjs +4884 -3259
- package/dist/secretStore-CF6Nse__.d.mts +292 -0
- package/dist/secretStore-CF6Nse__.d.ts +292 -0
- package/dist/wallet/worker/index.d.mts +1 -0
- package/dist/wallet/worker/index.d.ts +1 -0
- package/dist/wallet/worker/index.js +859 -0
- package/dist/wallet/worker/index.mjs +28 -0
- package/package.json +24 -7
|
@@ -0,0 +1,1242 @@
|
|
|
1
|
+
// src/protocol/types.ts
|
|
2
|
+
var CURRENT_CIRCUIT_VERSION = 1;
|
|
3
|
+
|
|
4
|
+
// src/protocol/memo/EncryptedMemo.ts
|
|
5
|
+
import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
|
|
6
|
+
import { randomBytes } from "@noble/ciphers/utils.js";
|
|
7
|
+
import { packPoint, unpackPoint } from "@zk-kit/baby-jubjub";
|
|
8
|
+
|
|
9
|
+
// src/foundation/crypto/bjj-fast.ts
|
|
10
|
+
import { edwards } from "@noble/curves/abstract/edwards.js";
|
|
11
|
+
var P = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
12
|
+
var N = 2736030358979909402780800718157159386076813972158567259200215660948447373041n;
|
|
13
|
+
var BjjPoint = edwards({
|
|
14
|
+
p: P,
|
|
15
|
+
n: N,
|
|
16
|
+
h: 8n,
|
|
17
|
+
a: 168700n,
|
|
18
|
+
d: 168696n,
|
|
19
|
+
Gx: 5299619240641551281634865583518297030282874472190772894086521144482721001553n,
|
|
20
|
+
Gy: 16950150798460657717958625567821834550301663161624707787222815936182638968203n
|
|
21
|
+
});
|
|
22
|
+
function fastMulBase(scalar) {
|
|
23
|
+
const s = scalar % N;
|
|
24
|
+
if (s === 0n) return [0n, 1n];
|
|
25
|
+
const { x, y } = BjjPoint.BASE.multiply(s).toAffine();
|
|
26
|
+
return [x, y];
|
|
27
|
+
}
|
|
28
|
+
function fastMulPoint(point, scalar) {
|
|
29
|
+
const s = scalar % N;
|
|
30
|
+
if (s === 0n) return [0n, 1n];
|
|
31
|
+
const { x, y } = BjjPoint.fromAffine({ x: point[0], y: point[1] }).multiplyUnsafe(s).toAffine();
|
|
32
|
+
return [x, y];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/foundation/encoding/hex.ts
|
|
36
|
+
function toHex(bytes) {
|
|
37
|
+
return "0x" + Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
38
|
+
}
|
|
39
|
+
function fromHex(hex) {
|
|
40
|
+
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
41
|
+
if (clean.length % 2 !== 0) {
|
|
42
|
+
throw new Error(`Invalid hex string \u2014 odd length: "${hex}"`);
|
|
43
|
+
}
|
|
44
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
45
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
46
|
+
const byte = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
47
|
+
if (isNaN(byte)) throw new Error(`Invalid hex character at position ${i * 2}`);
|
|
48
|
+
bytes[i] = byte;
|
|
49
|
+
}
|
|
50
|
+
return bytes;
|
|
51
|
+
}
|
|
52
|
+
function ensureHexPrefix(hex) {
|
|
53
|
+
return hex.startsWith("0x") ? hex : `0x${hex}`;
|
|
54
|
+
}
|
|
55
|
+
function hexToNumber(hex) {
|
|
56
|
+
return parseInt(hex, 16);
|
|
57
|
+
}
|
|
58
|
+
function hexToBigint(hex) {
|
|
59
|
+
return BigInt(hex);
|
|
60
|
+
}
|
|
61
|
+
function scalarToHex(value) {
|
|
62
|
+
return "0x" + value.toString(16).padStart(64, "0");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/foundation/encoding/bytes.ts
|
|
66
|
+
function bigintTo32Le(n) {
|
|
67
|
+
const buf = new Uint8Array(32);
|
|
68
|
+
let v = n;
|
|
69
|
+
for (let i = 0; i < 32; i++) {
|
|
70
|
+
buf[i] = Number(v & 0xffn);
|
|
71
|
+
v >>= 8n;
|
|
72
|
+
}
|
|
73
|
+
return buf;
|
|
74
|
+
}
|
|
75
|
+
function bytesToBigintLE(bytes) {
|
|
76
|
+
let result = 0n;
|
|
77
|
+
for (let i = bytes.length - 1; i >= 0; i--) {
|
|
78
|
+
result = result << 8n | BigInt(bytes[i] ?? 0);
|
|
79
|
+
}
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
function bigintTo32Be(n) {
|
|
83
|
+
const buf = new Uint8Array(32);
|
|
84
|
+
let v = n;
|
|
85
|
+
for (let i = 31; i >= 0 && v > 0n; i--) {
|
|
86
|
+
buf[i] = Number(v & 0xffn);
|
|
87
|
+
v >>= 8n;
|
|
88
|
+
}
|
|
89
|
+
return buf;
|
|
90
|
+
}
|
|
91
|
+
function bigintTo32LeArr(n) {
|
|
92
|
+
const out = new Array(32).fill(0);
|
|
93
|
+
let v = n;
|
|
94
|
+
for (let i = 0; i < 32; i++) {
|
|
95
|
+
out[i] = Number(v & 0xffn);
|
|
96
|
+
v >>= 8n;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
function computePathIndices(leafIndex, depth) {
|
|
101
|
+
const indices = [];
|
|
102
|
+
let idx = leafIndex;
|
|
103
|
+
for (let i = 0; i < depth; i++) {
|
|
104
|
+
indices.push(idx & 1);
|
|
105
|
+
idx >>= 1;
|
|
106
|
+
}
|
|
107
|
+
return indices;
|
|
108
|
+
}
|
|
109
|
+
function leHexToBigint(hex) {
|
|
110
|
+
return bytesToBigintLE(fromHex(hex));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/foundation/crypto/constants.ts
|
|
114
|
+
var BN254_R = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
115
|
+
var BABYJUB_SUBORDER = 2736030358979909402780800718157159386076813972158567259200215660948447373041n;
|
|
116
|
+
|
|
117
|
+
// src/protocol/memo/plaintext.ts
|
|
118
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
119
|
+
var KEY_DOMAIN = new TextEncoder().encode("orbinum-note-encryption-v1");
|
|
120
|
+
var VIEW_TAG_DOMAIN = new TextEncoder().encode("orbinum-view-tag-v1");
|
|
121
|
+
var MEMO_PLAINTEXT_SIZE = 120;
|
|
122
|
+
function serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk, circuitVersion) {
|
|
123
|
+
const buf = new Uint8Array(MEMO_PLAINTEXT_SIZE);
|
|
124
|
+
const view = new DataView(buf.buffer);
|
|
125
|
+
view.setBigUint64(0, value & 0xffffffffffffffffn, true);
|
|
126
|
+
view.setBigUint64(8, value >> 64n & 0xffffffffffffffffn, true);
|
|
127
|
+
buf.set(ownerPk.slice(0, 32), 16);
|
|
128
|
+
buf.set(blinding.slice(0, 32), 48);
|
|
129
|
+
view.setUint32(80, assetId >>> 0, true);
|
|
130
|
+
buf.set(counterpartyPk.slice(0, 32), 84);
|
|
131
|
+
view.setUint32(116, circuitVersion >>> 0, true);
|
|
132
|
+
return buf;
|
|
133
|
+
}
|
|
134
|
+
function deriveEncryptionKey(sharedSecret, commitment) {
|
|
135
|
+
const h = sha256.create();
|
|
136
|
+
h.update(sharedSecret);
|
|
137
|
+
h.update(commitment);
|
|
138
|
+
h.update(KEY_DOMAIN);
|
|
139
|
+
return h.digest();
|
|
140
|
+
}
|
|
141
|
+
function deriveViewTag(sharedSecret) {
|
|
142
|
+
const h = sha256.create();
|
|
143
|
+
h.update(VIEW_TAG_DOMAIN);
|
|
144
|
+
h.update(sharedSecret);
|
|
145
|
+
return h.digest()[0];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/protocol/memo/EncryptedMemo.ts
|
|
149
|
+
var NONCE_SIZE = 12;
|
|
150
|
+
var CIPHERTEXT_SIZE = 136;
|
|
151
|
+
var EPH_PK_SIZE = 32;
|
|
152
|
+
var ENCRYPTED_MEMO_SIZE = NONCE_SIZE + CIPHERTEXT_SIZE + EPH_PK_SIZE;
|
|
153
|
+
function bytesToBjjScalar(bytes) {
|
|
154
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
155
|
+
return BigInt("0x" + hex) % BABYJUB_SUBORDER || 1n;
|
|
156
|
+
}
|
|
157
|
+
function parsePlaintext(nonce, ciphertextWithMac, encKey) {
|
|
158
|
+
try {
|
|
159
|
+
const cipher = chacha20poly1305(encKey, nonce);
|
|
160
|
+
const plaintext = cipher.decrypt(ciphertextWithMac);
|
|
161
|
+
const view = new DataView(plaintext.buffer, plaintext.byteOffset, plaintext.byteLength);
|
|
162
|
+
const valueLo = view.getBigUint64(0, true);
|
|
163
|
+
const valueHi = view.getBigUint64(8, true);
|
|
164
|
+
const value = valueLo | valueHi << 64n;
|
|
165
|
+
const ownerPk = bytesToBigintLE(plaintext.slice(16, 48));
|
|
166
|
+
const blinding = bytesToBigintLE(plaintext.slice(48, 80));
|
|
167
|
+
const assetId = BigInt(view.getUint32(80, true));
|
|
168
|
+
const counterpartyPk = bytesToBigintLE(plaintext.slice(84, 116));
|
|
169
|
+
const circuitVersion = view.getUint32(116, true);
|
|
170
|
+
return { value, ownerPk, blinding, assetId, counterpartyPk, circuitVersion };
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
var EncryptedMemo = {
|
|
176
|
+
/**
|
|
177
|
+
* Build and encrypt a memo for a note using ECDH (v2, 180 bytes).
|
|
178
|
+
*
|
|
179
|
+
* @param value Note value in planck.
|
|
180
|
+
* @param ownerPk 32-byte owner public key (LE).
|
|
181
|
+
* @param blinding 32-byte blinding scalar (LE).
|
|
182
|
+
* @param assetId Asset identifier.
|
|
183
|
+
* @param commitment 32-byte commitment bytes (LE).
|
|
184
|
+
* @param recipientIvkPacked 32-byte LE-encoded packed BJJ viewing public key
|
|
185
|
+
* (from PrivacyKeyManager.getViewingPublicKeyPacked() or
|
|
186
|
+
* decoded from a privacy address).
|
|
187
|
+
* Pass `new Uint8Array(32)` (all zeros) for a publicly-readable memo.
|
|
188
|
+
* @param counterpartyPk 32-byte counterparty BJJ Ax. Default: all zeros.
|
|
189
|
+
* @param circuitVersion ZK circuit version the note is spent under. Default: 0.
|
|
190
|
+
* @param ephSkOverride 32-byte ephemeral secret key (stealth coordination). Optional.
|
|
191
|
+
* @returns 180-byte encrypted memo: nonce(12) || ciphertext+MAC(136) || ephPk(32).
|
|
192
|
+
*/
|
|
193
|
+
encrypt(value, ownerPk, blinding, assetId, commitment, recipientIvkPacked, counterpartyPk = new Uint8Array(32), circuitVersion = 0, ephSkOverride) {
|
|
194
|
+
const nonce = randomBytes(NONCE_SIZE);
|
|
195
|
+
const plaintext = serializeMemo(
|
|
196
|
+
value,
|
|
197
|
+
ownerPk,
|
|
198
|
+
blinding,
|
|
199
|
+
assetId,
|
|
200
|
+
counterpartyPk,
|
|
201
|
+
circuitVersion
|
|
202
|
+
);
|
|
203
|
+
const isZeroKey = recipientIvkPacked.every((b) => b === 0);
|
|
204
|
+
let sharedSecret;
|
|
205
|
+
let ephPkPackedBytes;
|
|
206
|
+
if (isZeroKey) {
|
|
207
|
+
sharedSecret = new Uint8Array(32);
|
|
208
|
+
ephPkPackedBytes = new Uint8Array(EPH_PK_SIZE);
|
|
209
|
+
} else {
|
|
210
|
+
const ephSkBytes = ephSkOverride ?? randomBytes(32);
|
|
211
|
+
if (ephSkBytes.length !== 32)
|
|
212
|
+
throw new Error("EncryptedMemo.encrypt: ephSkOverride must be 32 bytes");
|
|
213
|
+
const ephSkScalar = bytesToBjjScalar(ephSkBytes);
|
|
214
|
+
const ephPkPoint = fastMulBase(ephSkScalar);
|
|
215
|
+
ephPkPackedBytes = bigintTo32Le(packPoint(ephPkPoint));
|
|
216
|
+
const ivkPackedBigint = bytesToBigintLE(recipientIvkPacked);
|
|
217
|
+
const ivkPoint = unpackPoint(ivkPackedBigint);
|
|
218
|
+
if (!ivkPoint)
|
|
219
|
+
throw new Error("EncryptedMemo.encrypt: invalid recipient viewing public key");
|
|
220
|
+
const sharedPoint = fastMulPoint(ivkPoint, ephSkScalar);
|
|
221
|
+
sharedSecret = bigintTo32Le(sharedPoint[0]);
|
|
222
|
+
}
|
|
223
|
+
nonce[0] = deriveViewTag(sharedSecret);
|
|
224
|
+
const encKey = deriveEncryptionKey(sharedSecret, commitment);
|
|
225
|
+
const cipher = chacha20poly1305(encKey, nonce);
|
|
226
|
+
const ciphertext = cipher.encrypt(plaintext);
|
|
227
|
+
const result = new Uint8Array(ENCRYPTED_MEMO_SIZE);
|
|
228
|
+
result.set(nonce, 0);
|
|
229
|
+
result.set(ciphertext, NONCE_SIZE);
|
|
230
|
+
result.set(ephPkPackedBytes, NONCE_SIZE + CIPHERTEXT_SIZE);
|
|
231
|
+
return result;
|
|
232
|
+
},
|
|
233
|
+
/**
|
|
234
|
+
* Returns a 180-byte public memo encrypted with a zero viewing key.
|
|
235
|
+
* Decryptable by anyone with `decrypt(memo, commitment, new Uint8Array(32))`.
|
|
236
|
+
* Convenience alias for `encrypt(..., new Uint8Array(32))`.
|
|
237
|
+
*/
|
|
238
|
+
encryptPublic(value, ownerPk, blinding, assetId, commitment, circuitVersion = 0) {
|
|
239
|
+
return EncryptedMemo.encrypt(
|
|
240
|
+
value,
|
|
241
|
+
ownerPk,
|
|
242
|
+
blinding,
|
|
243
|
+
assetId,
|
|
244
|
+
commitment,
|
|
245
|
+
new Uint8Array(32),
|
|
246
|
+
new Uint8Array(32),
|
|
247
|
+
circuitVersion
|
|
248
|
+
);
|
|
249
|
+
},
|
|
250
|
+
/**
|
|
251
|
+
* Returns a 180-byte zeroed dummy memo (no information, always valid on-chain).
|
|
252
|
+
*/
|
|
253
|
+
dummy() {
|
|
254
|
+
return new Uint8Array(ENCRYPTED_MEMO_SIZE);
|
|
255
|
+
},
|
|
256
|
+
/**
|
|
257
|
+
* Validates that `bytes` is a properly-sized encrypted memo.
|
|
258
|
+
* Throws an Error if the length is not ENCRYPTED_MEMO_SIZE (180 bytes).
|
|
259
|
+
*
|
|
260
|
+
* Call this at system boundaries (extrinsic builders, precompile encoders)
|
|
261
|
+
* to catch malformed memos before they reach the chain and fail on-chain.
|
|
262
|
+
*
|
|
263
|
+
* @param bytes The memo bytes to validate.
|
|
264
|
+
* @param context Optional context string included in the error (e.g. 'shield', 'output[0]').
|
|
265
|
+
*/
|
|
266
|
+
validate(bytes, context) {
|
|
267
|
+
if (bytes.length !== ENCRYPTED_MEMO_SIZE) {
|
|
268
|
+
const ctx = context ? ` (${context})` : "";
|
|
269
|
+
throw new Error(
|
|
270
|
+
`EncryptedMemo: invalid size${ctx} \u2014 expected ${ENCRYPTED_MEMO_SIZE} bytes, got ${bytes.length}`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
/**
|
|
275
|
+
* Decrypt an on-chain EncryptedMemo using the recipient's viewing secret key.
|
|
276
|
+
* Returns null if decryption fails — wrong key, bad MAC, or malformed memo.
|
|
277
|
+
* Never throws; safe for scan loops.
|
|
278
|
+
*
|
|
279
|
+
* @param memoBytes 180-byte encrypted memo.
|
|
280
|
+
* @param commitment 32-byte note commitment (LE).
|
|
281
|
+
* @param viewingSecretKey 32-byte HKDF viewing secret key from deriveViewingSecretKey().
|
|
282
|
+
*/
|
|
283
|
+
decrypt(memoBytes, commitment, viewingSecretKey) {
|
|
284
|
+
if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
|
|
285
|
+
return EncryptedMemo._decrypt(memoBytes, commitment, viewingSecretKey);
|
|
286
|
+
},
|
|
287
|
+
/**
|
|
288
|
+
* Extract the ECDH shared secret from an encrypted memo using the recipient's viewing secret key.
|
|
289
|
+
*
|
|
290
|
+
* Used by NoteDecryptor to obtain the shared secret needed for stealth address derivation
|
|
291
|
+
* without re-running the full decrypt path. Safe to call on any 180-byte memo.
|
|
292
|
+
*
|
|
293
|
+
* Returns `new Uint8Array(32)` (all zeros) for public/dummy memos (zero ephPk).
|
|
294
|
+
* Returns `null` if the memo is malformed or the ephPk is not a valid BJJ point.
|
|
295
|
+
* Never throws; safe for scan loops.
|
|
296
|
+
*
|
|
297
|
+
* @param memoBytes 180-byte encrypted memo.
|
|
298
|
+
* @param viewingSecretKey 32-byte HKDF viewing secret key from deriveViewingSecretKey().
|
|
299
|
+
*/
|
|
300
|
+
extractSharedSecret(memoBytes, viewingSecretKey) {
|
|
301
|
+
if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
|
|
302
|
+
const ephPkPackedBytes = memoBytes.slice(NONCE_SIZE + CIPHERTEXT_SIZE);
|
|
303
|
+
const ephPkPackedBigint = bytesToBigintLE(ephPkPackedBytes);
|
|
304
|
+
if (ephPkPackedBigint === 0n) {
|
|
305
|
+
return new Uint8Array(32);
|
|
306
|
+
}
|
|
307
|
+
const ephPkPoint = unpackPoint(ephPkPackedBigint);
|
|
308
|
+
if (!ephPkPoint) return null;
|
|
309
|
+
const ivskScalar = bytesToBjjScalar(viewingSecretKey);
|
|
310
|
+
const sharedPoint = fastMulPoint(ephPkPoint, ivskScalar);
|
|
311
|
+
return bigintTo32Le(sharedPoint[0]);
|
|
312
|
+
},
|
|
313
|
+
/**
|
|
314
|
+
* Cheap view-tag check: does memo nonce[0] match the tag derived from
|
|
315
|
+
* `sharedSecret`? One SHA256 + one byte compare — no AEAD work.
|
|
316
|
+
*
|
|
317
|
+
* Only meaningful for memos built with view tags (commitments at/after
|
|
318
|
+
* the wallet's tagActivationLeaf): a legacy memo carries a random byte
|
|
319
|
+
* there and would false-negative 255/256 of the time.
|
|
320
|
+
*/
|
|
321
|
+
checkViewTag(memoBytes, sharedSecret) {
|
|
322
|
+
if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return false;
|
|
323
|
+
return memoBytes[0] === deriveViewTag(sharedSecret);
|
|
324
|
+
},
|
|
325
|
+
/**
|
|
326
|
+
* Decrypt with an already-computed shared secret (from
|
|
327
|
+
* extractSharedSecret), skipping the ECDH. Pair with checkViewTag for the
|
|
328
|
+
* fast scan path: ECDH once → tag check → decrypt only on match.
|
|
329
|
+
*/
|
|
330
|
+
decryptWithSharedSecret(memoBytes, commitment, sharedSecret) {
|
|
331
|
+
if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
|
|
332
|
+
const nonce = memoBytes.slice(0, NONCE_SIZE);
|
|
333
|
+
const ciphertextWithMac = memoBytes.slice(NONCE_SIZE, NONCE_SIZE + CIPHERTEXT_SIZE);
|
|
334
|
+
const encKey = deriveEncryptionKey(sharedSecret, commitment);
|
|
335
|
+
return parsePlaintext(nonce, ciphertextWithMac, encKey);
|
|
336
|
+
},
|
|
337
|
+
/** @internal */
|
|
338
|
+
_decrypt(memoBytes, commitment, viewingSecretKey) {
|
|
339
|
+
const sharedSecret = EncryptedMemo.extractSharedSecret(memoBytes, viewingSecretKey);
|
|
340
|
+
if (!sharedSecret) return null;
|
|
341
|
+
return EncryptedMemo.decryptWithSharedSecret(memoBytes, commitment, sharedSecret);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// src/foundation/crypto/stealth.ts
|
|
346
|
+
import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
|
|
347
|
+
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
348
|
+
import { mulPointEscalar, Base8, addPoint } from "@zk-kit/baby-jubjub";
|
|
349
|
+
var STEALTH_INFO = new TextEncoder().encode("orbinum-stealth-v1");
|
|
350
|
+
function deriveStealthScalar(sharedSecret, ownerPkBigint) {
|
|
351
|
+
const salt = bigintTo32Le(ownerPkBigint);
|
|
352
|
+
const stealthBytes = hkdf(sha2562, sharedSecret, salt, STEALTH_INFO, 32);
|
|
353
|
+
return bytesToBigintLE(stealthBytes) % BABYJUB_SUBORDER || 1n;
|
|
354
|
+
}
|
|
355
|
+
function deriveStealthOwnerPk(sharedSecret, ownerPkBigint, ownerPkPoint) {
|
|
356
|
+
const stealthScalar = deriveStealthScalar(sharedSecret, ownerPkBigint);
|
|
357
|
+
const stealthPt = addPoint(mulPointEscalar(Base8, stealthScalar), ownerPkPoint);
|
|
358
|
+
return stealthPt[0];
|
|
359
|
+
}
|
|
360
|
+
function deriveStealthSk(sharedSecret, ownerPkBigint, spendingKey) {
|
|
361
|
+
const stealthScalar = deriveStealthScalar(sharedSecret, ownerPkBigint);
|
|
362
|
+
return (stealthScalar + spendingKey) % BABYJUB_SUBORDER || 1n;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/foundation/crypto/bjj.ts
|
|
366
|
+
import { mulPointEscalar as mulPointEscalar2 } from "@zk-kit/baby-jubjub";
|
|
367
|
+
var BJJ_A = 168700n;
|
|
368
|
+
var BJJ_D = 168696n;
|
|
369
|
+
function _modpow(base, exp, mod) {
|
|
370
|
+
let result = 1n;
|
|
371
|
+
base = base % mod;
|
|
372
|
+
while (exp > 0n) {
|
|
373
|
+
if (exp & 1n) result = result * base % mod;
|
|
374
|
+
exp >>= 1n;
|
|
375
|
+
base = base * base % mod;
|
|
376
|
+
}
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
function _sqrtModP(y2) {
|
|
380
|
+
if (y2 === 0n) return 0n;
|
|
381
|
+
if (_modpow(y2, (BN254_R - 1n) / 2n, BN254_R) !== 1n) return null;
|
|
382
|
+
let s = 0n;
|
|
383
|
+
let q = BN254_R - 1n;
|
|
384
|
+
while ((q & 1n) === 0n) {
|
|
385
|
+
q >>= 1n;
|
|
386
|
+
s++;
|
|
387
|
+
}
|
|
388
|
+
if (s === 1n) return _modpow(y2, (BN254_R + 1n) / 4n, BN254_R);
|
|
389
|
+
let z = 2n;
|
|
390
|
+
while (_modpow(z, (BN254_R - 1n) / 2n, BN254_R) === 1n) z++;
|
|
391
|
+
let m = s;
|
|
392
|
+
let c = _modpow(z, q, BN254_R);
|
|
393
|
+
let t = _modpow(y2, q, BN254_R);
|
|
394
|
+
let r = _modpow(y2, (q + 1n) / 2n, BN254_R);
|
|
395
|
+
for (; ; ) {
|
|
396
|
+
if (t === 1n) return r;
|
|
397
|
+
let i = 1n;
|
|
398
|
+
let tmp = t * t % BN254_R;
|
|
399
|
+
while (tmp !== 1n) {
|
|
400
|
+
tmp = tmp * tmp % BN254_R;
|
|
401
|
+
i++;
|
|
402
|
+
}
|
|
403
|
+
const b = _modpow(c, 1n << m - i - 1n, BN254_R);
|
|
404
|
+
m = i;
|
|
405
|
+
c = b * b % BN254_R;
|
|
406
|
+
t = t * c % BN254_R;
|
|
407
|
+
r = r * b % BN254_R;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function recoverOwnerPkPoint(ax) {
|
|
411
|
+
const x2 = ax * ax % BN254_R;
|
|
412
|
+
const num = ((1n - BJJ_A * x2) % BN254_R + BN254_R) % BN254_R;
|
|
413
|
+
const den = ((1n - BJJ_D * x2) % BN254_R + BN254_R) % BN254_R;
|
|
414
|
+
if (den === 0n) return null;
|
|
415
|
+
const denInv = _modpow(den, BN254_R - 2n, BN254_R);
|
|
416
|
+
const y2 = num * denInv % BN254_R;
|
|
417
|
+
const y = _sqrtModP(y2);
|
|
418
|
+
if (y === null) return null;
|
|
419
|
+
const yAlt = BN254_R - y;
|
|
420
|
+
try {
|
|
421
|
+
const check = mulPointEscalar2([ax, y], BABYJUB_SUBORDER);
|
|
422
|
+
return check[0] === 0n && check[1] === 1n ? [ax, y] : [ax, yAlt];
|
|
423
|
+
} catch {
|
|
424
|
+
return [ax, yAlt];
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// src/protocol/note/NoteBuilder.ts
|
|
429
|
+
import { mulPointEscalar as mulPointEscalar3, unpackPoint as unpackPoint2 } from "@zk-kit/baby-jubjub";
|
|
430
|
+
import { randomBytes as randomBytes2 } from "@noble/ciphers/utils.js";
|
|
431
|
+
import { poseidon2, poseidon4 } from "poseidon-lite";
|
|
432
|
+
var NoteBuilder = class {
|
|
433
|
+
/**
|
|
434
|
+
* Build a ZkNote from the given inputs.
|
|
435
|
+
*
|
|
436
|
+
* @param input.value Amount in planck (required).
|
|
437
|
+
* @param input.assetId Asset ID — default 0n (native ORB-Privacy).
|
|
438
|
+
* @param input.ownerPk Sender's or recipient's global BabyJubJub Ax — default 0n.
|
|
439
|
+
* @param input.blinding Random scalar — defaults to BigInt(Date.now()).
|
|
440
|
+
* @param input.spendingKey Secret key for nullifier — default 0n.
|
|
441
|
+
* @param input.viewingPublicKey Recipient's 32-byte LE packed BJJ ivk. Triggers memo encryption.
|
|
442
|
+
* @param input.recipientOwnerPk Recipient's global ownerPk. Required with viewingPublicKey
|
|
443
|
+
* to enable stealth address derivation. Without it, the
|
|
444
|
+
* commitment uses ownerPk directly (no stealth).
|
|
445
|
+
*/
|
|
446
|
+
static async build(input) {
|
|
447
|
+
const value = input.value;
|
|
448
|
+
const assetId = input.assetId ?? 0n;
|
|
449
|
+
const ownerPk = input.ownerPk ?? 0n;
|
|
450
|
+
const blinding = input.blinding ?? BigInt(Date.now());
|
|
451
|
+
const spendingKey = input.spendingKey ?? 0n;
|
|
452
|
+
const counterpartyPk = input.counterpartyPk ?? 0n;
|
|
453
|
+
const circuitVersion = input.circuitVersion ?? CURRENT_CIRCUIT_VERSION;
|
|
454
|
+
const useStealth = input.viewingPublicKey !== void 0 && input.recipientOwnerPk !== void 0;
|
|
455
|
+
let memo;
|
|
456
|
+
if (useStealth) {
|
|
457
|
+
const recipientOwnerPk = input.recipientOwnerPk;
|
|
458
|
+
const recipientIvkPacked = input.viewingPublicKey;
|
|
459
|
+
const ephSk = randomBytes2(32);
|
|
460
|
+
const ivkPackedBigint = bytesToBigintLE(recipientIvkPacked);
|
|
461
|
+
const ivkPoint = unpackPoint2(ivkPackedBigint);
|
|
462
|
+
if (!ivkPoint)
|
|
463
|
+
throw new Error("NoteBuilder.build: invalid recipient viewing public key");
|
|
464
|
+
const ephSkScalar = BigInt(toHex(ephSk)) % BABYJUB_SUBORDER || 1n;
|
|
465
|
+
const sharedPoint = mulPointEscalar3(ivkPoint, ephSkScalar);
|
|
466
|
+
const sharedSecret = bigintTo32Le(sharedPoint[0]);
|
|
467
|
+
const recipientPkPoint = recoverOwnerPkPoint(recipientOwnerPk);
|
|
468
|
+
if (!recipientPkPoint)
|
|
469
|
+
throw new Error(
|
|
470
|
+
"NoteBuilder.build: recipientOwnerPk is not a valid BJJ x-coordinate"
|
|
471
|
+
);
|
|
472
|
+
const effectiveOwnerPk = deriveStealthOwnerPk(
|
|
473
|
+
sharedSecret,
|
|
474
|
+
recipientOwnerPk,
|
|
475
|
+
recipientPkPoint
|
|
476
|
+
);
|
|
477
|
+
const stealthCommitment = poseidon4([value, assetId, effectiveOwnerPk, blinding]);
|
|
478
|
+
const stealthCommitmentBytes = bigintTo32Le(stealthCommitment);
|
|
479
|
+
memo = Array.from(
|
|
480
|
+
EncryptedMemo.encrypt(
|
|
481
|
+
value,
|
|
482
|
+
bigintTo32Le(effectiveOwnerPk),
|
|
483
|
+
bigintTo32Le(blinding),
|
|
484
|
+
Number(assetId),
|
|
485
|
+
stealthCommitmentBytes,
|
|
486
|
+
recipientIvkPacked,
|
|
487
|
+
bigintTo32Le(counterpartyPk),
|
|
488
|
+
circuitVersion,
|
|
489
|
+
ephSk
|
|
490
|
+
)
|
|
491
|
+
);
|
|
492
|
+
const commitment2 = stealthCommitment;
|
|
493
|
+
const nullifier2 = poseidon2([commitment2, spendingKey]);
|
|
494
|
+
const commitmentBytes2 = stealthCommitmentBytes;
|
|
495
|
+
const nullifierBytes2 = bigintTo32Le(nullifier2);
|
|
496
|
+
if (memo.length !== ENCRYPTED_MEMO_SIZE)
|
|
497
|
+
throw new Error(
|
|
498
|
+
`NoteBuilder.build: invariant violated \u2014 memo must be ${ENCRYPTED_MEMO_SIZE} bytes, got ${memo.length}`
|
|
499
|
+
);
|
|
500
|
+
return {
|
|
501
|
+
value,
|
|
502
|
+
assetId,
|
|
503
|
+
ownerPk: effectiveOwnerPk,
|
|
504
|
+
blinding,
|
|
505
|
+
spendingKey,
|
|
506
|
+
circuitVersion,
|
|
507
|
+
spent: false,
|
|
508
|
+
spentAt: null,
|
|
509
|
+
commitment: commitment2,
|
|
510
|
+
nullifier: nullifier2,
|
|
511
|
+
commitmentHex: toHex(commitmentBytes2),
|
|
512
|
+
nullifierHex: toHex(nullifierBytes2),
|
|
513
|
+
memo,
|
|
514
|
+
counterpartyPk
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
const commitment = poseidon4([value, assetId, ownerPk, blinding]);
|
|
518
|
+
const nullifier = poseidon2([commitment, spendingKey]);
|
|
519
|
+
const commitmentBytes = bigintTo32Le(commitment);
|
|
520
|
+
const nullifierBytes = bigintTo32Le(nullifier);
|
|
521
|
+
memo = input.viewingPublicKey !== void 0 ? Array.from(
|
|
522
|
+
EncryptedMemo.encrypt(
|
|
523
|
+
value,
|
|
524
|
+
bigintTo32Le(ownerPk),
|
|
525
|
+
bigintTo32Le(blinding),
|
|
526
|
+
Number(assetId),
|
|
527
|
+
commitmentBytes,
|
|
528
|
+
input.viewingPublicKey,
|
|
529
|
+
bigintTo32Le(counterpartyPk),
|
|
530
|
+
circuitVersion,
|
|
531
|
+
input.ephSkOverride
|
|
532
|
+
)
|
|
533
|
+
) : Array.from(EncryptedMemo.dummy());
|
|
534
|
+
if (memo.length !== ENCRYPTED_MEMO_SIZE)
|
|
535
|
+
throw new Error(
|
|
536
|
+
`NoteBuilder.build: invariant violated \u2014 memo must be ${ENCRYPTED_MEMO_SIZE} bytes, got ${memo.length}`
|
|
537
|
+
);
|
|
538
|
+
return {
|
|
539
|
+
value,
|
|
540
|
+
assetId,
|
|
541
|
+
ownerPk,
|
|
542
|
+
blinding,
|
|
543
|
+
spendingKey,
|
|
544
|
+
circuitVersion,
|
|
545
|
+
spent: false,
|
|
546
|
+
spentAt: null,
|
|
547
|
+
commitment,
|
|
548
|
+
nullifier,
|
|
549
|
+
commitmentHex: toHex(commitmentBytes),
|
|
550
|
+
nullifierHex: toHex(nullifierBytes),
|
|
551
|
+
memo,
|
|
552
|
+
counterpartyPk
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Build the 180-byte ECDH-encrypted memo for a note.
|
|
557
|
+
*
|
|
558
|
+
* Pure TypeScript implementation — no WASM dependency.
|
|
559
|
+
* Uses ChaCha20-Poly1305 with ECDH key agreement (BabyJubJub ephemeral keypair).
|
|
560
|
+
*
|
|
561
|
+
* @param note The ZkNote whose fields populate the plaintext.
|
|
562
|
+
* @param recipientIvkPacked 32-byte LE packed BJJ viewing public key of the recipient.
|
|
563
|
+
* Pass `new Uint8Array(32)` (default) for a public/dummy memo.
|
|
564
|
+
* @param counterpartyPk 32-byte counterparty BabyJubJub Ax.
|
|
565
|
+
* Pass `new Uint8Array(32)` (default) for no counterparty.
|
|
566
|
+
*/
|
|
567
|
+
static buildMemo(note, recipientIvkPacked, counterpartyPk) {
|
|
568
|
+
return EncryptedMemo.encrypt(
|
|
569
|
+
note.value,
|
|
570
|
+
bigintTo32Le(note.ownerPk),
|
|
571
|
+
bigintTo32Le(note.blinding),
|
|
572
|
+
Number(note.assetId),
|
|
573
|
+
bigintTo32Le(note.commitment),
|
|
574
|
+
recipientIvkPacked ?? new Uint8Array(32),
|
|
575
|
+
counterpartyPk ?? bigintTo32Le(note.counterpartyPk ?? 0n),
|
|
576
|
+
note.circuitVersion
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
// src/protocol/note/NoteDecryptor.ts
|
|
582
|
+
import { poseidon2 as poseidon22, poseidon4 as poseidon42 } from "poseidon-lite";
|
|
583
|
+
|
|
584
|
+
// src/protocol/spend/coinSelection.ts
|
|
585
|
+
var TRANSFER_TREE_DEPTH = 20;
|
|
586
|
+
var LEAVES_PER_TREE = 1 << TRANSFER_TREE_DEPTH;
|
|
587
|
+
function isValidLeafIndex(leafIndex) {
|
|
588
|
+
return leafIndex !== null && leafIndex !== void 0 && Number.isSafeInteger(leafIndex) && leafIndex >= 0 && leafIndex < 2 ** 32;
|
|
589
|
+
}
|
|
590
|
+
function treeIdOf(note) {
|
|
591
|
+
const idx = note.leafIndex;
|
|
592
|
+
return isValidLeafIndex(idx) ? Math.floor(idx / LEAVES_PER_TREE) : 0;
|
|
593
|
+
}
|
|
594
|
+
function isSpendable(note) {
|
|
595
|
+
return !note.spent && note.value > 0n;
|
|
596
|
+
}
|
|
597
|
+
function canPairWith(a, b) {
|
|
598
|
+
return a.circuitVersion === b.circuitVersion && treeIdOf(a) === treeIdOf(b);
|
|
599
|
+
}
|
|
600
|
+
function selectNotes(notes, needed) {
|
|
601
|
+
const unspent = notes.filter(isSpendable);
|
|
602
|
+
const sorted = [...unspent].sort((a, b) => a.value < b.value ? -1 : 1);
|
|
603
|
+
const single = sorted.find((n) => n.value >= needed);
|
|
604
|
+
if (single) return [single, null];
|
|
605
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
606
|
+
for (let j = i + 1; j < sorted.length; j++) {
|
|
607
|
+
const a = sorted[i];
|
|
608
|
+
const b = sorted[j];
|
|
609
|
+
if (a !== void 0 && b !== void 0 && canPairWith(a, b) && a.value + b.value >= needed) {
|
|
610
|
+
return [a, b];
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
615
|
+
for (let j = i + 1; j < sorted.length; j++) {
|
|
616
|
+
const a = sorted[i];
|
|
617
|
+
const b = sorted[j];
|
|
618
|
+
if (a !== void 0 && b !== void 0 && a.circuitVersion === b.circuitVersion && a.value + b.value >= needed) {
|
|
619
|
+
return { needsConsolidation: true };
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
function buildDummyTransferInput(assetId) {
|
|
626
|
+
const zeroSibling = "0x" + "00".repeat(32);
|
|
627
|
+
return {
|
|
628
|
+
nullifier: 0n,
|
|
629
|
+
// Constraint 9: nullifier * is_dummy.out === 0 → must be 0
|
|
630
|
+
value: 0n,
|
|
631
|
+
// triggers is_dummy[i].out = 1 in the circuit
|
|
632
|
+
assetId,
|
|
633
|
+
// must match real note (Constraint 7)
|
|
634
|
+
ownerPk: 0n,
|
|
635
|
+
blinding: 0n,
|
|
636
|
+
spendingKey: 1n,
|
|
637
|
+
// arbitrary; EdDSA is disabled (enabled = 0) for dummy inputs
|
|
638
|
+
pathSiblings: Array(TRANSFER_TREE_DEPTH).fill(zeroSibling),
|
|
639
|
+
leafIndex: 0
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/protocol/note/NoteDecryptor.ts
|
|
644
|
+
function computeNullifier(commitment, spendingKey) {
|
|
645
|
+
return poseidon22([commitment, spendingKey]);
|
|
646
|
+
}
|
|
647
|
+
function commitmentHexOf(commitment) {
|
|
648
|
+
return toHex(bigintTo32Le(commitment));
|
|
649
|
+
}
|
|
650
|
+
function computeNoteCommitment(value, assetId, ownerPk, blinding) {
|
|
651
|
+
return poseidon42([value, assetId, ownerPk, blinding]);
|
|
652
|
+
}
|
|
653
|
+
function tryDecryptNote(commitment, viewingSecretKey, spendingKey, ownOwnerPk = 0n, opts) {
|
|
654
|
+
return tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwnerPk, opts).note;
|
|
655
|
+
}
|
|
656
|
+
function tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwnerPk = 0n, opts) {
|
|
657
|
+
if (!commitment.encryptedMemo) return { note: null, reason: "no_memo" };
|
|
658
|
+
let commitmentBytes;
|
|
659
|
+
let memoBytes;
|
|
660
|
+
try {
|
|
661
|
+
commitmentBytes = fromHex(commitment.commitmentHex);
|
|
662
|
+
memoBytes = fromHex(commitment.encryptedMemo);
|
|
663
|
+
} catch {
|
|
664
|
+
return { note: null, reason: "hex_parse_error" };
|
|
665
|
+
}
|
|
666
|
+
if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) {
|
|
667
|
+
return {
|
|
668
|
+
note: null,
|
|
669
|
+
reason: `memo_size_mismatch:got_${memoBytes.length}_expected_${ENCRYPTED_MEMO_SIZE}`
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
let sharedSecret = opts?.sharedSecret ?? null;
|
|
673
|
+
if (!sharedSecret && opts?.viewTag) {
|
|
674
|
+
sharedSecret = EncryptedMemo.extractSharedSecret(memoBytes, viewingSecretKey);
|
|
675
|
+
if (!sharedSecret) return { note: null, reason: "stealth_shared_secret_failed" };
|
|
676
|
+
if (!EncryptedMemo.checkViewTag(memoBytes, sharedSecret)) {
|
|
677
|
+
return { note: null, reason: "view_tag_mismatch" };
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const plaintext = sharedSecret ? EncryptedMemo.decryptWithSharedSecret(memoBytes, commitmentBytes, sharedSecret) : EncryptedMemo.decrypt(memoBytes, commitmentBytes, viewingSecretKey);
|
|
681
|
+
if (!plaintext) return { note: null, reason: "decrypt_failed:wrong_key_or_corrupt_mac" };
|
|
682
|
+
let effectiveOwnerPk = plaintext.ownerPk;
|
|
683
|
+
let effectiveSpendingKey = spendingKey;
|
|
684
|
+
if (ownOwnerPk !== 0n && plaintext.ownerPk !== ownOwnerPk) {
|
|
685
|
+
const ss = sharedSecret ?? EncryptedMemo.extractSharedSecret(memoBytes, viewingSecretKey);
|
|
686
|
+
if (!ss) return { note: null, reason: "stealth_shared_secret_failed" };
|
|
687
|
+
const ownPkPoint = recoverOwnerPkPoint(ownOwnerPk);
|
|
688
|
+
if (!ownPkPoint) return { note: null, reason: "stealth_invalid_own_owner_pk" };
|
|
689
|
+
const stealthOwnerPk = deriveStealthOwnerPk(ss, ownOwnerPk, ownPkPoint);
|
|
690
|
+
if (stealthOwnerPk !== plaintext.ownerPk) {
|
|
691
|
+
return { note: null, reason: "commitment_mismatch" };
|
|
692
|
+
}
|
|
693
|
+
effectiveOwnerPk = stealthOwnerPk;
|
|
694
|
+
effectiveSpendingKey = deriveStealthSk(ss, ownOwnerPk, spendingKey);
|
|
695
|
+
}
|
|
696
|
+
const recomputed = poseidon42([
|
|
697
|
+
plaintext.value,
|
|
698
|
+
plaintext.assetId,
|
|
699
|
+
effectiveOwnerPk,
|
|
700
|
+
plaintext.blinding
|
|
701
|
+
]);
|
|
702
|
+
if (recomputed !== bytesToBigintLE(commitmentBytes)) {
|
|
703
|
+
return { note: null, reason: "commitment_mismatch" };
|
|
704
|
+
}
|
|
705
|
+
const nullifier = poseidon22([recomputed, effectiveSpendingKey]);
|
|
706
|
+
return {
|
|
707
|
+
note: {
|
|
708
|
+
value: plaintext.value,
|
|
709
|
+
assetId: plaintext.assetId,
|
|
710
|
+
ownerPk: effectiveOwnerPk,
|
|
711
|
+
blinding: plaintext.blinding,
|
|
712
|
+
spendingKey: effectiveSpendingKey,
|
|
713
|
+
circuitVersion: plaintext.circuitVersion,
|
|
714
|
+
...isValidLeafIndex(commitment.leafIndex) ? { leafIndex: commitment.leafIndex } : {},
|
|
715
|
+
spent: false,
|
|
716
|
+
spentAt: null,
|
|
717
|
+
commitment: recomputed,
|
|
718
|
+
nullifier,
|
|
719
|
+
commitmentHex: toHex(bigintTo32Le(recomputed)),
|
|
720
|
+
nullifierHex: toHex(bigintTo32Le(nullifier)),
|
|
721
|
+
memo: Array.from(memoBytes),
|
|
722
|
+
counterpartyPk: plaintext.counterpartyPk
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/protocol/note/NoteDisclosure.ts
|
|
728
|
+
import { poseidon4 as poseidon43 } from "poseidon-lite";
|
|
729
|
+
|
|
730
|
+
// src/foundation/encoding/base64url.ts
|
|
731
|
+
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
732
|
+
function base64UrlEncode(input) {
|
|
733
|
+
const bytes = new TextEncoder().encode(input);
|
|
734
|
+
let out = "";
|
|
735
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
736
|
+
const a = bytes[i];
|
|
737
|
+
const b = bytes[i + 1];
|
|
738
|
+
const c = bytes[i + 2];
|
|
739
|
+
out += ALPHABET[a >> 2];
|
|
740
|
+
out += ALPHABET[(a & 3) << 4 | (b ?? 0) >> 4];
|
|
741
|
+
if (b === void 0) break;
|
|
742
|
+
out += ALPHABET[(b & 15) << 2 | (c ?? 0) >> 6];
|
|
743
|
+
if (c === void 0) break;
|
|
744
|
+
out += ALPHABET[c & 63];
|
|
745
|
+
}
|
|
746
|
+
return out;
|
|
747
|
+
}
|
|
748
|
+
function base64UrlDecode(input) {
|
|
749
|
+
const lookup = new Map([...ALPHABET].map((ch, i) => [ch, i]));
|
|
750
|
+
const bytes = [];
|
|
751
|
+
let buffer = 0;
|
|
752
|
+
let bits = 0;
|
|
753
|
+
for (const ch of input) {
|
|
754
|
+
const value = lookup.get(ch);
|
|
755
|
+
if (value === void 0) throw new Error(`Invalid base64url character: ${ch}`);
|
|
756
|
+
buffer = buffer << 6 | value;
|
|
757
|
+
bits += 6;
|
|
758
|
+
if (bits >= 8) {
|
|
759
|
+
bits -= 8;
|
|
760
|
+
bytes.push(buffer >> bits & 255);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return new TextDecoder().decode(new Uint8Array(bytes));
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// src/protocol/note/NoteDisclosure.ts
|
|
767
|
+
var PREFIX = "orbdisc:";
|
|
768
|
+
var VERSION = 1;
|
|
769
|
+
function toHex2(n) {
|
|
770
|
+
return "0x" + n.toString(16);
|
|
771
|
+
}
|
|
772
|
+
function fromHex2(s) {
|
|
773
|
+
return BigInt(s);
|
|
774
|
+
}
|
|
775
|
+
function createNoteDisclosureKey(note) {
|
|
776
|
+
const payload = {
|
|
777
|
+
v: VERSION,
|
|
778
|
+
c: toHex2(note.commitment),
|
|
779
|
+
val: toHex2(note.value),
|
|
780
|
+
aid: toHex2(note.assetId),
|
|
781
|
+
opk: toHex2(note.ownerPk),
|
|
782
|
+
bld: toHex2(note.blinding)
|
|
783
|
+
};
|
|
784
|
+
return PREFIX + base64UrlEncode(JSON.stringify(payload));
|
|
785
|
+
}
|
|
786
|
+
function decodeNoteDisclosureKey(key) {
|
|
787
|
+
try {
|
|
788
|
+
if (!key.startsWith(PREFIX)) return null;
|
|
789
|
+
const payload = JSON.parse(base64UrlDecode(key.slice(PREFIX.length)));
|
|
790
|
+
if (payload.v !== VERSION) return null;
|
|
791
|
+
const disclosure = {
|
|
792
|
+
commitment: fromHex2(payload.c),
|
|
793
|
+
value: fromHex2(payload.val),
|
|
794
|
+
assetId: fromHex2(payload.aid),
|
|
795
|
+
ownerPk: fromHex2(payload.opk),
|
|
796
|
+
blinding: fromHex2(payload.bld)
|
|
797
|
+
};
|
|
798
|
+
const recomputed = poseidon43([
|
|
799
|
+
disclosure.value,
|
|
800
|
+
disclosure.assetId,
|
|
801
|
+
disclosure.ownerPk,
|
|
802
|
+
disclosure.blinding
|
|
803
|
+
]);
|
|
804
|
+
if (recomputed !== disclosure.commitment) return null;
|
|
805
|
+
return disclosure;
|
|
806
|
+
} catch {
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// src/protocol/eph/selfEph.ts
|
|
812
|
+
import { sha256 as sha2563 } from "@noble/hashes/sha2.js";
|
|
813
|
+
import { packPoint as packPoint2, unpackPoint as unpackPoint3 } from "@zk-kit/baby-jubjub";
|
|
814
|
+
var SELF_EPH_DOMAIN = new TextEncoder().encode("orbinum-self-eph-v1");
|
|
815
|
+
function deriveSelfEphSk(spendingKey, index) {
|
|
816
|
+
const h = sha2563.create();
|
|
817
|
+
h.update(SELF_EPH_DOMAIN);
|
|
818
|
+
h.update(bigintTo32Le(spendingKey));
|
|
819
|
+
const idx = new Uint8Array(4);
|
|
820
|
+
new DataView(idx.buffer).setUint32(0, index >>> 0, true);
|
|
821
|
+
h.update(idx);
|
|
822
|
+
return h.digest();
|
|
823
|
+
}
|
|
824
|
+
function selfEphWindow(spendingKey, ivkPacked, from, count) {
|
|
825
|
+
const ivkPoint = unpackPoint3(bytesToBigintLE(ivkPacked));
|
|
826
|
+
if (!ivkPoint) throw new Error("selfEphWindow: invalid viewing public key");
|
|
827
|
+
const entries = [];
|
|
828
|
+
for (let i = from; i < from + count; i++) {
|
|
829
|
+
const scalar = bytesToBjjScalar(deriveSelfEphSk(spendingKey, i));
|
|
830
|
+
const ephPk = fastMulBase(scalar);
|
|
831
|
+
const sharedPoint = fastMulPoint(ivkPoint, scalar);
|
|
832
|
+
entries.push({
|
|
833
|
+
index: i,
|
|
834
|
+
ephPkHex: toHex(bigintTo32Le(packPoint2(ephPk))),
|
|
835
|
+
sharedSecret: bigintTo32Le(sharedPoint[0])
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
return entries;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// src/protocol/eph/pairwiseEph.ts
|
|
842
|
+
import { sha256 as sha2564 } from "@noble/hashes/sha2.js";
|
|
843
|
+
import { packPoint as packPoint3, unpackPoint as unpackPoint4 } from "@zk-kit/baby-jubjub";
|
|
844
|
+
var PAIRWISE_EPH_DOMAIN = new TextEncoder().encode("orbinum-pairwise-eph-v1");
|
|
845
|
+
function derivePairwiseSharedSecret(myViewingSk, theirIvkPacked) {
|
|
846
|
+
const theirPoint = unpackPoint4(bytesToBigintLE(theirIvkPacked));
|
|
847
|
+
if (!theirPoint) throw new Error("derivePairwiseSharedSecret: invalid viewing public key");
|
|
848
|
+
const shared = fastMulPoint(theirPoint, bytesToBjjScalar(myViewingSk));
|
|
849
|
+
return bigintTo32Le(shared[0]);
|
|
850
|
+
}
|
|
851
|
+
function derivePairwiseEphSk(pairSecret, index) {
|
|
852
|
+
const h = sha2564.create();
|
|
853
|
+
h.update(PAIRWISE_EPH_DOMAIN);
|
|
854
|
+
h.update(pairSecret);
|
|
855
|
+
const idx = new Uint8Array(4);
|
|
856
|
+
new DataView(idx.buffer).setUint32(0, index >>> 0, true);
|
|
857
|
+
h.update(idx);
|
|
858
|
+
return h.digest();
|
|
859
|
+
}
|
|
860
|
+
function pairwiseEphWindow(pairSecret, receiverIvkPacked, from, count) {
|
|
861
|
+
const ivkPoint = unpackPoint4(bytesToBigintLE(receiverIvkPacked));
|
|
862
|
+
if (!ivkPoint) throw new Error("pairwiseEphWindow: invalid viewing public key");
|
|
863
|
+
const entries = [];
|
|
864
|
+
for (let i = from; i < from + count; i++) {
|
|
865
|
+
const scalar = bytesToBjjScalar(derivePairwiseEphSk(pairSecret, i));
|
|
866
|
+
const ephPk = fastMulBase(scalar);
|
|
867
|
+
const sharedPoint = fastMulPoint(ivkPoint, scalar);
|
|
868
|
+
entries.push({
|
|
869
|
+
index: i,
|
|
870
|
+
ephPkHex: toHex(bigintTo32Le(packPoint3(ephPk))),
|
|
871
|
+
sharedSecret: bigintTo32Le(sharedPoint[0])
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
return entries;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// src/protocol/keys/PrivacyKeys.ts
|
|
878
|
+
import { hkdf as hkdf2 } from "@noble/hashes/hkdf.js";
|
|
879
|
+
import { sha256 as sha2565 } from "@noble/hashes/sha2.js";
|
|
880
|
+
import { packPoint as packPoint4 } from "@zk-kit/baby-jubjub";
|
|
881
|
+
var IVK_DOMAIN = new TextEncoder().encode("orbinum-ivk-v1");
|
|
882
|
+
var KEY_VERSION = "v2";
|
|
883
|
+
function deriveSpendingKeyFromMaster(masterBytes) {
|
|
884
|
+
const skBigint = BigInt(toHex(masterBytes)) % BABYJUB_SUBORDER;
|
|
885
|
+
return skBigint === 0n ? 1n : skBigint;
|
|
886
|
+
}
|
|
887
|
+
function deriveViewingSecretKey(spendingKey) {
|
|
888
|
+
const ikm = bigintTo32Le(spendingKey);
|
|
889
|
+
return hkdf2(sha2565, ikm, void 0, IVK_DOMAIN, 32);
|
|
890
|
+
}
|
|
891
|
+
function deriveViewingPublicKey(ivsk) {
|
|
892
|
+
const ivskScalar = BigInt(toHex(ivsk)) % BABYJUB_SUBORDER || 1n;
|
|
893
|
+
const ivkPoint = fastMulBase(ivskScalar);
|
|
894
|
+
const packed = packPoint4(ivkPoint);
|
|
895
|
+
return bigintTo32Le(packed);
|
|
896
|
+
}
|
|
897
|
+
function deriveOwnerPk(spendingKey) {
|
|
898
|
+
try {
|
|
899
|
+
const pubPoint = fastMulBase(spendingKey);
|
|
900
|
+
return pubPoint[0];
|
|
901
|
+
} catch {
|
|
902
|
+
return 0n;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// src/wallet/worker/kernel/types.ts
|
|
907
|
+
var SELF_EPH_WINDOW = 1024;
|
|
908
|
+
var PAIRWISE_EPH_WINDOW = 64;
|
|
909
|
+
var EMPTY_BATCH_RESULT = {
|
|
910
|
+
notes: [],
|
|
911
|
+
tagFiltered: 0,
|
|
912
|
+
selfMatched: 0,
|
|
913
|
+
pairwiseMatched: 0,
|
|
914
|
+
maxSelfEphIndex: null
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
// src/wallet/worker/kernel/ephWindow.ts
|
|
918
|
+
var cachedWindow = null;
|
|
919
|
+
function clearKnownEphWindow() {
|
|
920
|
+
cachedWindow = null;
|
|
921
|
+
}
|
|
922
|
+
function bytesKey(bytes) {
|
|
923
|
+
let out = "";
|
|
924
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
925
|
+
return out;
|
|
926
|
+
}
|
|
927
|
+
function getKnownEphWindow(keys) {
|
|
928
|
+
const selfSize = keys.selfEphWindowSize ?? SELF_EPH_WINDOW;
|
|
929
|
+
const pairSize = keys.pairwiseWindowSize ?? PAIRWISE_EPH_WINDOW;
|
|
930
|
+
const counterparties = keys.pairwiseCounterparties ?? [];
|
|
931
|
+
if (!keys.selfEph && counterparties.length === 0) return null;
|
|
932
|
+
const cacheKey = `${keys.spendingKey.toString(16)}:${keys.selfEph ? selfSize : 0}:${pairSize}:` + counterparties.map((c) => bytesKey(c)).join(",");
|
|
933
|
+
if (cachedWindow?.cacheKey === cacheKey) return cachedWindow.window;
|
|
934
|
+
try {
|
|
935
|
+
const ivkPacked = deriveViewingPublicKey(keys.viewingKey);
|
|
936
|
+
const byEphPk = /* @__PURE__ */ new Map();
|
|
937
|
+
if (keys.selfEph) {
|
|
938
|
+
for (const e of selfEphWindow(keys.spendingKey, ivkPacked, 0, selfSize)) {
|
|
939
|
+
byEphPk.set(e.ephPkHex.toLowerCase(), {
|
|
940
|
+
sharedSecret: e.sharedSecret,
|
|
941
|
+
index: e.index,
|
|
942
|
+
source: "self"
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
for (const theirIvk of counterparties) {
|
|
947
|
+
const pairSecret = derivePairwiseSharedSecret(keys.viewingKey, theirIvk);
|
|
948
|
+
for (const e of pairwiseEphWindow(pairSecret, ivkPacked, 0, pairSize)) {
|
|
949
|
+
const hex = e.ephPkHex.toLowerCase();
|
|
950
|
+
if (!byEphPk.has(hex)) {
|
|
951
|
+
byEphPk.set(hex, {
|
|
952
|
+
sharedSecret: e.sharedSecret,
|
|
953
|
+
index: e.index,
|
|
954
|
+
source: "pairwise"
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
cachedWindow = { cacheKey, window: { byEphPk } };
|
|
960
|
+
return cachedWindow.window;
|
|
961
|
+
} catch {
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/wallet/worker/kernel/decryptBatch.ts
|
|
967
|
+
function hintEphPkHex(hint) {
|
|
968
|
+
if (hint.ephPkHex) return hint.ephPkHex.toLowerCase();
|
|
969
|
+
const memo = hint.encryptedMemo;
|
|
970
|
+
if (!memo || memo.length < 66) return null;
|
|
971
|
+
return ("0x" + memo.slice(-64)).toLowerCase();
|
|
972
|
+
}
|
|
973
|
+
function decryptHintBatch(hints, keys) {
|
|
974
|
+
const activation = keys.viewTagActivationLeaf ?? null;
|
|
975
|
+
const knownWindow = getKnownEphWindow(keys);
|
|
976
|
+
let tagFiltered = 0;
|
|
977
|
+
let selfMatched = 0;
|
|
978
|
+
let pairwiseMatched = 0;
|
|
979
|
+
let maxSelfEphIndex = null;
|
|
980
|
+
const notes = hints.map((hint) => {
|
|
981
|
+
try {
|
|
982
|
+
const known = knownWindow?.byEphPk.get(hintEphPkHex(hint) ?? "");
|
|
983
|
+
if (known) {
|
|
984
|
+
const result2 = tryDecryptNoteVerbose(
|
|
985
|
+
hint,
|
|
986
|
+
keys.viewingKey,
|
|
987
|
+
keys.spendingKey,
|
|
988
|
+
keys.ownerPk,
|
|
989
|
+
{ sharedSecret: known.sharedSecret }
|
|
990
|
+
);
|
|
991
|
+
if (result2.note) {
|
|
992
|
+
if (known.source === "self") {
|
|
993
|
+
selfMatched++;
|
|
994
|
+
if (maxSelfEphIndex === null || known.index > maxSelfEphIndex) {
|
|
995
|
+
maxSelfEphIndex = known.index;
|
|
996
|
+
}
|
|
997
|
+
} else {
|
|
998
|
+
pairwiseMatched++;
|
|
999
|
+
}
|
|
1000
|
+
return result2.note;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
const viewTag = activation !== null && hint.leafIndex >= activation;
|
|
1004
|
+
const result = tryDecryptNoteVerbose(
|
|
1005
|
+
hint,
|
|
1006
|
+
keys.viewingKey,
|
|
1007
|
+
keys.spendingKey,
|
|
1008
|
+
keys.ownerPk,
|
|
1009
|
+
{ viewTag }
|
|
1010
|
+
);
|
|
1011
|
+
if (result.reason === "view_tag_mismatch") tagFiltered++;
|
|
1012
|
+
return result.note;
|
|
1013
|
+
} catch {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
return { notes, tagFiltered, selfMatched, pairwiseMatched, maxSelfEphIndex };
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// src/foundation/errors/abort.ts
|
|
1021
|
+
function scanAbortError() {
|
|
1022
|
+
if (typeof DOMException !== "undefined") {
|
|
1023
|
+
return new DOMException("Scan aborted", "AbortError");
|
|
1024
|
+
}
|
|
1025
|
+
const err = new Error("Scan aborted");
|
|
1026
|
+
err.name = "AbortError";
|
|
1027
|
+
return err;
|
|
1028
|
+
}
|
|
1029
|
+
function isAbortError(err) {
|
|
1030
|
+
return err instanceof Error && err.name === "AbortError";
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// src/wallet/worker/pool/types.ts
|
|
1034
|
+
var MAX_WORKERS = 4;
|
|
1035
|
+
var DECRYPT_YIELD_EVERY = 25;
|
|
1036
|
+
var WORKER_CRASHED = "Decrypt worker crashed";
|
|
1037
|
+
|
|
1038
|
+
// src/wallet/worker/pool/mainThreadPool.ts
|
|
1039
|
+
function yieldToBrowser() {
|
|
1040
|
+
const scheduler = globalThis.scheduler;
|
|
1041
|
+
if (scheduler?.yield) return scheduler.yield();
|
|
1042
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
1043
|
+
}
|
|
1044
|
+
function createMainThreadPool() {
|
|
1045
|
+
return {
|
|
1046
|
+
async decryptBatch(hints, keys, signal) {
|
|
1047
|
+
const notes = [];
|
|
1048
|
+
let tagFiltered = 0;
|
|
1049
|
+
let selfMatched = 0;
|
|
1050
|
+
let pairwiseMatched = 0;
|
|
1051
|
+
let maxSelfEphIndex = null;
|
|
1052
|
+
for (let i = 0; i < hints.length; i += DECRYPT_YIELD_EVERY) {
|
|
1053
|
+
if (i > 0) {
|
|
1054
|
+
await yieldToBrowser();
|
|
1055
|
+
if (signal?.aborted) throw scanAbortError();
|
|
1056
|
+
}
|
|
1057
|
+
const burst = decryptHintBatch(hints.slice(i, i + DECRYPT_YIELD_EVERY), keys);
|
|
1058
|
+
notes.push(...burst.notes);
|
|
1059
|
+
tagFiltered += burst.tagFiltered;
|
|
1060
|
+
selfMatched += burst.selfMatched;
|
|
1061
|
+
pairwiseMatched += burst.pairwiseMatched;
|
|
1062
|
+
if (burst.maxSelfEphIndex !== null) {
|
|
1063
|
+
maxSelfEphIndex = Math.max(maxSelfEphIndex ?? -1, burst.maxSelfEphIndex);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
return { notes, tagFiltered, selfMatched, pairwiseMatched, maxSelfEphIndex };
|
|
1067
|
+
},
|
|
1068
|
+
terminate() {
|
|
1069
|
+
clearKnownEphWindow();
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// src/wallet/worker/pool/workerRpc.ts
|
|
1075
|
+
function runOnWorker(worker, payload, signal) {
|
|
1076
|
+
return new Promise((resolve, reject) => {
|
|
1077
|
+
const onAbort = () => {
|
|
1078
|
+
cleanup();
|
|
1079
|
+
reject(scanAbortError());
|
|
1080
|
+
};
|
|
1081
|
+
const cleanup = () => {
|
|
1082
|
+
worker.onmessage = null;
|
|
1083
|
+
worker.onerror = null;
|
|
1084
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1085
|
+
};
|
|
1086
|
+
worker.onmessage = (event) => {
|
|
1087
|
+
cleanup();
|
|
1088
|
+
const data = event.data;
|
|
1089
|
+
if (data.error !== void 0) {
|
|
1090
|
+
reject(new Error(data.error));
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
resolve({
|
|
1094
|
+
notes: data.notes ?? [],
|
|
1095
|
+
tagFiltered: data.tagFiltered ?? 0,
|
|
1096
|
+
selfMatched: data.selfMatched ?? 0,
|
|
1097
|
+
pairwiseMatched: data.pairwiseMatched ?? 0,
|
|
1098
|
+
maxSelfEphIndex: data.maxSelfEphIndex ?? null
|
|
1099
|
+
});
|
|
1100
|
+
};
|
|
1101
|
+
worker.onerror = () => {
|
|
1102
|
+
cleanup();
|
|
1103
|
+
reject(new Error(WORKER_CRASHED));
|
|
1104
|
+
};
|
|
1105
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1106
|
+
worker.postMessage(payload);
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// src/wallet/worker/pool/workerPool.ts
|
|
1111
|
+
function sliceForWorkers(hints, size) {
|
|
1112
|
+
const sliceLength = Math.ceil(hints.length / size);
|
|
1113
|
+
const slices = [];
|
|
1114
|
+
for (let i = 0; i < hints.length; i += sliceLength) {
|
|
1115
|
+
slices.push(hints.slice(i, i + sliceLength));
|
|
1116
|
+
}
|
|
1117
|
+
return slices;
|
|
1118
|
+
}
|
|
1119
|
+
function mergeResults(results) {
|
|
1120
|
+
return {
|
|
1121
|
+
notes: results.flatMap((r) => r.notes),
|
|
1122
|
+
tagFiltered: results.reduce((sum, r) => sum + r.tagFiltered, 0),
|
|
1123
|
+
selfMatched: results.reduce((sum, r) => sum + r.selfMatched, 0),
|
|
1124
|
+
pairwiseMatched: results.reduce((sum, r) => sum + r.pairwiseMatched, 0),
|
|
1125
|
+
maxSelfEphIndex: results.reduce(
|
|
1126
|
+
(max, r) => r.maxSelfEphIndex === null ? max : Math.max(max ?? -1, r.maxSelfEphIndex),
|
|
1127
|
+
null
|
|
1128
|
+
)
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
function createWorkerPool(factory, size) {
|
|
1132
|
+
const workers = [];
|
|
1133
|
+
const workerAt = (i) => workers[i] ??= factory();
|
|
1134
|
+
return {
|
|
1135
|
+
async decryptBatch(hints, keys, signal) {
|
|
1136
|
+
if (signal?.aborted) throw scanAbortError();
|
|
1137
|
+
if (hints.length === 0) return { ...EMPTY_BATCH_RESULT, notes: [] };
|
|
1138
|
+
try {
|
|
1139
|
+
const results = await Promise.all(
|
|
1140
|
+
sliceForWorkers(hints, size).map(
|
|
1141
|
+
(slice, i) => runOnWorker(workerAt(i), { hints: slice, keys }, signal)
|
|
1142
|
+
)
|
|
1143
|
+
);
|
|
1144
|
+
return mergeResults(results);
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
this.terminate();
|
|
1147
|
+
throw err;
|
|
1148
|
+
}
|
|
1149
|
+
},
|
|
1150
|
+
terminate() {
|
|
1151
|
+
for (const worker of workers.splice(0)) worker.terminate();
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// src/wallet/worker/pool/createDecryptPool.ts
|
|
1157
|
+
function createDecryptPool(options) {
|
|
1158
|
+
const { factory } = options;
|
|
1159
|
+
if (!factory) return createMainThreadPool();
|
|
1160
|
+
let pool = createWorkerPool(factory, Math.min(options.size ?? MAX_WORKERS, MAX_WORKERS));
|
|
1161
|
+
return {
|
|
1162
|
+
async decryptBatch(hints, keys, signal) {
|
|
1163
|
+
try {
|
|
1164
|
+
return await pool.decryptBatch(hints, keys, signal);
|
|
1165
|
+
} catch (err) {
|
|
1166
|
+
if (!(err instanceof Error) || err.message !== WORKER_CRASHED) throw err;
|
|
1167
|
+
pool = createMainThreadPool();
|
|
1168
|
+
return pool.decryptBatch(hints, keys, signal);
|
|
1169
|
+
}
|
|
1170
|
+
},
|
|
1171
|
+
terminate: () => pool.terminate()
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
export {
|
|
1176
|
+
toHex,
|
|
1177
|
+
fromHex,
|
|
1178
|
+
ensureHexPrefix,
|
|
1179
|
+
hexToNumber,
|
|
1180
|
+
hexToBigint,
|
|
1181
|
+
scalarToHex,
|
|
1182
|
+
bigintTo32Le,
|
|
1183
|
+
bytesToBigintLE,
|
|
1184
|
+
bigintTo32Be,
|
|
1185
|
+
bigintTo32LeArr,
|
|
1186
|
+
computePathIndices,
|
|
1187
|
+
leHexToBigint,
|
|
1188
|
+
base64UrlEncode,
|
|
1189
|
+
base64UrlDecode,
|
|
1190
|
+
BN254_R,
|
|
1191
|
+
BABYJUB_SUBORDER,
|
|
1192
|
+
recoverOwnerPkPoint,
|
|
1193
|
+
fastMulBase,
|
|
1194
|
+
fastMulPoint,
|
|
1195
|
+
deriveStealthOwnerPk,
|
|
1196
|
+
deriveStealthSk,
|
|
1197
|
+
scanAbortError,
|
|
1198
|
+
isAbortError,
|
|
1199
|
+
serializeMemo,
|
|
1200
|
+
deriveViewTag,
|
|
1201
|
+
ENCRYPTED_MEMO_SIZE,
|
|
1202
|
+
bytesToBjjScalar,
|
|
1203
|
+
EncryptedMemo,
|
|
1204
|
+
deriveSelfEphSk,
|
|
1205
|
+
selfEphWindow,
|
|
1206
|
+
derivePairwiseSharedSecret,
|
|
1207
|
+
derivePairwiseEphSk,
|
|
1208
|
+
pairwiseEphWindow,
|
|
1209
|
+
CURRENT_CIRCUIT_VERSION,
|
|
1210
|
+
NoteBuilder,
|
|
1211
|
+
LEAVES_PER_TREE,
|
|
1212
|
+
isValidLeafIndex,
|
|
1213
|
+
treeIdOf,
|
|
1214
|
+
isSpendable,
|
|
1215
|
+
canPairWith,
|
|
1216
|
+
selectNotes,
|
|
1217
|
+
buildDummyTransferInput,
|
|
1218
|
+
computeNullifier,
|
|
1219
|
+
commitmentHexOf,
|
|
1220
|
+
computeNoteCommitment,
|
|
1221
|
+
tryDecryptNote,
|
|
1222
|
+
tryDecryptNoteVerbose,
|
|
1223
|
+
createNoteDisclosureKey,
|
|
1224
|
+
decodeNoteDisclosureKey,
|
|
1225
|
+
KEY_VERSION,
|
|
1226
|
+
deriveSpendingKeyFromMaster,
|
|
1227
|
+
deriveViewingSecretKey,
|
|
1228
|
+
deriveViewingPublicKey,
|
|
1229
|
+
deriveOwnerPk,
|
|
1230
|
+
SELF_EPH_WINDOW,
|
|
1231
|
+
PAIRWISE_EPH_WINDOW,
|
|
1232
|
+
EMPTY_BATCH_RESULT,
|
|
1233
|
+
clearKnownEphWindow,
|
|
1234
|
+
getKnownEphWindow,
|
|
1235
|
+
decryptHintBatch,
|
|
1236
|
+
MAX_WORKERS,
|
|
1237
|
+
DECRYPT_YIELD_EVERY,
|
|
1238
|
+
WORKER_CRASHED,
|
|
1239
|
+
createMainThreadPool,
|
|
1240
|
+
createWorkerPool,
|
|
1241
|
+
createDecryptPool
|
|
1242
|
+
};
|