@unlink-xyz/core 0.1.0 → 0.1.2
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/.eslintrc.json +4 -0
- package/account/zkAccount.test.ts +316 -0
- package/account/zkAccount.ts +222 -0
- package/clients/broadcaster.ts +67 -0
- package/clients/http.ts +94 -0
- package/clients/indexer.ts +150 -0
- package/config.ts +39 -0
- package/core.ts +17 -0
- package/dist/account/railgun-imports-prototype.d.ts +12 -0
- package/dist/account/railgun-imports-prototype.d.ts.map +1 -0
- package/dist/account/railgun-imports-prototype.js +30 -0
- package/dist/clients/indexer.d.ts.map +1 -1
- package/dist/clients/indexer.js +1 -1
- package/dist/state/hydrator.d.ts +16 -0
- package/dist/state/hydrator.d.ts.map +1 -0
- package/dist/state/hydrator.js +18 -0
- package/dist/state/job-store.d.ts +12 -0
- package/dist/state/job-store.d.ts.map +1 -0
- package/dist/state/job-store.js +118 -0
- package/dist/state/jobs.d.ts +50 -0
- package/dist/state/jobs.d.ts.map +1 -0
- package/dist/state/jobs.js +1 -0
- package/dist/state.d.ts +83 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +171 -0
- package/dist/transactions/deposit.d.ts +0 -2
- package/dist/transactions/deposit.d.ts.map +1 -1
- package/dist/transactions/deposit.js +5 -9
- package/dist/transactions/note-sync.d.ts.map +1 -1
- package/dist/transactions/note-sync.js +1 -1
- package/dist/transactions/shield.d.ts +5 -0
- package/dist/transactions/shield.d.ts.map +1 -0
- package/dist/transactions/shield.js +93 -0
- package/dist/transactions/transact.d.ts +0 -5
- package/dist/transactions/transact.d.ts.map +1 -1
- package/dist/transactions/transact.js +2 -2
- package/dist/transactions/utils.d.ts +10 -0
- package/dist/transactions/utils.d.ts.map +1 -0
- package/dist/transactions/utils.js +17 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/utils/time.d.ts +2 -0
- package/dist/utils/time.d.ts.map +1 -0
- package/dist/utils/time.js +3 -0
- package/dist/utils/witness.d.ts +11 -0
- package/dist/utils/witness.d.ts.map +1 -0
- package/dist/utils/witness.js +19 -0
- package/errors.ts +20 -0
- package/index.ts +17 -0
- package/key-derivation/babyjubjub.ts +11 -0
- package/key-derivation/bech32.test.ts +90 -0
- package/key-derivation/bech32.ts +124 -0
- package/key-derivation/bip32.ts +56 -0
- package/key-derivation/bip39.ts +76 -0
- package/key-derivation/bytes.ts +118 -0
- package/key-derivation/hash.ts +13 -0
- package/key-derivation/index.ts +7 -0
- package/key-derivation/wallet-node.ts +155 -0
- package/keys.ts +47 -0
- package/package.json +4 -5
- package/prover/config.ts +104 -0
- package/prover/index.ts +1 -0
- package/prover/prover.integration.test.ts +162 -0
- package/prover/prover.test.ts +309 -0
- package/prover/prover.ts +405 -0
- package/prover/registry.test.ts +90 -0
- package/prover/registry.ts +82 -0
- package/schema.ts +17 -0
- package/setup-artifacts.sh +57 -0
- package/state/index.ts +2 -0
- package/state/merkle/hydrator.ts +69 -0
- package/state/merkle/index.ts +12 -0
- package/state/merkle/merkle-tree.test.ts +50 -0
- package/state/merkle/merkle-tree.ts +163 -0
- package/state/store/ciphertext-store.ts +28 -0
- package/state/store/index.ts +24 -0
- package/state/store/job-store.ts +162 -0
- package/state/store/jobs.ts +64 -0
- package/state/store/leaf-store.ts +39 -0
- package/state/store/note-store.ts +177 -0
- package/state/store/nullifier-store.ts +39 -0
- package/state/store/records.ts +61 -0
- package/state/store/root-store.ts +34 -0
- package/state/store/store.ts +25 -0
- package/state.test.ts +235 -0
- package/storage/index.ts +3 -0
- package/storage/indexeddb.test.ts +99 -0
- package/storage/indexeddb.ts +235 -0
- package/storage/memory.test.ts +59 -0
- package/storage/memory.ts +93 -0
- package/transactions/deposit.test.ts +160 -0
- package/transactions/deposit.ts +227 -0
- package/transactions/index.ts +20 -0
- package/transactions/note-sync.test.ts +155 -0
- package/transactions/note-sync.ts +452 -0
- package/transactions/reconcile.ts +73 -0
- package/transactions/transact.test.ts +451 -0
- package/transactions/transact.ts +811 -0
- package/transactions/types.ts +141 -0
- package/tsconfig.json +14 -0
- package/types/global.d.ts +15 -0
- package/types.ts +24 -0
- package/utils/async.ts +15 -0
- package/utils/bigint.ts +34 -0
- package/utils/crypto.test.ts +69 -0
- package/utils/crypto.ts +58 -0
- package/utils/json-codec.ts +38 -0
- package/utils/polling.ts +6 -0
- package/utils/signature.ts +16 -0
- package/utils/validators.test.ts +64 -0
- package/utils/validators.ts +86 -0
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { Mnemonic } from "../key-derivation/bip39.js";
|
|
4
|
+
import { ByteUtils } from "../key-derivation/bytes.js";
|
|
5
|
+
import {
|
|
6
|
+
deriveNodesFromSeed,
|
|
7
|
+
WalletNode,
|
|
8
|
+
} from "../key-derivation/wallet-node.js";
|
|
9
|
+
import { createMemoryStorage } from "../storage/memory.js";
|
|
10
|
+
import {
|
|
11
|
+
deriveZkAccount,
|
|
12
|
+
deriveZkAccountFromMnemonic,
|
|
13
|
+
generateMasterSeed,
|
|
14
|
+
importMasterMnemonic,
|
|
15
|
+
loadMasterMnemonic,
|
|
16
|
+
loadMasterSeed,
|
|
17
|
+
MASTER_MNEMONIC_KEY,
|
|
18
|
+
MASTER_SEED_KEY,
|
|
19
|
+
MASTER_SEED_LENGTH,
|
|
20
|
+
storeMasterMnemonic,
|
|
21
|
+
type MasterSeedCrypto,
|
|
22
|
+
} from "./zkAccount.js";
|
|
23
|
+
|
|
24
|
+
describe("deriveZkAccount", () => {
|
|
25
|
+
const seed = Uint8Array.from(
|
|
26
|
+
{ length: MASTER_SEED_LENGTH },
|
|
27
|
+
(_, i) => i & 0xff,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
it("derives wallet key material", async () => {
|
|
31
|
+
const account = await deriveZkAccount(seed, 0);
|
|
32
|
+
|
|
33
|
+
expect(account.spendingKeyPair.privateKey).toHaveLength(32);
|
|
34
|
+
expect(typeof account.spendingKeyPair.pubkey[0]).toBe("bigint");
|
|
35
|
+
expect(typeof account.spendingKeyPair.pubkey[1]).toBe("bigint");
|
|
36
|
+
|
|
37
|
+
expect(account.viewingKeyPair.privateKey).toHaveLength(32);
|
|
38
|
+
expect(account.viewingKeyPair.pubkey).toHaveLength(32);
|
|
39
|
+
|
|
40
|
+
expect(account.nullifyingKey).toBeTypeOf("bigint");
|
|
41
|
+
|
|
42
|
+
const expectedNullifyingKey = await deriveNodesFromSeed(
|
|
43
|
+
seed,
|
|
44
|
+
0,
|
|
45
|
+
).viewing.getNullifyingKey();
|
|
46
|
+
expect(account.nullifyingKey).toBe(expectedNullifyingKey);
|
|
47
|
+
|
|
48
|
+
const expectedMPK = WalletNode.getMasterPublicKey(
|
|
49
|
+
account.spendingKeyPair.pubkey,
|
|
50
|
+
account.nullifyingKey,
|
|
51
|
+
);
|
|
52
|
+
expect(account.masterPublicKey).toBe(expectedMPK);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("derives distinct accounts per index", async () => {
|
|
56
|
+
const first = await deriveZkAccount(seed, 0);
|
|
57
|
+
const second = await deriveZkAccount(seed, 1);
|
|
58
|
+
|
|
59
|
+
expect(first.spendingKeyPair.privateKey).not.toEqual(
|
|
60
|
+
second.spendingKeyPair.privateKey,
|
|
61
|
+
);
|
|
62
|
+
expect(first.viewingKeyPair.privateKey).not.toEqual(
|
|
63
|
+
second.viewingKeyPair.privateKey,
|
|
64
|
+
);
|
|
65
|
+
expect(first.nullifyingKey).not.toEqual(second.nullifyingKey);
|
|
66
|
+
expect(first.masterPublicKey).not.toEqual(second.masterPublicKey);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("rejects negative indices", async () => {
|
|
70
|
+
await expect(deriveZkAccount(seed, -1)).rejects.toThrow(
|
|
71
|
+
"accountIndex must be a non-negative integer",
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("rejects non-integer indices", async () => {
|
|
76
|
+
await expect(deriveZkAccount(seed, Number.NaN)).rejects.toThrow(
|
|
77
|
+
"accountIndex must be a non-negative integer",
|
|
78
|
+
);
|
|
79
|
+
await expect(deriveZkAccount(seed, 0.25)).rejects.toThrow(
|
|
80
|
+
"accountIndex must be a non-negative integer",
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("rejects seeds that are not 64 bytes", async () => {
|
|
85
|
+
const shortSeed = Uint8Array.from({ length: 32 }, () => 1);
|
|
86
|
+
await expect(deriveZkAccount(shortSeed, 0)).rejects.toThrow(
|
|
87
|
+
"master seed must be 64 bytes",
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("derives the same keys when using mnemonic helper", async () => {
|
|
92
|
+
const entropy = Uint8Array.from({ length: 32 }, (_, i) => (i * 19) & 0xff);
|
|
93
|
+
const mnemonic = Mnemonic.fromEntropy(ByteUtils.bytesToHex(entropy));
|
|
94
|
+
const seedFromMnemonic = ByteUtils.hexStringToBytes(
|
|
95
|
+
Mnemonic.toSeed(mnemonic),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const fromSeed = await deriveZkAccount(seedFromMnemonic, 2);
|
|
99
|
+
const fromMnemonic = await deriveZkAccountFromMnemonic(mnemonic, 2);
|
|
100
|
+
|
|
101
|
+
expect(fromMnemonic).toEqual(fromSeed);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("generateMasterSeed", () => {
|
|
106
|
+
const fixtureEntropy = Uint8Array.from(
|
|
107
|
+
{ length: 32 },
|
|
108
|
+
(_, i) => (i * 13) & 0xff,
|
|
109
|
+
);
|
|
110
|
+
const fixtureMnemonic = Mnemonic.fromEntropy(
|
|
111
|
+
ByteUtils.bytesToHex(fixtureEntropy),
|
|
112
|
+
);
|
|
113
|
+
const fixtureSeed = ByteUtils.hexStringToBytes(
|
|
114
|
+
Mnemonic.toSeed(fixtureMnemonic),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
function rngFixture(): (n: number) => Uint8Array {
|
|
118
|
+
return (n) => {
|
|
119
|
+
expect(n).toBe(32);
|
|
120
|
+
return new Uint8Array(fixtureEntropy);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
it("creates and persists a new seed when none exists", async () => {
|
|
125
|
+
const storage = createMemoryStorage();
|
|
126
|
+
|
|
127
|
+
const seed = await generateMasterSeed({
|
|
128
|
+
storage,
|
|
129
|
+
rng: rngFixture(),
|
|
130
|
+
mnemonicPassphrase: "",
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(seed).toEqual(fixtureSeed);
|
|
134
|
+
const stored = await storage.get(MASTER_SEED_KEY);
|
|
135
|
+
expect(stored).not.toBeNull();
|
|
136
|
+
expect(stored).toEqual(fixtureSeed);
|
|
137
|
+
|
|
138
|
+
const mnemonic = await loadMasterMnemonic(storage);
|
|
139
|
+
expect(mnemonic).toEqual(fixtureMnemonic);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("returns existing seed without invoking rng", async () => {
|
|
143
|
+
const storage = createMemoryStorage();
|
|
144
|
+
await storage.put(MASTER_SEED_KEY, fixtureSeed);
|
|
145
|
+
|
|
146
|
+
const seed = await generateMasterSeed({
|
|
147
|
+
storage,
|
|
148
|
+
rng: () => {
|
|
149
|
+
throw new Error("rng should not be called when seed exists");
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(seed).toEqual(fixtureSeed);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("overwrites when requested", async () => {
|
|
157
|
+
const storage = createMemoryStorage();
|
|
158
|
+
const existing = Uint8Array.from(
|
|
159
|
+
{ length: MASTER_SEED_LENGTH },
|
|
160
|
+
(_, i) => (i * 3) & 0xff,
|
|
161
|
+
);
|
|
162
|
+
await storage.put(MASTER_SEED_KEY, existing);
|
|
163
|
+
const replacementEntropy = Uint8Array.from(
|
|
164
|
+
{ length: 32 },
|
|
165
|
+
(_, i) => (i * 7) & 0xff,
|
|
166
|
+
);
|
|
167
|
+
const replacementMnemonic = Mnemonic.fromEntropy(
|
|
168
|
+
ByteUtils.bytesToHex(replacementEntropy),
|
|
169
|
+
);
|
|
170
|
+
const replacementSeed = ByteUtils.hexStringToBytes(
|
|
171
|
+
Mnemonic.toSeed(replacementMnemonic),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const seed = await generateMasterSeed({
|
|
175
|
+
storage,
|
|
176
|
+
rng: (n) => {
|
|
177
|
+
expect(n).toBe(32);
|
|
178
|
+
return new Uint8Array(replacementEntropy);
|
|
179
|
+
},
|
|
180
|
+
overwrite: true,
|
|
181
|
+
mnemonicPassphrase: "",
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(seed).toEqual(replacementSeed);
|
|
185
|
+
const stored = await storage.get(MASTER_SEED_KEY);
|
|
186
|
+
expect(stored).toEqual(replacementSeed);
|
|
187
|
+
const mnemonic = await loadMasterMnemonic(storage);
|
|
188
|
+
expect(mnemonic).toEqual(replacementMnemonic);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("applies custom crypto hooks", async () => {
|
|
192
|
+
const storage = createMemoryStorage();
|
|
193
|
+
|
|
194
|
+
const crypto: MasterSeedCrypto = {
|
|
195
|
+
encrypt(seed) {
|
|
196
|
+
return seed.map((byte) => byte ^ 0xff);
|
|
197
|
+
},
|
|
198
|
+
decrypt(payload) {
|
|
199
|
+
return payload.map((byte) => byte ^ 0xff);
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const seed = await generateMasterSeed({
|
|
204
|
+
storage,
|
|
205
|
+
rng: rngFixture(),
|
|
206
|
+
crypto,
|
|
207
|
+
mnemonicPassphrase: "",
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
expect(seed).toEqual(fixtureSeed);
|
|
211
|
+
const roundTrip = await loadMasterSeed(storage, crypto);
|
|
212
|
+
expect(roundTrip).toEqual(fixtureSeed);
|
|
213
|
+
|
|
214
|
+
const storedMnemonic = await storage.get(MASTER_MNEMONIC_KEY);
|
|
215
|
+
expect(storedMnemonic).toEqual(
|
|
216
|
+
textEncode(fixtureMnemonic).map((byte) => byte ^ 0xff),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const mnemonicRoundTrip = await loadMasterMnemonic(storage, crypto);
|
|
220
|
+
expect(mnemonicRoundTrip).toEqual(fixtureMnemonic);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("returns null when no seed is stored", async () => {
|
|
224
|
+
const storage = createMemoryStorage();
|
|
225
|
+
|
|
226
|
+
const result = await loadMasterSeed(storage);
|
|
227
|
+
|
|
228
|
+
expect(result).toBeNull();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("throws if rng returns incorrect length", async () => {
|
|
232
|
+
const storage = createMemoryStorage();
|
|
233
|
+
|
|
234
|
+
await expect(
|
|
235
|
+
generateMasterSeed({
|
|
236
|
+
storage,
|
|
237
|
+
rng: () => new Uint8Array(16),
|
|
238
|
+
mnemonicPassphrase: "",
|
|
239
|
+
}),
|
|
240
|
+
).rejects.toThrow(/mnemonic entropy must be 32 bytes/);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("reconstitutes seed from stored mnemonic when seed missing", async () => {
|
|
244
|
+
const storage = createMemoryStorage();
|
|
245
|
+
await storeMnemonicFixture(storage, fixtureMnemonic);
|
|
246
|
+
|
|
247
|
+
const seed = await generateMasterSeed({
|
|
248
|
+
storage,
|
|
249
|
+
rng: rngFixture(),
|
|
250
|
+
mnemonicPassphrase: "",
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(seed).toEqual(fixtureSeed);
|
|
254
|
+
const stored = await storage.get(MASTER_SEED_KEY);
|
|
255
|
+
expect(stored).toEqual(fixtureSeed);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("importMasterMnemonic", () => {
|
|
260
|
+
const mnemonic =
|
|
261
|
+
"legal winner thank year wave sausage worth useful legal winner thank yellow";
|
|
262
|
+
const seed = ByteUtils.hexStringToBytes(Mnemonic.toSeed(mnemonic));
|
|
263
|
+
|
|
264
|
+
it("imports mnemonic and stores derived seed", async () => {
|
|
265
|
+
const storage = createMemoryStorage();
|
|
266
|
+
|
|
267
|
+
const derived = await importMasterMnemonic({ storage, mnemonic });
|
|
268
|
+
|
|
269
|
+
expect(derived).toEqual(seed);
|
|
270
|
+
const storedSeed = await loadMasterSeed(storage);
|
|
271
|
+
expect(storedSeed).toEqual(seed);
|
|
272
|
+
const storedMnemonic = await loadMasterMnemonic(storage);
|
|
273
|
+
expect(storedMnemonic).toEqual(mnemonic);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("refuses to overwrite without flag", async () => {
|
|
277
|
+
const storage = createMemoryStorage();
|
|
278
|
+
await importMasterMnemonic({ storage, mnemonic });
|
|
279
|
+
|
|
280
|
+
await expect(importMasterMnemonic({ storage, mnemonic })).rejects.toThrow(
|
|
281
|
+
/already exists/,
|
|
282
|
+
);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("overwrites when requested", async () => {
|
|
286
|
+
const storage = createMemoryStorage();
|
|
287
|
+
await importMasterMnemonic({ storage, mnemonic });
|
|
288
|
+
|
|
289
|
+
const alternate =
|
|
290
|
+
"letter advice cage absurd amount doctor acoustic avoid letter advice cage above";
|
|
291
|
+
const alternateSeed = ByteUtils.hexStringToBytes(
|
|
292
|
+
Mnemonic.toSeed(alternate),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const derived = await importMasterMnemonic({
|
|
296
|
+
storage,
|
|
297
|
+
mnemonic: alternate,
|
|
298
|
+
overwrite: true,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
expect(derived).toEqual(alternateSeed);
|
|
302
|
+
const storedMnemonic = await loadMasterMnemonic(storage);
|
|
303
|
+
expect(storedMnemonic).toEqual(alternate);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
function textEncode(value: string): Uint8Array {
|
|
308
|
+
return new TextEncoder().encode(value);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function storeMnemonicFixture(
|
|
312
|
+
storage: ReturnType<typeof createMemoryStorage>,
|
|
313
|
+
mnemonic: string,
|
|
314
|
+
) {
|
|
315
|
+
await storeMasterMnemonic(storage, mnemonic);
|
|
316
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { Mnemonic } from "../key-derivation/bip39.js";
|
|
2
|
+
import { ByteUtils } from "../key-derivation/bytes.js";
|
|
3
|
+
import {
|
|
4
|
+
deriveNodesFromSeed,
|
|
5
|
+
WalletNode,
|
|
6
|
+
type SpendingKeyPair,
|
|
7
|
+
type ViewingKeyPair,
|
|
8
|
+
} from "../key-derivation/wallet-node.js";
|
|
9
|
+
import type { Storage } from "../types.js";
|
|
10
|
+
|
|
11
|
+
export type MasterSeedCrypto = {
|
|
12
|
+
encrypt(seed: Uint8Array): Promise<Uint8Array> | Uint8Array;
|
|
13
|
+
decrypt(payload: Uint8Array): Promise<Uint8Array> | Uint8Array;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type GenerateMasterSeedOptions = {
|
|
17
|
+
storage: Storage;
|
|
18
|
+
rng: (n: number) => Uint8Array;
|
|
19
|
+
crypto?: MasterSeedCrypto;
|
|
20
|
+
overwrite?: boolean;
|
|
21
|
+
mnemonicPassphrase: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const MASTER_SEED_LENGTH = 64;
|
|
25
|
+
export const MASTER_SEED_KEY = "cfg:wallet:master_seed/v1";
|
|
26
|
+
export const MASTER_MNEMONIC_KEY = "cfg:wallet:master_mnemonic/v1";
|
|
27
|
+
|
|
28
|
+
const MNEMONIC_ENTROPY_BYTES = 32;
|
|
29
|
+
|
|
30
|
+
const textEncoder = new TextEncoder();
|
|
31
|
+
const textDecoder = new TextDecoder();
|
|
32
|
+
|
|
33
|
+
export type ZkAccount = {
|
|
34
|
+
spendingKeyPair: SpendingKeyPair;
|
|
35
|
+
viewingKeyPair: ViewingKeyPair;
|
|
36
|
+
nullifyingKey: bigint;
|
|
37
|
+
masterPublicKey: bigint;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export async function deriveZkAccount(
|
|
41
|
+
masterSeed: Uint8Array,
|
|
42
|
+
accountIndex: number = 0,
|
|
43
|
+
): Promise<ZkAccount> {
|
|
44
|
+
const seed = normalizeMasterSeed(masterSeed);
|
|
45
|
+
if (!Number.isInteger(accountIndex) || accountIndex < 0) {
|
|
46
|
+
throw new Error("accountIndex must be a non-negative integer");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { spending, viewing } = deriveNodesFromSeed(seed, accountIndex);
|
|
50
|
+
const spendingKeyPair = spending.getSpendingKeyPair();
|
|
51
|
+
const viewingKeyPair = await viewing.getViewingKeyPair();
|
|
52
|
+
const nullifyingKey = WalletNode.getNullifyingKey(viewingKeyPair.privateKey);
|
|
53
|
+
const masterPublicKey = WalletNode.getMasterPublicKey(
|
|
54
|
+
spendingKeyPair.pubkey,
|
|
55
|
+
nullifyingKey,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
spendingKeyPair,
|
|
60
|
+
viewingKeyPair,
|
|
61
|
+
nullifyingKey,
|
|
62
|
+
masterPublicKey,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function deriveZkAccountFromMnemonic(
|
|
67
|
+
mnemonic: string,
|
|
68
|
+
accountIndex: number = 0,
|
|
69
|
+
password: string = "",
|
|
70
|
+
): Promise<ZkAccount> {
|
|
71
|
+
const normalized = normalizeMnemonic(mnemonic);
|
|
72
|
+
const seed = mnemonicToSeed(normalized, password);
|
|
73
|
+
return deriveZkAccount(seed, accountIndex);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const identityMasterSeedCrypto: MasterSeedCrypto = {
|
|
77
|
+
encrypt(seed) {
|
|
78
|
+
return clone(seed);
|
|
79
|
+
},
|
|
80
|
+
decrypt(payload) {
|
|
81
|
+
return clone(payload);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export async function generateMasterSeed({
|
|
86
|
+
storage,
|
|
87
|
+
rng,
|
|
88
|
+
crypto = identityMasterSeedCrypto,
|
|
89
|
+
overwrite = false,
|
|
90
|
+
mnemonicPassphrase,
|
|
91
|
+
}: GenerateMasterSeedOptions): Promise<Uint8Array> {
|
|
92
|
+
if (!overwrite) {
|
|
93
|
+
const existingSeed = await loadMasterSeed(storage, crypto);
|
|
94
|
+
if (existingSeed) return existingSeed;
|
|
95
|
+
|
|
96
|
+
const existingMnemonic = await loadMasterMnemonic(storage, crypto);
|
|
97
|
+
if (existingMnemonic) {
|
|
98
|
+
const seedFromMnemonic = mnemonicToSeed(
|
|
99
|
+
existingMnemonic,
|
|
100
|
+
mnemonicPassphrase,
|
|
101
|
+
);
|
|
102
|
+
await storeMasterSeed(storage, seedFromMnemonic, crypto);
|
|
103
|
+
return seedFromMnemonic;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const entropy = rng(MNEMONIC_ENTROPY_BYTES);
|
|
108
|
+
if (entropy.length !== MNEMONIC_ENTROPY_BYTES) {
|
|
109
|
+
throw new Error(`mnemonic entropy must be ${MNEMONIC_ENTROPY_BYTES} bytes`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const mnemonic = Mnemonic.fromEntropy(ByteUtils.bytesToHex(entropy));
|
|
113
|
+
await storeMasterMnemonic(storage, mnemonic, crypto);
|
|
114
|
+
const seed = mnemonicToSeed(mnemonic, mnemonicPassphrase);
|
|
115
|
+
await storeMasterSeed(storage, seed, crypto);
|
|
116
|
+
return seed;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function loadMasterSeed(
|
|
120
|
+
storage: Storage,
|
|
121
|
+
crypto: MasterSeedCrypto = identityMasterSeedCrypto,
|
|
122
|
+
): Promise<Uint8Array | null> {
|
|
123
|
+
const stored = await storage.get(MASTER_SEED_KEY);
|
|
124
|
+
if (!stored) return null;
|
|
125
|
+
const plaintext = await crypto.decrypt(stored);
|
|
126
|
+
return normalizeMasterSeed(plaintext);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function storeMasterSeed(
|
|
130
|
+
storage: Storage,
|
|
131
|
+
seed: Uint8Array,
|
|
132
|
+
crypto: MasterSeedCrypto = identityMasterSeedCrypto,
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
const normalized = normalizeMasterSeed(seed);
|
|
135
|
+
const encrypted = await crypto.encrypt(normalized);
|
|
136
|
+
if (!(encrypted instanceof Uint8Array)) {
|
|
137
|
+
throw new Error("master seed encryptor must return Uint8Array");
|
|
138
|
+
}
|
|
139
|
+
await storage.put(MASTER_SEED_KEY, clone(encrypted));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function loadMasterMnemonic(
|
|
143
|
+
storage: Storage,
|
|
144
|
+
crypto: MasterSeedCrypto = identityMasterSeedCrypto,
|
|
145
|
+
): Promise<string | null> {
|
|
146
|
+
const stored = await storage.get(MASTER_MNEMONIC_KEY);
|
|
147
|
+
if (!stored) return null;
|
|
148
|
+
const plaintext = await crypto.decrypt(stored);
|
|
149
|
+
const mnemonic = textDecoder.decode(plaintext);
|
|
150
|
+
return normalizeMnemonic(mnemonic);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export type ImportMasterMnemonicOptions = {
|
|
154
|
+
storage: Storage;
|
|
155
|
+
mnemonic: string;
|
|
156
|
+
crypto?: MasterSeedCrypto;
|
|
157
|
+
overwrite?: boolean;
|
|
158
|
+
password?: string;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export async function importMasterMnemonic({
|
|
162
|
+
storage,
|
|
163
|
+
mnemonic,
|
|
164
|
+
crypto = identityMasterSeedCrypto,
|
|
165
|
+
overwrite = false,
|
|
166
|
+
password = "",
|
|
167
|
+
}: ImportMasterMnemonicOptions): Promise<Uint8Array> {
|
|
168
|
+
if (!overwrite) {
|
|
169
|
+
const existing = await loadMasterMnemonic(storage, crypto);
|
|
170
|
+
if (existing) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
"master mnemonic already exists; set overwrite to true to replace it",
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const normalized = normalizeMnemonic(mnemonic);
|
|
178
|
+
const seed = mnemonicToSeed(normalized, password);
|
|
179
|
+
await storeMasterMnemonic(storage, normalized, crypto);
|
|
180
|
+
await storeMasterSeed(storage, seed, crypto);
|
|
181
|
+
return seed;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function storeMasterMnemonic(
|
|
185
|
+
storage: Storage,
|
|
186
|
+
mnemonic: string,
|
|
187
|
+
crypto: MasterSeedCrypto = identityMasterSeedCrypto,
|
|
188
|
+
): Promise<void> {
|
|
189
|
+
const normalized = normalizeMnemonic(mnemonic);
|
|
190
|
+
const encrypted = await crypto.encrypt(textEncoder.encode(normalized));
|
|
191
|
+
if (!(encrypted instanceof Uint8Array)) {
|
|
192
|
+
throw new Error("master mnemonic encryptor must return Uint8Array");
|
|
193
|
+
}
|
|
194
|
+
await storage.put(MASTER_MNEMONIC_KEY, clone(encrypted));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function clone(bytes: Uint8Array): Uint8Array {
|
|
198
|
+
return new Uint8Array(bytes);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizeMasterSeed(seed: Uint8Array): Uint8Array {
|
|
202
|
+
if (!(seed instanceof Uint8Array)) {
|
|
203
|
+
throw new Error("master seed must be a Uint8Array");
|
|
204
|
+
}
|
|
205
|
+
if (seed.length !== MASTER_SEED_LENGTH) {
|
|
206
|
+
throw new Error(`master seed must be ${MASTER_SEED_LENGTH} bytes`);
|
|
207
|
+
}
|
|
208
|
+
return clone(seed);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function normalizeMnemonic(mnemonic: string): string {
|
|
212
|
+
const formatted = mnemonic.trim().replace(/\s+/g, " ");
|
|
213
|
+
if (!Mnemonic.validate(formatted)) {
|
|
214
|
+
throw new Error("invalid BIP-39 mnemonic phrase");
|
|
215
|
+
}
|
|
216
|
+
return formatted;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function mnemonicToSeed(mnemonic: string, password: string = ""): Uint8Array {
|
|
220
|
+
const seedHex = Mnemonic.toSeed(mnemonic, password);
|
|
221
|
+
return ByteUtils.hexStringToBytes(seedHex);
|
|
222
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { FetchLike } from "./http.js";
|
|
2
|
+
import { createJsonHttpClient } from "./http.js";
|
|
3
|
+
|
|
4
|
+
export type BroadcasterRelayPayload = {
|
|
5
|
+
kind: "call_data";
|
|
6
|
+
to: string;
|
|
7
|
+
data: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type SubmitRelayParams = {
|
|
11
|
+
clientTxId: string;
|
|
12
|
+
chainId: number;
|
|
13
|
+
payload: BroadcasterRelayPayload;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type SubmitRelayResponse = {
|
|
17
|
+
id: string;
|
|
18
|
+
accepted: boolean;
|
|
19
|
+
message?: string | null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type RelayState =
|
|
23
|
+
| "pending"
|
|
24
|
+
| "broadcasting"
|
|
25
|
+
| "succeeded"
|
|
26
|
+
| "failed"
|
|
27
|
+
| "dead";
|
|
28
|
+
|
|
29
|
+
export type RelayStatusResponse = {
|
|
30
|
+
id: string;
|
|
31
|
+
state: RelayState;
|
|
32
|
+
txHash?: string | null;
|
|
33
|
+
receipt?: unknown;
|
|
34
|
+
error?: string | null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type BroadcasterClientDeps = {
|
|
38
|
+
fetch: FetchLike;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function createBroadcasterClient(
|
|
42
|
+
baseUrl: string,
|
|
43
|
+
deps: BroadcasterClientDeps,
|
|
44
|
+
) {
|
|
45
|
+
const http = createJsonHttpClient(baseUrl, deps);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
async submitRelay(params: SubmitRelayParams) {
|
|
49
|
+
return http.request<SubmitRelayResponse>({
|
|
50
|
+
method: "POST",
|
|
51
|
+
path: "/relays",
|
|
52
|
+
json: {
|
|
53
|
+
client_tx_id: params.clientTxId,
|
|
54
|
+
chain_id: params.chainId,
|
|
55
|
+
...params.payload,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async getRelayStatus(relayId: string) {
|
|
61
|
+
return http.request<RelayStatusResponse>({
|
|
62
|
+
method: "GET",
|
|
63
|
+
path: `/relays/${relayId}`,
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
package/clients/http.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import ky, { HTTPError, TimeoutError } from "ky";
|
|
2
|
+
|
|
3
|
+
export type FetchLike = (
|
|
4
|
+
input: RequestInfo | URL,
|
|
5
|
+
init?: RequestInit,
|
|
6
|
+
) => Promise<Response>;
|
|
7
|
+
|
|
8
|
+
export type JsonHttpDeps = { fetch: FetchLike };
|
|
9
|
+
|
|
10
|
+
export type JsonRequestOptions = {
|
|
11
|
+
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
12
|
+
path: string;
|
|
13
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
14
|
+
json?: unknown;
|
|
15
|
+
body?: BodyInit;
|
|
16
|
+
headers?: HeadersInit;
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type JsonHttpClient = {
|
|
21
|
+
request<T>(opts: JsonRequestOptions): Promise<T>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export class HttpError extends Error {
|
|
25
|
+
readonly status: number;
|
|
26
|
+
readonly body: unknown;
|
|
27
|
+
|
|
28
|
+
constructor(message: string, status: number, body: unknown) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.status = status;
|
|
31
|
+
this.body = body;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function readErrorBodySafe(res: Response) {
|
|
36
|
+
try {
|
|
37
|
+
return await res.clone().json();
|
|
38
|
+
} catch {
|
|
39
|
+
const text = await res.text();
|
|
40
|
+
return text || null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createJsonHttpClient(
|
|
45
|
+
baseUrl: string,
|
|
46
|
+
deps: JsonHttpDeps,
|
|
47
|
+
): JsonHttpClient {
|
|
48
|
+
if (!baseUrl) throw new Error("baseUrl is required");
|
|
49
|
+
if (!deps?.fetch) throw new Error("deps.fetch is required");
|
|
50
|
+
|
|
51
|
+
// Ensure `fetch` is invoked with a global `this` to avoid "Illegal invocation"
|
|
52
|
+
// errors in some browser environments when the function is extracted. // TODO: can we fix this in another way?
|
|
53
|
+
const fetchImpl: FetchLike = (...args) => deps.fetch.apply(globalThis, args);
|
|
54
|
+
|
|
55
|
+
const api = ky.create({
|
|
56
|
+
prefixUrl: baseUrl.replace(/\/+$/, ""),
|
|
57
|
+
fetch: fetchImpl,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
async request<T>(opts: JsonRequestOptions): Promise<T> {
|
|
62
|
+
try {
|
|
63
|
+
const res = api(opts.path.replace(/^\//, ""), {
|
|
64
|
+
method: opts.method,
|
|
65
|
+
searchParams: opts.query,
|
|
66
|
+
json: opts.json,
|
|
67
|
+
body: opts.body,
|
|
68
|
+
headers: opts.headers,
|
|
69
|
+
signal: opts.signal,
|
|
70
|
+
});
|
|
71
|
+
return await res.json<T>();
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (err instanceof HTTPError) {
|
|
74
|
+
const body = await readErrorBodySafe(err.response);
|
|
75
|
+
throw new HttpError(
|
|
76
|
+
`HTTP ${err.response.status} ${err.response.statusText}`.trim(),
|
|
77
|
+
err.response.status,
|
|
78
|
+
body,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (err instanceof TimeoutError) {
|
|
83
|
+
throw new HttpError("HTTP timeout", 408, null);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw new HttpError(
|
|
87
|
+
err instanceof Error ? err.message : "Network error",
|
|
88
|
+
0,
|
|
89
|
+
null,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|