@zerc20/wallet-sdk 0.2.0 → 0.3.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/src/wallet.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  type Account,
20
20
  type Address,
21
21
  type Hex,
22
+ type LocalAccount,
22
23
  type WalletClient,
23
24
  bytesToHex,
24
25
  formatUnits,
@@ -28,7 +29,7 @@ import {
28
29
  zeroAddress,
29
30
  } from 'viem';
30
31
  import { Chain, MAX_REFERRAL_BPS, NATIVE, type Referrer, type Token, erc20Abi, tokenAbi } from './chain.js';
31
- import { type Ctx, type SpendKey, chainCtx, chainState, spendKey, walletSees } from './context.js';
32
+ import { type Ctx, type SpendKey, chainCtx, chainState, owner, switched, walletOn, walletSees } from './context.js';
32
33
  import { burnAddressFor, core, initCore } from './core.js';
33
34
  import { type ErrorCode, WalletError, fail, guard } from './errors.js';
34
35
  import { type Network, checkNetwork } from './network.js';
@@ -62,10 +63,14 @@ import {
62
63
  emptyWalletState,
63
64
  openState,
64
65
  sealState,
66
+ stateKey,
67
+ unspent,
65
68
  } from './state.js';
66
69
  import {
67
70
  WAIT_MS,
68
71
  commitmentTx,
72
+ keyAll,
73
+ nextBurn,
69
74
  recordBurn,
70
75
  storageState,
71
76
  syncAll,
@@ -75,16 +80,23 @@ import {
75
80
  unusedBurn,
76
81
  } from './sync.js';
77
82
 
78
- const DAY_MS = 86_400_000;
79
83
  /** The ledger's bound outside an emergency. */
80
84
  const NO_BOUND = 2n ** 64n - 1n;
81
85
 
82
86
  export interface OpenOptions {
83
87
  network: Network;
84
- /** The user's wallet (MetaMask, WalletConnect, ...) as a viem client with its account. */
85
- walletClient: WalletClient;
86
- /** The 32-byte root of every key. See `seedFromSignature`. */
87
- seed: Uint8Array;
88
+ /** The account's key, held by the app as a viem local account (a private key's, or `accountKey`'s): the wallet sends
89
+ * its transactions through each chain's RPC. Give this or `walletClient`. */
90
+ account?: LocalAccount;
91
+ /** The user's wallet (MetaMask, WalletConnect, ...) as a viem client with its account, which the wallet switches to
92
+ * each chain it sends on. */
93
+ walletClient?: WalletClient;
94
+ /** The 32-byte root of every key (see `seedFromSignature`), held while the wallet is open. Or a function that gives
95
+ * it, a passkey's say, to hold the seed only while it is needed: asked as the wallet opens, and again to pay,
96
+ * withdraw, consolidate or exit, or for a receive code past those derived ahead, each time before the call waits on
97
+ * anything (a browser shows a passkey prompt only in the click that asked). The seed goes at the end of a sync that
98
+ * nothing waits behind, once nothing in flight signs again. */
99
+ seed: Uint8Array | (() => Promise<Uint8Array>);
88
100
  /** Where the encrypted state lives between sessions. */
89
101
  persist: { load(): Promise<Uint8Array | null | undefined>; save(bytes: Uint8Array): Promise<void> };
90
102
  /** Where a wallet with no saved state starts looking: the block its seed was first used at, or roughly when. Without
@@ -108,7 +120,7 @@ export type DepositStep =
108
120
 
109
121
  /** Why an amount is not spendable yet. */
110
122
  export type PendingReason =
111
- /** Announced by the payer, and not yet read on chain. */
123
+ /** A note this device's payment, withdrawal or merge under way is making, not yet read on chain. */
112
124
  | 'incoming'
113
125
  /** Waiting for a proved root that covers it (minutes). */
114
126
  | 'awaiting-root'
@@ -157,7 +169,7 @@ export interface Activity {
157
169
  txHash: Hex | null;
158
170
  /** `done` once on chain (in a block: a reorg may still take it back until final), `failed` for a payment or
159
171
  * withdrawal that cannot land, shown only on the device that tried it. What is not on chain yet is not activity:
160
- * an operation under way is in `operations()`, a note only announced is `incoming` in `balances()`. */
172
+ * an operation under way is in `operations()`, and the note it is making `incoming` in `balances()`. */
161
173
  status: 'done' | 'failed';
162
174
  }
163
175
 
@@ -240,10 +252,12 @@ export class Wallet {
240
252
  #busy = 0;
241
253
  #release = () => {};
242
254
  #referrer: Referrer | undefined;
255
+ readonly #unlock: (() => Promise<Uint8Array>) | undefined;
243
256
  #closed = new AbortController();
244
257
 
245
- private constructor(ctx: Ctx) {
258
+ private constructor(ctx: Ctx, unlock?: () => Promise<Uint8Array>) {
246
259
  this.#ctx = ctx;
260
+ this.#unlock = unlock;
247
261
  }
248
262
 
249
263
  /** Lets this wallet open again: in another tab, or here for another account. This object is done with: what is
@@ -260,9 +274,7 @@ export class Wallet {
260
274
 
261
275
  /** Opens the wallet. The first time on this address, registers it for payments: one signature. */
262
276
  static async open(opts: OpenOptions): Promise<Wallet> {
263
- await initCore();
264
277
  checkNetwork(opts.network);
265
- if (opts.seed.length !== 32) fail('invalid', 'the seed is 32 bytes');
266
278
  // the factory refuses these, which the relayer would report as its own failure
267
279
  const r = opts.referrer;
268
280
  if (r) {
@@ -271,17 +283,23 @@ export class Wallet {
271
283
  if (!Number.isInteger(r.bps) || r.bps < 0 || r.bps > MAX_REFERRAL_BPS)
272
284
  fail('invalid', `a referrer takes 0 to ${MAX_REFERRAL_BPS} bps, not ${r.bps}`);
273
285
  }
274
- const account = opts.walletClient.account ?? fail('invalid', 'the wallet client has no account');
286
+ if (!opts.account === !opts.walletClient) fail('invalid', 'give an account or a wallet client, one of the two');
287
+ const account = opts.account ?? opts.walletClient!.account ?? fail('invalid', 'the wallet client has no account');
288
+ // asked once the options are checked, and before anything is waited on: a browser shows a passkey prompt only in
289
+ // the click that opened
290
+ const seed = typeof opts.seed === 'function' ? await opts.seed() : opts.seed;
291
+ if (seed.length !== 32) fail('invalid', 'the seed is 32 bytes');
292
+ await initCore();
275
293
  const release = await lockWallet(account.address);
276
294
  try {
277
- return await Wallet.#open(opts, account, release);
295
+ return await Wallet.#open(opts, seed, account, release);
278
296
  } catch (e) {
279
297
  release();
280
298
  throw e;
281
299
  }
282
300
  }
283
301
 
284
- static async #open(opts: OpenOptions, account: Account, release: () => void): Promise<Wallet> {
302
+ static async #open(opts: OpenOptions, seed: Uint8Array, account: Account, release: () => void): Promise<Wallet> {
285
303
  const address = account.address;
286
304
  const chains = opts.network.chains.map((config) => ({
287
305
  config,
@@ -294,16 +312,23 @@ export class Wallet {
294
312
  if (code && code !== '0x' && !code.startsWith('0xef0100'))
295
313
  fail('unsupported-account', 'contract accounts cannot sign for a wallet');
296
314
  const signer: Signer = async (message) =>
297
- hexToBytes(await guard(opts.walletClient.signMessage({ account, message })));
315
+ hexToBytes(
316
+ await guard(
317
+ account.type === 'local'
318
+ ? account.signMessage({ message })
319
+ : opts.walletClient!.signMessage({ account, message }),
320
+ ),
321
+ );
298
322
  const storage = await CanisterClient.connect(opts.network.storage.host, opts.network.storage.canisterId);
299
- const publicKey = hexToBytes((core.spendKey(opts.seed) as SpendKey).publicKey);
323
+ const publicKey = hexToBytes((core.spendKey(seed) as SpendKey).publicKey);
300
324
  const saved = await opts.persist.load();
301
325
  // registered already: used before, here or on another device, so its past is looked for
302
326
  // ponytail: from genesis (~4 MB per million commitments); ask for a birthday once the chain has many
303
327
  const restoring =
304
328
  !saved?.length && opts.birthday === undefined && !!(await storage.registration(hexToBytes(address)));
305
- const view = await registeredViewKey(storage, opts.seed, signer, hexToBytes(address), publicKey);
306
- const loaded = saved?.length ? openState(opts.seed, saved) : null;
329
+ const view = await registeredViewKey(storage, seed, signer, hexToBytes(address), publicKey);
330
+ const key = stateKey(seed);
331
+ const loaded = saved?.length ? openState(key, saved) : null;
307
332
  // never started over in place: the first save would overwrite what it held, operations in flight included
308
333
  if (saved?.length && !loaded) {
309
334
  fail(
@@ -316,27 +341,60 @@ export class Wallet {
316
341
  const state: WalletState = loaded ?? emptyWalletState(address);
317
342
  const ctx: Ctx = {
318
343
  network: opts.network,
319
- seed: opts.seed,
344
+ seed,
345
+ owners: new Map(),
320
346
  address,
321
- walletClient: opts.walletClient,
347
+ account,
348
+ walletClient: opts.walletClient ?? null,
322
349
  storage,
323
350
  view,
324
351
  chains,
325
352
  state,
326
- save: () => opts.persist.save(sealState(opts.seed, state)),
353
+ save: () => opts.persist.save(sealState(key, state)),
327
354
  lastFullRefreshMs: 0,
328
355
  };
329
356
  if (!loaded) await start(ctx, restoring ? 0n : opts.birthday);
330
- const wallet = new Wallet(ctx);
357
+ const wallet = new Wallet(ctx, typeof opts.seed === 'function' ? opts.seed : undefined);
331
358
  wallet.#release = release;
332
359
  wallet.#referrer = opts.referrer;
333
360
  // a saved state names its tokens by id, and its coins stand by the chains' roots: read both now, so balances, coins
334
361
  // and history work before the first sync
335
362
  const [tokens] = await Promise.all([Promise.all(chains.map((c) => c.chain.tokens())), wallet.#readRoots()]);
336
363
  wallet.#tokens = tokens.flat();
364
+ keyAll(ctx, wallet.#tokens);
337
365
  return wallet;
338
366
  }
339
367
 
368
+ /** Holds `seed`, asked of `#unlock`, once it is checked to be this wallet's. */
369
+ #hold(seed: Uint8Array): void {
370
+ if ((core.spendKey(seed) as SpendKey).publicKey !== owner(this.#ctx)!.publicKey)
371
+ fail('invalid', "the seed given is another wallet's");
372
+ this.#ctx.seed = seed;
373
+ keyAll(this.#ctx, this.#tokens);
374
+ }
375
+
376
+ /** Within a task: the seed held, asked of `#unlock` if need be. */
377
+ async #seed(): Promise<void> {
378
+ if (!this.#ctx.seed) this.#hold(await this.#unlock!());
379
+ }
380
+
381
+ /** At the end of a sync: lets the seed go when it was opened with a function for it, nothing waits behind the sync,
382
+ * and no payment, withdrawal or consolidation is in flight, as each may sign again (a JoinSplit that reverts is
383
+ * proved again, and every merge round is signed). */
384
+ #dropSeed(): void {
385
+ if (!this.#unlock || this.#busy > 1) return;
386
+ if (this.#ctx.state.ops.some((o) => o.status === 'pending' && o.kind !== 'deposit')) return;
387
+ this.#ctx.seed = null;
388
+ }
389
+
390
+ /** `#exclusive` for a call that `needs` the seed: held first, asked of `#unlock` if need be before the call waits on
391
+ * anything else (a browser shows a passkey prompt only in the click that asked), and queued in the step that took it,
392
+ * so that a sync ending meanwhile does not let it go. */
393
+ async #seeded<T>(f: () => Promise<T>, needs = true): Promise<T> {
394
+ if (needs && !this.#ctx.seed) this.#hold(await this.#unlock!());
395
+ return this.#exclusive(f);
396
+ }
397
+
340
398
  /** Runs `f` after everything queued before it: one state, changed by one task at a time. */
341
399
  #exclusive<T>(f: () => Promise<T>): Promise<T> {
342
400
  this.#busy++;
@@ -372,13 +430,14 @@ export class Wallet {
372
430
 
373
431
  /** Catches up: new notes, deposits, spends and roots, then advances every operation in flight. A chain or server that
374
432
  * fails holds back only what needs it, and the first error is thrown once the rest is done. Settles before the burn
375
- * addresses are topped up, which can take a phone tens of seconds. */
433
+ * addresses are topped up, which can take a phone some seconds. */
376
434
  sync(): Promise<void> {
377
435
  return new Promise((resolve, reject) => {
378
436
  this.#exclusive(async () => {
379
437
  await this.#catchUp().then(resolve, reject);
380
438
  // still this task's: a deposit queued meanwhile finds the one recorded ahead, rather than writing at handout
381
439
  await topUpBurns(this.#ctx, this.#closed.signal).catch(() => {}); // tried again after the next sync
440
+ this.#dropSeed();
382
441
  }).catch(reject); // closed before it ran
383
442
  });
384
443
  }
@@ -424,7 +483,7 @@ export class Wallet {
424
483
  if (errors.length > 0) throw errors[0];
425
484
  }
426
485
 
427
- /** Syncs on its own: every 15 s while something is under way, every 5 min otherwise. Returns a stop function. */
486
+ /** Syncs on its own: every 3 s while something is under way, every 10 s otherwise. Returns a stop function. */
428
487
  autoSync(onChange?: () => void, onError?: (e: unknown) => void): () => void {
429
488
  let stopped = false;
430
489
  let timer: ReturnType<typeof setTimeout> | undefined;
@@ -437,7 +496,7 @@ export class Wallet {
437
496
  }
438
497
  if (stopped || this.#closed.signal.aborted) return;
439
498
  const busy = this.#ctx.state.ops.some((o) => o.status === 'pending' && Date.now() - o.createdMs < WAIT_MS * 4);
440
- timer = setTimeout(tick, busy ? 15_000 : 300_000);
499
+ timer = setTimeout(tick, busy ? 3_000 : 10_000);
441
500
  };
442
501
  void tick();
443
502
  return () => {
@@ -460,18 +519,22 @@ export class Wallet {
460
519
 
461
520
  balances(): Balance[] {
462
521
  const coins = this.coins();
522
+ // storage says which notes to look for, not that they come: a payer writes its notes there before its JoinSplit
523
+ // leaves, and one that never lands leaves them behind. So a note counts once it is a coin (its commitment read);
524
+ // before that, only a note this device's own operation under way is making counts, as incoming. A zero-value
525
+ // change, there to carry its payment's receipt, never counts
526
+ const making = new Set(
527
+ this.#ctx.state.ops.flatMap((o) =>
528
+ o.status === 'pending' && o.kind !== 'deposit' ? (o.data.changes ?? []) : [],
529
+ ),
530
+ );
463
531
  return this.#tokens.map((token) => {
464
532
  const { chainId, tokenId } = token;
465
533
  const b: Balance = { token, spendable: 0n, pending: [], blocked: 0n };
466
- // a merge that never landed (another device spent an input first) stops counting after a day; a zero-value
467
- // change, there to carry its payment's receipt, never counts. A note is a coin once its commitment is read, so
468
- // until then it is incoming, as a payment's change is between its relay and the next sync
469
534
  for (const n of this.#ctx.state.notes.filter(
470
- (n) => n.chainId === chainId && n.tokenId === tokenId && n.value > 0n,
535
+ (n) => n.chainId === chainId && n.tokenId === tokenId && n.value > 0n && making.has(n.salt),
471
536
  )) {
472
- const stale =
473
- n.receipt?.kind === 'merged' && n.atNs !== null && Date.now() - Number(n.atNs / 1_000_000n) > DAY_MS;
474
- if (!n.found.some((f) => f.confirmed) && !stale) b.pending.push({ amount: n.value, reason: 'incoming' });
537
+ if (!n.found.some((f) => f.confirmed)) b.pending.push({ amount: n.value, reason: 'incoming' });
475
538
  }
476
539
  for (const c of coins.filter((c) => c.token === token)) {
477
540
  if (c.state === 'spendable') b.spendable += c.amount;
@@ -483,13 +546,14 @@ export class Wallet {
483
546
  }
484
547
 
485
548
  /** Every unspent note and deposit on chain, and where each stands, as of opening or the last sync. A note announced
486
- * but not on chain yet is not one: `balances()` counts it as incoming. */
549
+ * but not read on chain yet is not one: `balances()` counts it as incoming if this device's operation is making it. */
487
550
  coins(): CoinInfo[] {
488
551
  const size = (chainId: bigint) => this.#status.get(chainId)?.latestRoot?.size ?? 0;
489
552
  const unproved = (chainId: bigint) => (this.#status.get(chainId)?.halted ? 'roots-halted' : 'awaiting-root');
490
553
  const spending = new Set(this.#ctx.state.ops.flatMap((o) => inputsOf(o).map((c) => `${o.chainId}:${c.index}`)));
491
- // spent by a JoinSplit in a block, which waits only to be final: gone, as activity() counts it (a reorg that drops
492
- // it brings them back as spending, and the operation relays it again)
554
+ // spent by a JoinSplit in a block, which waits only to be final: gone, as activity() counts it. Sync sees it by its
555
+ // nullifier (`unspent`); this device's own is gone already from its relay on (a reorg that drops it brings them
556
+ // back as spending, and the operation relays it again)
493
557
  const landed = new Set(
494
558
  this.#ctx.state.ops.flatMap((o) =>
495
559
  o.kind !== 'deposit' && o.data.step?.stage === 'relayed'
@@ -500,7 +564,7 @@ export class Wallet {
500
564
  const out: CoinInfo[] = [];
501
565
  // a zero-value change carries its payment's receipt, and is no coin
502
566
  for (const n of this.#ctx.state.notes.filter((n) => n.value > 0n)) {
503
- for (const f of n.found.filter((f) => !f.spent)) {
567
+ for (const f of n.found.filter(unspent)) {
504
568
  const id = `${n.chainId}:${f.index}`;
505
569
  // a 4-byte tag alone matches someone else's now and then: a coin once its commitment is read
506
570
  if (!f.confirmed || landed.has(id)) continue;
@@ -518,7 +582,7 @@ export class Wallet {
518
582
  }
519
583
  for (const d of this.#ctx.state.deposits) {
520
584
  // a coin once its commitment is read, which names the amount
521
- for (const f of d.found.filter((f) => !f.spent && f.amount !== null)) {
585
+ for (const f of d.found.filter((f) => unspent(f) && f.amount !== null)) {
522
586
  const id = `${d.chainId}:${f.index}`;
523
587
  if (landed.has(id)) continue;
524
588
  const state =
@@ -768,7 +832,7 @@ export class Wallet {
768
832
  /** Refunds deposit `id` to its sender without the ASP, the attester or a proved root: for a deposit the ASP
769
833
  * rejected, or when those services stop. The account's wallet sends the transactions and pays their gas. */
770
834
  rageExit(id: string): Promise<Hex[]> {
771
- return this.#exclusive(async () => {
835
+ return this.#seeded(async () => {
772
836
  const [chain, index] = id.split(':');
773
837
  const record =
774
838
  this.#ctx.state.deposits.find((d) => `${d.chainId}` === chain && d.found.some((f) => `${f.index}` === index)) ??
@@ -874,22 +938,26 @@ export class Wallet {
874
938
  * With `wrapped`, it pays from the account's zERC20 itself, as a withdrawal pays out, rather than its underlying
875
939
  * asset: a plain transfer into the burn address, with no wrap and no approval.
876
940
  *
941
+ * With `from`, another wallet of the user's (a browser wallet) pays instead of the account: its balance is checked,
942
+ * it approves and wraps, and an exit refunds it there. The account pays nothing and signs nothing.
943
+ *
877
944
  * Returns once the wrap is sent: the operation is done once it is mined, which `wait()` or the next sync finds.
878
945
  * `onStep` hears what it waits on meanwhile, for an app to show. */
879
- deposit(opts: {
946
+ async deposit(opts: {
880
947
  token: Token;
881
948
  amount: bigint;
882
949
  to?: Address;
883
950
  wrapped?: boolean;
951
+ from?: WalletClient;
884
952
  onStep?: (step: DepositStep) => void;
885
953
  }): Promise<Operation> {
954
+ if (opts.amount <= 0n) fail('invalid', 'the amount must be positive');
955
+ const payer = opts.from ? (opts.from.account ?? fail('invalid', 'the wallet client has no account')) : null;
886
956
  const step = opts.onStep ?? (() => {});
887
957
  if (this.#busy > 0) step('queued');
888
958
  return this.#exclusive(async () => {
889
959
  const { token, amount } = opts;
890
- if (amount <= 0n) fail('invalid', 'the amount must be positive');
891
- const wc = this.#ctx.walletClient;
892
- const account = wc.account!;
960
+ const account = payer ?? this.#ctx.account;
893
961
  const client = chainCtx(this.#ctx, token.chainId).chain.client;
894
962
  const { wrapped } = opts;
895
963
  const native = !wrapped && token.underlying.toLowerCase() === NATIVE.toLowerCase();
@@ -906,7 +974,7 @@ export class Wallet {
906
974
  const of = wrapped ? token.symbol : `${token.symbol}'s underlying asset`;
907
975
  fail(
908
976
  'insufficient-funds',
909
- `the account holds ${formatUnits(held, token.decimals)} of ${of}, less than ${formatUnits(amount, token.decimals)}`,
977
+ `${payer ? 'the wallet' : 'the account'} holds ${formatUnits(held, token.decimals)} of ${of}, less than ${formatUnits(amount, token.decimals)}`,
910
978
  );
911
979
  }
912
980
  const address = opts.to
@@ -920,8 +988,7 @@ export class Wallet {
920
988
  );
921
989
  await this.#ctx.save();
922
990
  try {
923
- if ((await wc.getChainId()) !== Number(token.chainId))
924
- await guard(wc.switchChain({ id: Number(token.chainId) }));
991
+ const wc = opts.from ? await switched(opts.from, token.chainId) : await walletOn(this.#ctx, token.chainId);
925
992
  if (!native && !wrapped) {
926
993
  const allowance = await client.readContract({
927
994
  address: token.underlying,
@@ -1056,19 +1123,21 @@ export class Wallet {
1056
1123
  * nothing; it names neither this address nor its public key. Anyone who gets the code can pay it, and see what its
1057
1124
  * other payers paid privately: give each payer its own. It is checksummed text (bech32m, `zerc201…`). */
1058
1125
  receiveCode(token: Token): Promise<string> {
1059
- return this.#exclusive(async () => {
1060
- const ctx = this.#ctx;
1126
+ const ctx = this.#ctx;
1127
+ const code = (index: number) => ({ chainId: token.chainId, tokenId: token.tokenId, index });
1128
+ // the seed is asked for only past the codes derived ahead
1129
+ const derived = !!owner(ctx, code(nextBurn(ctx, token.chainId, token.tokenId)));
1130
+ return this.#seeded(async () => {
1061
1131
  const { burnIndex, address } = await this.#burnAddress(token);
1132
+ if (!owner(ctx, code(burnIndex))) await this.#seed(); // calls queued before took the next ones
1062
1133
  return encodeCode({
1063
1134
  chainId: token.chainId,
1064
1135
  tokenId: token.tokenId,
1065
1136
  burnAddress: hexToBytes(address),
1066
- publicKey: hexToBytes(
1067
- spendKey(ctx, { chainId: token.chainId, tokenId: token.tokenId, index: burnIndex }).publicKey,
1068
- ),
1137
+ publicKey: hexToBytes(owner(ctx, code(burnIndex))!.publicKey),
1069
1138
  key: codeKey(ctx.view.seed, token.chainId, token.tokenId, burnIndex),
1070
1139
  });
1071
- });
1140
+ }, !derived);
1072
1141
  }
1073
1142
 
1074
1143
  /** What a send or withdrawal of `amount` would cost now, or a consolidation of what the wallet holds. A send or
@@ -1153,10 +1222,11 @@ export class Wallet {
1153
1222
  * what is spendable now, or it fails here: `needs-consolidation`, `funds-settling` or `insufficient-funds`.
1154
1223
  * `amount` is what the recipient gets, and `maxFee` caps what it pays on top, `fees({ kind: 'send' }).fee`: the
1155
1224
  * relayer names its fee, so a quote the user saw is the cap. */
1156
- send(opts: { to: string; token: Token; amount: bigint; maxFee: bigint }): Promise<Operation> {
1157
- return this.#exclusive(async () => {
1158
- const { token, amount } = opts;
1159
- if (amount <= 0n) fail('invalid', 'the amount must be positive');
1225
+ async send(opts: { to: string; token: Token; amount: bigint; maxFee: bigint }): Promise<Operation> {
1226
+ const { token, amount } = opts;
1227
+ if (amount <= 0n) fail('invalid', 'the amount must be positive');
1228
+ // the JoinSplit is proved by a later sync, which holds the seed until the payment is done
1229
+ return this.#seeded(async () => {
1160
1230
  let to: Destination;
1161
1231
  if (isAddress(opts.to)) {
1162
1232
  const recipient = await lookupRecipient(this.#ctx.storage, hexToBytes(opts.to));
@@ -1188,7 +1258,7 @@ export class Wallet {
1188
1258
  * touched; one still settling is waited for. `maxFee` caps the fees of every merge together,
1189
1259
  * `fees({ kind: 'consolidate' }).fee`. */
1190
1260
  consolidate(opts: { token: Token; coins?: string[]; maxFee: bigint }): Promise<Operation> {
1191
- return this.#exclusive(async () => {
1261
+ return this.#seeded(async () => {
1192
1262
  if (!this.#ctx.network.mergeProver) fail('invalid', 'this network has no merge prover');
1193
1263
  const to: Destination = { kind: 'self' };
1194
1264
  const data: SpendData = { to, maxFee: opts.maxFee, step: null, paid: 0n };
@@ -1215,10 +1285,16 @@ export class Wallet {
1215
1285
  /** Withdraws `amount` publicly to `to`, as zERC20 or with `unwrap` the underlying asset itself, so `to` needs no gas
1216
1286
  * to unwrap. One JoinSplit, as `send`, and as there `to` gets all of `amount` and `maxFee` caps what is paid on top
1217
1287
  * (`fees({ kind: 'withdraw' }).fee`). */
1218
- withdraw(opts: { to: Address; token: Token; amount: bigint; maxFee: bigint; unwrap?: boolean }): Promise<Operation> {
1219
- return this.#exclusive(async () => {
1220
- if (!isAddress(opts.to)) fail('invalid', `${String(opts.to)} is not an address`);
1221
- if (opts.amount <= 0n) fail('invalid', 'the amount must be positive');
1288
+ async withdraw(opts: {
1289
+ to: Address;
1290
+ token: Token;
1291
+ amount: bigint;
1292
+ maxFee: bigint;
1293
+ unwrap?: boolean;
1294
+ }): Promise<Operation> {
1295
+ if (!isAddress(opts.to)) fail('invalid', `${String(opts.to)} is not an address`);
1296
+ if (opts.amount <= 0n) fail('invalid', 'the amount must be positive');
1297
+ return this.#seeded(async () => {
1222
1298
  const to: Destination = { kind: 'public', address: opts.to, unwrap: opts.unwrap };
1223
1299
  return this.#newPayment('withdraw', opts.token, opts.amount, { to, maxFee: opts.maxFee, step: null, paid: 0n });
1224
1300
  });
@@ -7,18 +7,27 @@
7
7
  */
8
8
  export function aspLeaves(bitmap: Uint8Array, first: bigint, count: number): Uint8Array;
9
9
 
10
+ export function burnAddressOf(chain_id: bigint, public_key: string, salt: string): string | undefined;
11
+
10
12
  /**
11
- * A seed's `index`-th burn address for one chain and token, with its salt: `{salt, address}`. About 2^16 hashes.
13
+ * The `index`-th burn address of `public_key` on one chain and token from `burn_key`, and its salt, if the salt's
14
+ * counter is in `from..=to`: `{salt, address}` or `undefined`. Searched in blocks of counters from 0, the result is
15
+ * the first block's hit.
12
16
  */
13
- export function burnAddress(seed: Uint8Array, chain_id: bigint, token_id: number, index: number): any;
17
+ export function burnBlock(burn_key: Uint8Array, chain_id: bigint, token_id: number, index: number, public_key: string, from: number, to: number): any;
14
18
 
15
- export function burnAddressOf(chain_id: bigint, public_key: string, salt: string): string | undefined;
19
+ /**
20
+ * A burn address of someone else's `public_key` on a chain, with its salt: the first of the `count` salts from
21
+ * `start` that has the proof of work, `{salt, address}`, or `undefined`. A random `start` makes the salt
22
+ * unguessable.
23
+ */
24
+ export function burnBlockFor(chain_id: bigint, public_key: string, start: string, count: number): any;
16
25
 
17
26
  /**
18
27
  * The key that owns the notes paid to a seed's `index`-th receive code for one chain and token, shaped like
19
28
  * `spendKey`.
20
29
  */
21
- export function codeKey(seed: Uint8Array, chain_id: bigint, token_id: number, index: number): any;
30
+ export function codeSpendKey(seed: Uint8Array, chain_id: bigint, token_id: number, index: number): any;
22
31
 
23
32
  /**
24
33
  * `CommitmentLedger`'s hash chain after appending a commitment with `fields` to `prev`.
@@ -64,12 +73,6 @@ export function path(i: bigint, n: bigint, chunk_leaves: Uint8Array, page_chunks
64
73
  */
65
74
  export function pathRoot(leaf: string, i: bigint, siblings: string[]): string;
66
75
 
67
- /**
68
- * The `k`-th burn address a point of sale holding `lane_key` makes for `public_key` on one chain and token, with its
69
- * salt: `{salt, address}`. About 2^16 hashes.
70
- */
71
- export function posBurnAddress(lane_key: Uint8Array, chain_id: bigint, token_id: number, k: number, public_key: string): any;
72
-
73
76
  /**
74
77
  * Seals a merge witness (JSON) to the merge prover's key, padded with spaces to `pad_to` bytes. Take the key from
75
78
  * `verifyAttestation`, as for the attester's.
@@ -123,9 +126,10 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
123
126
  export interface InitOutput {
124
127
  readonly memory: WebAssembly.Memory;
125
128
  readonly aspLeaves: (a: number, b: number, c: bigint, d: number) => [number, number, number, number];
126
- readonly burnAddress: (a: number, b: number, c: bigint, d: number, e: number) => [number, number, number];
127
129
  readonly burnAddressOf: (a: bigint, b: number, c: number, d: number, e: number) => [number, number, number, number];
128
- readonly codeKey: (a: number, b: number, c: bigint, d: number, e: number) => [number, number, number];
130
+ readonly burnBlock: (a: number, b: number, c: bigint, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number];
131
+ readonly burnBlockFor: (a: bigint, b: number, c: number, d: number, e: number, f: number) => [number, number, number];
132
+ readonly codeSpendKey: (a: number, b: number, c: bigint, d: number, e: number) => [number, number, number];
129
133
  readonly hashChain: (a: number, b: number, c: number, d: number) => [number, number, number, number];
130
134
  readonly leaf: (a: number, b: number) => [number, number, number, number];
131
135
  readonly leaves: (a: number, b: number) => [number, number, number, number];
@@ -134,7 +138,6 @@ export interface InitOutput {
134
138
  readonly nullifier: (a: bigint, b: bigint, c: number, d: number) => [number, number, number, number];
135
139
  readonly path: (a: bigint, b: bigint, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number) => [number, number, number, number];
136
140
  readonly pathRoot: (a: number, b: number, c: bigint, d: number, e: number) => [number, number, number, number];
137
- readonly posBurnAddress: (a: number, b: number, c: bigint, d: number, e: number, f: number, g: number) => [number, number, number];
138
141
  readonly sealMergeWitness: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
139
142
  readonly sealWitness: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
140
143
  readonly signMerge: (a: number, b: number, c: number, d: number) => [number, number, number, number];
@@ -20,24 +20,6 @@ export function aspLeaves(bitmap, first, count) {
20
20
  return v2;
21
21
  }
22
22
 
23
- /**
24
- * A seed's `index`-th burn address for one chain and token, with its salt: `{salt, address}`. About 2^16 hashes.
25
- * @param {Uint8Array} seed
26
- * @param {bigint} chain_id
27
- * @param {number} token_id
28
- * @param {number} index
29
- * @returns {any}
30
- */
31
- export function burnAddress(seed, chain_id, token_id, index) {
32
- const ptr0 = passArray8ToWasm0(seed, wasm.__wbindgen_malloc);
33
- const len0 = WASM_VECTOR_LEN;
34
- const ret = wasm.burnAddress(ptr0, len0, chain_id, token_id, index);
35
- if (ret[2]) {
36
- throw takeFromExternrefTable0(ret[1]);
37
- }
38
- return takeFromExternrefTable0(ret[0]);
39
- }
40
-
41
23
  /**
42
24
  * @param {bigint} chain_id
43
25
  * @param {string} public_key
@@ -61,6 +43,53 @@ export function burnAddressOf(chain_id, public_key, salt) {
61
43
  return v3;
62
44
  }
63
45
 
46
+ /**
47
+ * The `index`-th burn address of `public_key` on one chain and token from `burn_key`, and its salt, if the salt's
48
+ * counter is in `from..=to`: `{salt, address}` or `undefined`. Searched in blocks of counters from 0, the result is
49
+ * the first block's hit.
50
+ * @param {Uint8Array} burn_key
51
+ * @param {bigint} chain_id
52
+ * @param {number} token_id
53
+ * @param {number} index
54
+ * @param {string} public_key
55
+ * @param {number} from
56
+ * @param {number} to
57
+ * @returns {any}
58
+ */
59
+ export function burnBlock(burn_key, chain_id, token_id, index, public_key, from, to) {
60
+ const ptr0 = passArray8ToWasm0(burn_key, wasm.__wbindgen_malloc);
61
+ const len0 = WASM_VECTOR_LEN;
62
+ const ptr1 = passStringToWasm0(public_key, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
63
+ const len1 = WASM_VECTOR_LEN;
64
+ const ret = wasm.burnBlock(ptr0, len0, chain_id, token_id, index, ptr1, len1, from, to);
65
+ if (ret[2]) {
66
+ throw takeFromExternrefTable0(ret[1]);
67
+ }
68
+ return takeFromExternrefTable0(ret[0]);
69
+ }
70
+
71
+ /**
72
+ * A burn address of someone else's `public_key` on a chain, with its salt: the first of the `count` salts from
73
+ * `start` that has the proof of work, `{salt, address}`, or `undefined`. A random `start` makes the salt
74
+ * unguessable.
75
+ * @param {bigint} chain_id
76
+ * @param {string} public_key
77
+ * @param {string} start
78
+ * @param {number} count
79
+ * @returns {any}
80
+ */
81
+ export function burnBlockFor(chain_id, public_key, start, count) {
82
+ const ptr0 = passStringToWasm0(public_key, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
83
+ const len0 = WASM_VECTOR_LEN;
84
+ const ptr1 = passStringToWasm0(start, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
85
+ const len1 = WASM_VECTOR_LEN;
86
+ const ret = wasm.burnBlockFor(chain_id, ptr0, len0, ptr1, len1, count);
87
+ if (ret[2]) {
88
+ throw takeFromExternrefTable0(ret[1]);
89
+ }
90
+ return takeFromExternrefTable0(ret[0]);
91
+ }
92
+
64
93
  /**
65
94
  * The key that owns the notes paid to a seed's `index`-th receive code for one chain and token, shaped like
66
95
  * `spendKey`.
@@ -70,10 +99,10 @@ export function burnAddressOf(chain_id, public_key, salt) {
70
99
  * @param {number} index
71
100
  * @returns {any}
72
101
  */
73
- export function codeKey(seed, chain_id, token_id, index) {
102
+ export function codeSpendKey(seed, chain_id, token_id, index) {
74
103
  const ptr0 = passArray8ToWasm0(seed, wasm.__wbindgen_malloc);
75
104
  const len0 = WASM_VECTOR_LEN;
76
- const ret = wasm.codeKey(ptr0, len0, chain_id, token_id, index);
105
+ const ret = wasm.codeSpendKey(ptr0, len0, chain_id, token_id, index);
77
106
  if (ret[2]) {
78
107
  throw takeFromExternrefTable0(ret[1]);
79
108
  }
@@ -295,28 +324,6 @@ export function pathRoot(leaf, i, siblings) {
295
324
  }
296
325
  }
297
326
 
298
- /**
299
- * The `k`-th burn address a point of sale holding `lane_key` makes for `public_key` on one chain and token, with its
300
- * salt: `{salt, address}`. About 2^16 hashes.
301
- * @param {Uint8Array} lane_key
302
- * @param {bigint} chain_id
303
- * @param {number} token_id
304
- * @param {number} k
305
- * @param {string} public_key
306
- * @returns {any}
307
- */
308
- export function posBurnAddress(lane_key, chain_id, token_id, k, public_key) {
309
- const ptr0 = passArray8ToWasm0(lane_key, wasm.__wbindgen_malloc);
310
- const len0 = WASM_VECTOR_LEN;
311
- const ptr1 = passStringToWasm0(public_key, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
312
- const len1 = WASM_VECTOR_LEN;
313
- const ret = wasm.posBurnAddress(ptr0, len0, chain_id, token_id, k, ptr1, len1);
314
- if (ret[2]) {
315
- throw takeFromExternrefTable0(ret[1]);
316
- }
317
- return takeFromExternrefTable0(ret[0]);
318
- }
319
-
320
327
  /**
321
328
  * Seals a merge witness (JSON) to the merge prover's key, padded with spaces to `pad_to` bytes. Take the key from
322
329
  * `verifyAttestation`, as for the attester's.
Binary file
@@ -2,9 +2,10 @@
2
2
  /* eslint-disable */
3
3
  export const memory: WebAssembly.Memory;
4
4
  export const aspLeaves: (a: number, b: number, c: bigint, d: number) => [number, number, number, number];
5
- export const burnAddress: (a: number, b: number, c: bigint, d: number, e: number) => [number, number, number];
6
5
  export const burnAddressOf: (a: bigint, b: number, c: number, d: number, e: number) => [number, number, number, number];
7
- export const codeKey: (a: number, b: number, c: bigint, d: number, e: number) => [number, number, number];
6
+ export const burnBlock: (a: number, b: number, c: bigint, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number];
7
+ export const burnBlockFor: (a: bigint, b: number, c: number, d: number, e: number, f: number) => [number, number, number];
8
+ export const codeSpendKey: (a: number, b: number, c: bigint, d: number, e: number) => [number, number, number];
8
9
  export const hashChain: (a: number, b: number, c: number, d: number) => [number, number, number, number];
9
10
  export const leaf: (a: number, b: number) => [number, number, number, number];
10
11
  export const leaves: (a: number, b: number) => [number, number, number, number];
@@ -13,7 +14,6 @@ export const noteTag: (a: number, b: number, c: number) => [number, number, numb
13
14
  export const nullifier: (a: bigint, b: bigint, c: number, d: number) => [number, number, number, number];
14
15
  export const path: (a: bigint, b: bigint, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number) => [number, number, number, number];
15
16
  export const pathRoot: (a: number, b: number, c: bigint, d: number, e: number) => [number, number, number, number];
16
- export const posBurnAddress: (a: number, b: number, c: bigint, d: number, e: number, f: number, g: number) => [number, number, number];
17
17
  export const sealMergeWitness: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
18
18
  export const sealWitness: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
19
19
  export const signMerge: (a: number, b: number, c: number, d: number) => [number, number, number, number];