@opeqlabs/sdk 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js ADDED
@@ -0,0 +1,384 @@
1
+ import { installLocks } from "./locks.js";
2
+ import * as M from "./src/lib/mnemonic.js";
3
+ import * as P from "./src/lib/protocol.js";
4
+ import * as W from "./src/lib/wots.js";
5
+ import * as E from "./src/lib/evmwots.js";
6
+ import { Engine, signedLeaf, EXEC_LIMIT } from "./src/lib/engine.js";
7
+ import { localCompute } from "./src/lib/compute.js";
8
+ import { EvmChain, checkAddress, parseUnits, formatUnits } from "./src/lib/evmwallet.js";
9
+ import { MemoryStore, IdbStore } from "./src/lib/vaultstore.js";
10
+ import { Rpc, pubkey, b58encode, b58decode, eq, hex, unhex } from "./src/lib/solana.js";
11
+ import { sha256 } from "./src/lib/sha256.js";
12
+ import { hexToBytes, bytesToHex } from "./src/lib/evmtx.js";
13
+ import { feePayer } from "./src/feekey.js";
14
+
15
+ installLocks();
16
+
17
+ export const VERSION = "0.1.0";
18
+
19
+ export const NETWORKS = Object.freeze({
20
+ mainnet: Object.freeze({ program: "HL598vrersxS5AUUV6NyzHgpSyK31xAmqMsqUhxzeKm8", genesis: "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d" }),
21
+ devnet: Object.freeze({ program: "424ckcA6GGswYmQbrGjQmzdfhk4G9eB2zD1gK8xLw7sN", genesis: "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG" }),
22
+ });
23
+
24
+ export const EVM_CHAINS = Object.freeze({
25
+ 1: Object.freeze({ chainId: 1, name: "Ethereum", symbol: "ETH", explorer: "https://etherscan.io" }),
26
+ 8453: Object.freeze({ chainId: 8453, name: "Base", symbol: "ETH", explorer: "https://basescan.org" }),
27
+ 42161: Object.freeze({ chainId: 42161, name: "Arbitrum", symbol: "ETH", explorer: "https://arbiscan.io" }),
28
+ 4663: Object.freeze({ chainId: 4663, name: "Robinhood Chain", symbol: "ETH", explorer: "https://robinhoodchain.blockscout.com" }),
29
+ 99901: Object.freeze({ chainId: 99901, name: "Elysium", symbol: "HYPE", explorer: "https://elysium.kinetiq.xyz" }),
30
+ 84532: Object.freeze({ chainId: 84532, name: "Base Sepolia", symbol: "ETH", explorer: "https://sepolia.basescan.org" }),
31
+ 31337: Object.freeze({ chainId: 31337, name: "Local", symbol: "ETH", explorer: "" }),
32
+ });
33
+
34
+ const MAX_PRIORITY = 100000;
35
+ const enc = new TextEncoder();
36
+ const LOCAL = new Set(["localhost", "127.0.0.1", "[::1]"]);
37
+ const httpsUrl = (u, what) => {
38
+ let x;
39
+ try { x = new URL(u); } catch { throw new Error(`${what} must be an https URL`); }
40
+ if (x.protocol !== "https:" && !(x.protocol === "http:" && LOCAL.has(x.hostname))) throw new Error(`${what} must be an https URL (plain http only for localhost)`);
41
+ return u;
42
+ };
43
+ const differ = (a, b, what) => {
44
+ if (new URL(a).host.toLowerCase() === new URL(b).host.toLowerCase()) throw new Error(`${what} must be a different provider from the main RPC, or the second check means nothing`);
45
+ };
46
+ const U64 = 1n << 64n, U256 = 1n << 256n;
47
+ const amount = (v, what, max, min = 0n) => {
48
+ let n;
49
+ if (typeof v === "bigint") n = v;
50
+ else if (typeof v === "number" && Number.isSafeInteger(v)) n = BigInt(v);
51
+ else if (typeof v === "string" && /^[0-9]+$/.test(v)) n = BigInt(v);
52
+ else throw new Error(`${what} must be a whole number in base units (bigint, integer or digit string)`);
53
+ if (n < min || n >= max) throw new Error(`${what} is out of range`);
54
+ return n;
55
+ };
56
+ const key = (k) => (typeof k === "string" ? pubkey(k) : k instanceof Uint8Array && k.length === 32 ? k : (() => { throw new Error("a Solana address must be base58 text or 32 bytes"); })());
57
+ const bytes = (d) => {
58
+ if (d == null) return new Uint8Array(0);
59
+ if (d instanceof Uint8Array) return new Uint8Array(d);
60
+ if (typeof d === "string") {
61
+ if (!/^(0x)?([0-9a-fA-F]{2})*$/.test(d)) throw new Error("hex data must have an even number of hex digits");
62
+ return hexToBytes(d.startsWith("0x") ? d : "0x" + d);
63
+ }
64
+ if (Array.isArray(d) && d.every((x) => Number.isInteger(x) && x >= 0 && x <= 255)) return Uint8Array.from(d);
65
+ throw new Error("data must be a Uint8Array, a Buffer, a hex string or an array of bytes 0-255");
66
+ };
67
+
68
+ export function newWords() {
69
+ const m = M.newMaster();
70
+ try { return M.toWords(m).join(" "); } finally { m.fill(0); }
71
+ }
72
+
73
+ const text = (w) => (Array.isArray(w) ? w.join(" ") : String(w));
74
+
75
+ export function checkWords(words) {
76
+ M.fromWords(text(words)).fill(0);
77
+ return true;
78
+ }
79
+
80
+ function toInner(ix, wallet) {
81
+ let out;
82
+ if (Array.isArray(ix)) out = [key(ix[0]), ix[1].map(([k, s, w]) => [key(k), s === true, w === true]), bytes(ix[2])];
83
+ else if (ix && ix.programId && Array.isArray(ix.keys)) out = [key(ix.programId), ix.keys.map((m) => [key(m.pubkey), m.isSigner === true, m.isWritable === true]), bytes(ix.data)];
84
+ else throw new Error("an instruction needs programId, keys and data");
85
+ for (const [k, signer] of out[1]) if (signer && !eq(k, wallet)) throw new Error("only the OPEQ account itself can sign inside an instruction; nothing was signed");
86
+ return out;
87
+ }
88
+
89
+ function priceFor(rpc) {
90
+ return async (cuLimit, keys) => {
91
+ try {
92
+ const recent = await rpc.call("getRecentPrioritizationFees", [keys.slice(0, 128).map(b58encode)]);
93
+ if (!Array.isArray(recent)) return 0;
94
+ const fees = recent.slice(0, 300).map((x) => x && x.prioritizationFee).filter((f) => Number.isSafeInteger(f) && f >= 0).sort((a, b) => a - b);
95
+ if (!fees.length) return 0;
96
+ return Math.max(0, Math.min(fees[Math.floor(fees.length * 0.75)], Math.floor((MAX_PRIORITY * 1e6) / cuLimit)));
97
+ } catch { return 0; }
98
+ };
99
+ }
100
+
101
+ export class Vault {
102
+ constructor({ rpcUrl, network = "mainnet", program, genesis, store, compute = localCompute, checkRpcUrl } = {}) {
103
+ const net = NETWORKS[network];
104
+ if (!net && !(program && genesis)) throw new Error("network must be mainnet or devnet, or pass program and genesis");
105
+ if (!store) throw new Error("pass a store: FileStore.open(dir) in Node, IdbStore in a browser, MemoryStore for tests");
106
+ httpsUrl(rpcUrl, "rpcUrl");
107
+ if (network === "mainnet" && !checkRpcUrl) throw new Error("pass checkRpcUrl: a second Solana RPC from another provider. OPEQ uses it to confirm that a transaction can no longer land before it reuses anything");
108
+ if (checkRpcUrl) { httpsUrl(checkRpcUrl, "checkRpcUrl"); differ(rpcUrl, checkRpcUrl, "checkRpcUrl"); }
109
+ this.cfg = { rpcUrl, checkRpcUrl, program: program || net.program, genesis: genesis || net.genesis, compute };
110
+ this.store = store;
111
+ this.engine = this.newEngine();
112
+ this.rpc = this.engine.rpc;
113
+ }
114
+
115
+ newEngine() {
116
+ const rpc = new Rpc(this.cfg.rpcUrl);
117
+ const e = new Engine({ rpc, program: this.cfg.program, store: this.store, compute: this.cfg.compute, payer: null });
118
+ e.genesis = this.cfg.genesis;
119
+ e.maxPriority = MAX_PRIORITY;
120
+ e.priceFor = priceFor(rpc);
121
+ if (this.cfg.checkRpcUrl) e.refRpc = new Rpc(this.cfg.checkRpcUrl);
122
+ return e;
123
+ }
124
+
125
+ async create(password, progress) {
126
+ const { record, words } = await this.engine.create(password, progress);
127
+ await this.engine.save(record);
128
+ return { id: record.id, words: words.join(" ") };
129
+ }
130
+
131
+ async recover(words, password, progress) {
132
+ await this.engine.checkCluster();
133
+ const rec = await this.engine.recover(text(words), password, progress, null);
134
+ const old = await this.store.get(rec.id);
135
+ if (old) throw new Error("this wallet is already in the store; open it instead");
136
+ await this.engine.save(rec);
137
+ return { id: rec.id, active: rec.active };
138
+ }
139
+
140
+ async list() {
141
+ return (await this.store.all()).filter((r) => r.v === 1 && r.program === b58encode(this.engine.program)).map((r) => ({ id: r.id, epoch: r.epoch, nextLeaf: r.next_leaf, active: r.active }));
142
+ }
143
+
144
+ async open(id, password) {
145
+ const record = await this.store.get(id);
146
+ if (!record) throw new Error("no wallet with that id in this store");
147
+ const engine = this.newEngine();
148
+ const session = await engine.unlock(record, password);
149
+ return new Account(this, engine, session);
150
+ }
151
+ }
152
+
153
+ export class Account {
154
+ constructor(vault, engine, session) {
155
+ this.vault = vault;
156
+ this.e = engine;
157
+ this.s = session;
158
+ this.id = session.id;
159
+ this.address = session.id;
160
+ this.payer = null;
161
+ this.chains = new Map();
162
+ }
163
+
164
+ live() { if (!this.s) throw new Error("this account is closed"); }
165
+
166
+ async feeKey() {
167
+ this.live();
168
+ if (!this.payer) this.payer = await feePayer(this.s.master, this.e.program);
169
+ this.e.payer = this.payer;
170
+ return this.payer.address;
171
+ }
172
+
173
+ async use() {
174
+ await this.feeKey();
175
+ return this.s;
176
+ }
177
+
178
+ async status() {
179
+ this.live();
180
+ const st = await this.s.status();
181
+ return {
182
+ state: st.state,
183
+ epoch: st.r.epoch,
184
+ nextLeaf: Math.max(st.r.next_leaf, st.st ? st.st.next : 0),
185
+ keysLeft: st.st ? Math.max(0, EXEC_LIMIT - Math.max(st.r.next_leaf, st.st.next)) : EXEC_LIMIT,
186
+ };
187
+ }
188
+
189
+ async balance() {
190
+ this.live();
191
+ return this.e.rpc.balance(this.s.wallet);
192
+ }
193
+
194
+ async activate() {
195
+ await this.use();
196
+ return this.e.init(await this.s.load());
197
+ }
198
+
199
+ async send(to, lamports, progress) {
200
+ const n = amount(lamports, "lamports", U64, 1n);
201
+ return (await this.use()).sendSol(key(to), n, progress);
202
+ }
203
+
204
+ async execute(instructions, { computeUnits = 400000, progress } = {}) {
205
+ if (!Array.isArray(instructions) || !instructions.length) throw new Error("pass at least one instruction");
206
+ if (!Number.isInteger(computeUnits) || computeUnits < 1 || computeUnits > 1400000) throw new Error("computeUnits must be a whole number from 1 to 1,400,000");
207
+ this.live();
208
+ const inner = instructions.map((ix) => toInner(ix, this.s.wallet));
209
+ return (await this.use()).exec(inner, computeUnits, progress);
210
+ }
211
+
212
+ async rekey(progress) {
213
+ return (await this.use()).rekey(progress);
214
+ }
215
+
216
+ async useHere(progress) {
217
+ return (await this.use()).takeover(progress);
218
+ }
219
+
220
+ words() {
221
+ this.live();
222
+ return this.s.words().join(" ");
223
+ }
224
+
225
+ async evm(chainId, { rpcUrl, checkUrl } = {}) {
226
+ this.live();
227
+ const meta = EVM_CHAINS[chainId];
228
+ if (!meta) throw new Error(`chain ${chainId} is not an OPEQ network`);
229
+ httpsUrl(rpcUrl, "rpcUrl");
230
+ if (!checkUrl && chainId !== 31337) throw new Error("pass checkUrl: a second, independent RPC for this chain. OPEQ compares both before it signs");
231
+ if (checkUrl) { httpsUrl(checkUrl, "checkUrl"); differ(rpcUrl, checkUrl, "checkUrl"); }
232
+ const raw = sha256(enc.encode("opeq/evm/records/v1"), this.s.master);
233
+ let macKey;
234
+ try { macKey = await crypto.subtle.importKey("raw", raw, { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]); }
235
+ finally { raw.fill(0); }
236
+ const c = new EvmChain({
237
+ chain: { ...meta, rpc: rpcUrl, check: checkUrl || null },
238
+ store: this.vault.store, macKey, legacyMac: this.s.macKey, vaultId: this.s.id,
239
+ master: this.s.master.slice(), compute: this.e.compute,
240
+ });
241
+ await c.loadIdentity(null);
242
+ const a = new EvmAccount(c);
243
+ this.chains.set(a, true);
244
+ return a;
245
+ }
246
+
247
+ close() {
248
+ for (const a of this.chains.keys()) a.close();
249
+ this.chains.clear();
250
+ if (this.s) {
251
+ this.s.master.fill(0);
252
+ if (this.s.macRaw instanceof Uint8Array) this.s.macRaw.fill(0);
253
+ this.s = null;
254
+ }
255
+ this.e.payer = null;
256
+ this.payer = null;
257
+ }
258
+ }
259
+
260
+ export class EvmAccount {
261
+ constructor(chain) {
262
+ this.c = chain;
263
+ this.chainId = chain.id;
264
+ this.address = chain.account;
265
+ this.feeAddress = chain.feeAddress;
266
+ }
267
+
268
+ live() { if (!this.c) throw new Error("this EVM account is closed"); }
269
+
270
+ async status() {
271
+ this.live();
272
+ const s = await this.c.status();
273
+ return { state: s.state, balance: s.bal, feeBalance: s.feeBal, keysLeft: s.left };
274
+ }
275
+
276
+ async setup(progress, step) {
277
+ this.live();
278
+ const s = await this.c.setup(progress, step);
279
+ return s.state;
280
+ }
281
+
282
+ async execute(calls, onHash) {
283
+ this.live();
284
+ if (!Array.isArray(calls) || !calls.length) throw new Error("pass at least one call");
285
+ const norm = calls.map((x) => {
286
+ if (!x || typeof x !== "object") throw new Error("each call needs to, value and data");
287
+ return { to: checkAddress(x.to), value: x.value == null ? 0n : amount(x.value, "value", U256), data: bytes(x.data) };
288
+ });
289
+ return this.c.execute(norm, onHash);
290
+ }
291
+
292
+ async send(to, wei, onHash) {
293
+ return this.execute([{ to, value: amount(wei, "wei", U256, 1n) }], onHash);
294
+ }
295
+
296
+ async rekey(progress, onHash) {
297
+ this.live();
298
+ return this.c.rekey(progress, onHash);
299
+ }
300
+
301
+ async useHere(progress, onHash) {
302
+ this.live();
303
+ return this.c.takeover(progress, onHash);
304
+ }
305
+
306
+ close() {
307
+ if (!this.c) return;
308
+ this.c.master.fill(0);
309
+ this.c.fee = 0n;
310
+ this.c = null;
311
+ }
312
+ }
313
+
314
+ export function solanaAccountFor(pubSeed, root, network = "mainnet") {
315
+ const program = pubkey(NETWORKS[network].program);
316
+ const aid = P.accountId(bytes(pubSeed), bytes(root));
317
+ return { id: hex(aid), wallet: b58encode(P.walletPda(aid, program).key), state: b58encode(P.statePda(aid, program).key) };
318
+ }
319
+
320
+ const exact = (v, n) => { const b = bytes(v); if (b.length !== n) throw new Error("wrong length"); return b; };
321
+
322
+ export function verifySolanaInstruction(data, accountKeys, { pubSeed, root, aid, epoch }, network = "mainnet") {
323
+ try {
324
+ const program = pubkey(NETWORKS[network].program);
325
+ const keys = accountKeys.map((k) => (typeof k === "string" ? b58encode(pubkey(k)) : b58encode(exact(k, 32))));
326
+ if (!Number.isInteger(epoch) || epoch < 0 || epoch > 0xffffffff) return null;
327
+ const cur = { pubSeed: exact(pubSeed, W.N), root: exact(root, W.N), aid: exact(aid, 32), epoch };
328
+ return signedLeaf(bytes(data), keys, program, cur);
329
+ } catch { return null; }
330
+ }
331
+
332
+ export function verifyWots({ pubSeed, root, leaf, digest, signature, path }) {
333
+ try {
334
+ if (!Number.isInteger(leaf) || leaf < 0 || leaf >= W.LEAVES) return false;
335
+ return W.verify(exact(pubSeed, W.N), exact(root, W.N), leaf, exact(digest, W.N), exact(signature, W.SIG_LEN), exact(path, W.PATH_LEN)) === true;
336
+ } catch { return false; }
337
+ }
338
+
339
+ const word = (b, o) => { let v = 0n; for (let i = 0; i < 32; i++) v = (v << 8n) | BigInt(b[o + i]); return v; };
340
+ const small = (b, o, max) => { const v = word(b, o); if (v > BigInt(max)) throw new Error("value out of range"); return Number(v); };
341
+
342
+ export function decodeEvmExecute(calldata) {
343
+ const d = bytes(calldata);
344
+ if (d.length < 4 + 192 || !eq(d.subarray(0, 4), E.EXECUTE_SELECTOR)) throw new Error("not an OPEQ execute call");
345
+ const b = d.subarray(4);
346
+ const callsAt = small(b, 0, b.length), leaf = small(b, 32, E.LEAVES - 1), deadline = word(b, 64);
347
+ const salt = small(b, 96, 0xffffffff), rnd = b.slice(128, 128 + E.N), proofAt = small(b, 160, b.length);
348
+ const n = small(b, callsAt, 256), calls = [];
349
+ for (let i = 0; i < n; i++) {
350
+ const at = callsAt + 32 + small(b, callsAt + 32 + i * 32, b.length);
351
+ const to = "0x" + bytesToHex(b.slice(at + 12, at + 32)).replace(/^0x/, "");
352
+ const value = word(b, at + 32);
353
+ const dAt = at + small(b, at + 64, b.length);
354
+ const len = small(b, dAt, b.length);
355
+ if (dAt + 32 + len > b.length) throw new Error("call data runs past the end");
356
+ calls.push({ to, value, data: b.slice(dAt + 32, dAt + 32 + len) });
357
+ }
358
+ const plen = small(b, proofAt, b.length);
359
+ if (plen !== E.PROOF_LEN || proofAt + 32 + plen > b.length) throw new Error("bad proof length");
360
+ const proof = b.slice(proofAt + 32, proofAt + 32 + plen);
361
+ if (!eq(E.encodeExecute(calls, leaf, deadline, salt, rnd, proof), d)) throw new Error("calldata is not in the exact form the OPEQ account accepts");
362
+ return { leaf, deadline, salt, rnd, calls, proof };
363
+ }
364
+
365
+ export function verifyEvmExecute(calldata, { chainId, account, epoch, pubSeed, root }) {
366
+ try {
367
+ if (!Number.isSafeInteger(chainId) || chainId < 1 || !Number.isSafeInteger(epoch) || epoch < 0) return null;
368
+ const ps = exact(pubSeed, E.N), rt = exact(root, E.N), acct = checkAddress(account);
369
+ const x = decodeEvmExecute(calldata);
370
+ const digest = E.execHasher(x.rnd, chainId, acct, epoch, x.leaf, x.deadline, x.calls)(x.salt);
371
+ return eq(E.rootFromProof(ps, x.leaf, digest, x.proof), rt) ? { leaf: x.leaf, calls: x.calls, deadline: x.deadline } : null;
372
+ } catch { return null; }
373
+ }
374
+
375
+ export function evmAccountAddress(words) {
376
+ const m = M.fromWords(text(words));
377
+ try {
378
+ const id = E.deriveIdentityTree(m);
379
+ return E.accountAddress(id.pubSeed, id.root);
380
+ } finally { m.fill(0); }
381
+ }
382
+
383
+ export { MemoryStore, IdbStore, localCompute, parseUnits, formatUnits, checkAddress, b58encode, b58decode, hex, unhex, hexToBytes, bytesToHex };
384
+ export const lowLevel = Object.freeze({ wots: W, evmWots: E, protocol: P, mnemonic: M });
package/locks.js ADDED
@@ -0,0 +1,28 @@
1
+ const held = new Map();
2
+
3
+ function request(name, opts, fn) {
4
+ if (typeof opts === "function") { fn = opts; opts = {}; }
5
+ const ifAvailable = !!(opts && opts.ifAvailable);
6
+ const prev = held.get(name);
7
+ if (prev && ifAvailable) return Promise.resolve().then(() => fn(null));
8
+ let release;
9
+ const mine = new Promise((r) => { release = r; });
10
+ const chain = (prev || Promise.resolve()).then(() => mine);
11
+ held.set(name, chain);
12
+ const run = (prev || Promise.resolve()).then(() => fn({ name, mode: "exclusive" }));
13
+ return run.finally(() => {
14
+ release();
15
+ if (held.get(name) === chain) held.delete(name);
16
+ });
17
+ }
18
+
19
+ export function installLocks() {
20
+ if (typeof navigator !== "undefined" && navigator.locks) return false;
21
+ const locks = Object.freeze({ request });
22
+ if (typeof navigator === "undefined") {
23
+ Object.defineProperty(globalThis, "navigator", { value: { locks }, configurable: true, writable: false });
24
+ } else {
25
+ Object.defineProperty(navigator, "locks", { value: locks, configurable: true, writable: false });
26
+ }
27
+ return true;
28
+ }
package/package.json CHANGED
@@ -1,6 +1,19 @@
1
1
  {
2
2
  "name": "@opeqlabs/sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Quantum-resistant accounts for Solana and EVM networks: one-time hash-based keys, signing and offline verification.",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./filestore": "./filestore.js",
10
+ "./compute": "./src/lib/compute.js"
11
+ },
12
+ "files": ["index.js", "filestore.js", "locks.js", "src/feekey.js", "src/lib/*.js", "examples/*.mjs", "README.md", "LICENSE"],
13
+ "engines": { "node": ">=20" },
14
+ "license": "Apache-2.0",
15
+ "repository": { "type": "git", "url": "https://github.com/Sercan0x/opeq", "directory": "sdk" },
16
+ "homepage": "https://opeq.io",
17
+ "keywords": ["solana", "ethereum", "post-quantum", "wots", "wallet", "self-custody"],
18
+ "sideEffects": ["./locks.js", "./index.js"]
19
+ }
package/src/feekey.js ADDED
@@ -0,0 +1,36 @@
1
+ import { b58encode, compact, concat } from "./lib/solana.js";
2
+ import { isLegacy } from "./lib/mnemonic.js";
3
+
4
+ const PKCS8_ED25519 = new Uint8Array([0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20]);
5
+ const TAG = new TextEncoder().encode("opeq/fee/v1");
6
+ const TAG2 = new TextEncoder().encode("opeq/fee/v2");
7
+ const unb64url = (s) => Uint8Array.from(atob(s.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
8
+
9
+ export async function feePayer(master, program) {
10
+ const input = isLegacy(program) ? concat(TAG, master) : concat(TAG2, program, master);
11
+ const seed = new Uint8Array(await crypto.subtle.digest("SHA-256", input));
12
+ input.fill(0);
13
+ const pkcs8 = concat(PKCS8_ED25519, seed);
14
+ let key, pub;
15
+ try {
16
+ const probe = await crypto.subtle.importKey("pkcs8", pkcs8, { name: "Ed25519" }, true, ["sign"]);
17
+ pub = unb64url((await crypto.subtle.exportKey("jwk", probe)).x);
18
+ key = await crypto.subtle.importKey("pkcs8", pkcs8, { name: "Ed25519" }, false, ["sign"]);
19
+ } catch {
20
+ throw new Error("this browser has no Ed25519 support in WebCrypto; update Chrome to version 137 or later");
21
+ } finally {
22
+ seed.fill(0);
23
+ pkcs8.fill(0);
24
+ }
25
+ return {
26
+ address: b58encode(pub),
27
+ key: () => pub,
28
+ async sign(tx) {
29
+ const n = tx[0];
30
+ if (n !== 1) throw new Error("fee key only signs single-signer transactions");
31
+ const message = tx.slice(1 + 64);
32
+ const sig = new Uint8Array(await crypto.subtle.sign({ name: "Ed25519" }, key, message));
33
+ return concat(new Uint8Array(compact(1)), sig, message);
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,73 @@
1
+ import * as M from "./mnemonic.js";
2
+ import * as P from "./protocol.js";
3
+ import { TreeKey, leafPk } from "./wots.js";
4
+ import { eq } from "./solana.js";
5
+ import * as E from "./evmwots.js";
6
+
7
+ export function treeFor({ master, epoch, program, pubSeed }, progress) {
8
+ const t = M.deriveTree(master, epoch, progress, program, pubSeed);
9
+ return { pubSeed: t.pubSeed, root: t.root, leaves: t.leavesBytes() };
10
+ }
11
+
12
+ function loadTree(master, epoch, leaves, leaf, program, chainPubSeed) {
13
+ const { skSeed, pubSeed } = M.treeSeeds(master, epoch, program, chainPubSeed);
14
+ const tree = TreeKey.fromLeaves(skSeed, pubSeed, leaves);
15
+ if (!eq(leafPk(skSeed, pubSeed, leaf), tree.levels[0][leaf])) throw new Error("secret seed does not match the stored tree, refusing to sign");
16
+ return tree;
17
+ }
18
+
19
+ export function signExec(a, progress) {
20
+ const tree = loadTree(a.master, a.epoch, a.leaves, a.leaf, a.program, a.pubSeed);
21
+ const g = P.grind(P.execHasher(a.program, a.aid, a.epoch, a.leaf, a.expiry, a.inner, a.r), a.tries || P.GRIND_TRIES, progress);
22
+ return { salt: g.salt, cost: g.cost, ...tree.sign(a.leaf, g.digest) };
23
+ }
24
+
25
+ export function signRekey(a, progress) {
26
+ const tree = loadTree(a.master, a.epoch, a.leaves, a.leaf, a.program, a.pubSeed);
27
+ const next = M.deriveTree(a.master, a.epoch + 1, progress && ((p) => progress(p * 0.9)), a.program, a.newPubSeed);
28
+ const g = P.grind(P.rekeyHasher(a.program, a.aid, a.epoch, a.leaf, a.expiry, next.pubSeed, next.root, a.r), a.tries || P.GRIND_TRIES);
29
+ return { salt: g.salt, ...tree.sign(a.leaf, g.digest), newPubSeed: next.pubSeed, newRoot: next.root, newLeaves: next.leavesBytes() };
30
+ }
31
+
32
+ function evmIdentity({ master }, progress) {
33
+ const t = E.deriveIdentityTree(master, progress);
34
+ return { pubSeed: t.pubSeed, root: t.root };
35
+ }
36
+
37
+ function evmBoot({ master, chainId, account }, progress) {
38
+ const identity = E.deriveIdentityTree(master, progress && ((p) => progress(p * 0.5)));
39
+ const b = E.bootstrapCall(master, chainId, account, identity, progress && ((p) => progress(0.5 + p * 0.5)));
40
+ return { pubSeed: b.tree.pubSeed, root: b.tree.root, leaves: E.leavesBytes(b.tree), calldata: b.calldata, cost: b.cost };
41
+ }
42
+
43
+ function evmTree({ master, chainId, account, epoch, pubSeed }, progress) {
44
+ const t = E.deriveChainTree(master, chainId, account, epoch, pubSeed, progress);
45
+ return { root: t.root, leaves: E.leavesBytes(t) };
46
+ }
47
+
48
+ function evmLoad(a) {
49
+ const { skSeed, pubSeed } = E.chainTreeSeeds(a.master, a.chainId, a.account, a.epoch, a.pubSeed);
50
+ const tree = E.treeFromLeaves(skSeed, pubSeed, a.leaves);
51
+ if (!eq(E.leafPk(skSeed, pubSeed, a.leaf), tree.levels[0][a.leaf])) throw new Error("secret seed does not match the stored EVM key tree, refusing to sign");
52
+ if (!eq(tree.root, a.root)) throw new Error("stored EVM key tree does not match the root on chain, refusing to sign");
53
+ return tree;
54
+ }
55
+
56
+ function evmSignExec(a, progress) {
57
+ const tree = evmLoad(a);
58
+ const g = E.grind(E.execHasher(a.rnd, a.chainId, a.account, a.epoch, a.leaf, a.deadline, a.calls), E.GRIND_TRIES, progress);
59
+ return { calldata: E.encodeExecute(a.calls, a.leaf, a.deadline, g.salt, a.rnd, tree.sign(a.leaf, g.digest)), cost: g.cost };
60
+ }
61
+
62
+ function evmSignRekey(a, progress) {
63
+ const tree = evmLoad(a);
64
+ const next = E.deriveChainTree(a.master, a.chainId, a.account, a.epoch + 1, a.newPubSeed, progress && ((p) => progress(p * 0.9)));
65
+ const g = E.grind(E.rekeyHasher(a.rnd, a.chainId, a.account, a.epoch, a.leaf, a.deadline, next.pubSeed, next.root), 1 << 14);
66
+ return { calldata: E.encodeRekey(next.pubSeed, next.root, a.leaf, a.deadline, g.salt, a.rnd, tree.sign(a.leaf, g.digest)), cost: g.cost, newRoot: next.root, newLeaves: E.leavesBytes(next) };
67
+ }
68
+
69
+ export const OPS = { treeFor, signExec, signRekey, evmIdentity, evmBoot, evmTree, evmSignExec, evmSignRekey };
70
+
71
+ export const localCompute = {
72
+ run: async (op, args, progress) => OPS[op](args, progress),
73
+ };