@zerc20/wallet-sdk 0.1.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.
Files changed (57) hide show
  1. package/dist/burnWorker.js +6 -6
  2. package/dist/burnWorker.js.map +1 -1
  3. package/dist/chain.d.ts +19 -5
  4. package/dist/chain.js +17 -11
  5. package/dist/chain.js.map +1 -1
  6. package/dist/context.d.ts +23 -4
  7. package/dist/context.js +35 -6
  8. package/dist/context.js.map +1 -1
  9. package/dist/core.d.ts +32 -17
  10. package/dist/core.js +253 -70
  11. package/dist/core.js.map +1 -1
  12. package/dist/errors.d.ts +2 -0
  13. package/dist/errors.js.map +1 -1
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.js +2 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/pos.js +2 -2
  18. package/dist/pos.js.map +1 -1
  19. package/dist/rageExit.js +3 -5
  20. package/dist/rageExit.js.map +1 -1
  21. package/dist/seed.d.ts +4 -0
  22. package/dist/seed.js +12 -1
  23. package/dist/seed.js.map +1 -1
  24. package/dist/services.d.ts +5 -5
  25. package/dist/services.js +10 -7
  26. package/dist/services.js.map +1 -1
  27. package/dist/spend.d.ts +1 -1
  28. package/dist/spend.js +5 -4
  29. package/dist/spend.js.map +1 -1
  30. package/dist/state.d.ts +14 -4
  31. package/dist/state.js +10 -7
  32. package/dist/state.js.map +1 -1
  33. package/dist/sync.d.ts +16 -3
  34. package/dist/sync.js +90 -25
  35. package/dist/sync.js.map +1 -1
  36. package/dist/wallet.d.ts +27 -13
  37. package/dist/wallet.js +125 -62
  38. package/dist/wallet.js.map +1 -1
  39. package/package.json +4 -3
  40. package/src/burnWorker.ts +9 -10
  41. package/src/chain.ts +35 -12
  42. package/src/context.ts +48 -8
  43. package/src/core.ts +279 -87
  44. package/src/errors.ts +2 -0
  45. package/src/index.ts +2 -2
  46. package/src/pos.ts +2 -2
  47. package/src/rageExit.ts +3 -4
  48. package/src/seed.ts +13 -1
  49. package/src/services.ts +18 -7
  50. package/src/spend.ts +5 -5
  51. package/src/state.ts +17 -7
  52. package/src/sync.ts +95 -24
  53. package/src/wallet.ts +159 -64
  54. package/wasm/zerc20_wallet_core.d.ts +16 -13
  55. package/wasm/zerc20_wallet_core.js +49 -42
  56. package/wasm/zerc20_wallet_core_bg.wasm +0 -0
  57. package/wasm/zerc20_wallet_core_bg.wasm.d.ts +3 -3
package/src/wallet.ts CHANGED
@@ -19,15 +19,17 @@ 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,
25
26
  hexToBytes,
26
27
  isAddress,
27
28
  numberToHex,
29
+ zeroAddress,
28
30
  } from 'viem';
29
- import { Chain, NATIVE, type Token, erc20Abi, tokenAbi } from './chain.js';
30
- import { type Ctx, type SpendKey, chainCtx, chainState, spendKey, walletSees } from './context.js';
31
+ import { Chain, MAX_REFERRAL_BPS, NATIVE, type Referrer, type Token, erc20Abi, tokenAbi } from './chain.js';
32
+ import { type Ctx, type SpendKey, chainCtx, chainState, owner, switched, walletOn, walletSees } from './context.js';
31
33
  import { burnAddressFor, core, initCore } from './core.js';
32
34
  import { type ErrorCode, WalletError, fail, guard } from './errors.js';
33
35
  import { type Network, checkNetwork } from './network.js';
@@ -61,10 +63,14 @@ import {
61
63
  emptyWalletState,
62
64
  openState,
63
65
  sealState,
66
+ stateKey,
67
+ unspent,
64
68
  } from './state.js';
65
69
  import {
66
70
  WAIT_MS,
67
71
  commitmentTx,
72
+ keyAll,
73
+ nextBurn,
68
74
  recordBurn,
69
75
  storageState,
70
76
  syncAll,
@@ -74,21 +80,30 @@ import {
74
80
  unusedBurn,
75
81
  } from './sync.js';
76
82
 
77
- const DAY_MS = 86_400_000;
78
83
  /** The ledger's bound outside an emergency. */
79
84
  const NO_BOUND = 2n ** 64n - 1n;
80
85
 
81
86
  export interface OpenOptions {
82
87
  network: Network;
83
- /** The user's wallet (MetaMask, WalletConnect, ...) as a viem client with its account. */
84
- walletClient: WalletClient;
85
- /** The 32-byte root of every key. See `seedFromSignature`. */
86
- 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>);
87
100
  /** Where the encrypted state lives between sessions. */
88
101
  persist: { load(): Promise<Uint8Array | null | undefined>; save(bytes: Uint8Array): Promise<void> };
89
102
  /** Where a wallet with no saved state starts looking: the block its seed was first used at, or roughly when. Without
90
103
  * it, an address registered before is looked for from the chain's start, and a new one from now on. */
91
104
  birthday?: bigint | Date;
105
+ /** The app opening the wallet, when it takes a share of what reaches the deposit addresses it hands out. */
106
+ referrer?: Referrer;
92
107
  }
93
108
 
94
109
  /** What a deposit, or a deposit address, waits on: for an app to show while it does. */
@@ -105,7 +120,7 @@ export type DepositStep =
105
120
 
106
121
  /** Why an amount is not spendable yet. */
107
122
  export type PendingReason =
108
- /** 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. */
109
124
  | 'incoming'
110
125
  /** Waiting for a proved root that covers it (minutes). */
111
126
  | 'awaiting-root'
@@ -154,7 +169,7 @@ export interface Activity {
154
169
  txHash: Hex | null;
155
170
  /** `done` once on chain (in a block: a reorg may still take it back until final), `failed` for a payment or
156
171
  * withdrawal that cannot land, shown only on the device that tried it. What is not on chain yet is not activity:
157
- * 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()`. */
158
173
  status: 'done' | 'failed';
159
174
  }
160
175
 
@@ -236,10 +251,13 @@ export class Wallet {
236
251
  /** Calls running or queued. */
237
252
  #busy = 0;
238
253
  #release = () => {};
254
+ #referrer: Referrer | undefined;
255
+ readonly #unlock: (() => Promise<Uint8Array>) | undefined;
239
256
  #closed = new AbortController();
240
257
 
241
- private constructor(ctx: Ctx) {
258
+ private constructor(ctx: Ctx, unlock?: () => Promise<Uint8Array>) {
242
259
  this.#ctx = ctx;
260
+ this.#unlock = unlock;
243
261
  }
244
262
 
245
263
  /** Lets this wallet open again: in another tab, or here for another account. This object is done with: what is
@@ -256,20 +274,32 @@ export class Wallet {
256
274
 
257
275
  /** Opens the wallet. The first time on this address, registers it for payments: one signature. */
258
276
  static async open(opts: OpenOptions): Promise<Wallet> {
259
- await initCore();
260
277
  checkNetwork(opts.network);
261
- if (opts.seed.length !== 32) fail('invalid', 'the seed is 32 bytes');
262
- const account = opts.walletClient.account ?? fail('invalid', 'the wallet client has no account');
278
+ // the factory refuses these, which the relayer would report as its own failure
279
+ const r = opts.referrer;
280
+ if (r) {
281
+ if (!isAddress(r.address, { strict: false }) || r.address.toLowerCase() === zeroAddress)
282
+ fail('invalid', `the referrer ${r.address} is not an address to pay`);
283
+ if (!Number.isInteger(r.bps) || r.bps < 0 || r.bps > MAX_REFERRAL_BPS)
284
+ fail('invalid', `a referrer takes 0 to ${MAX_REFERRAL_BPS} bps, not ${r.bps}`);
285
+ }
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();
263
293
  const release = await lockWallet(account.address);
264
294
  try {
265
- return await Wallet.#open(opts, account, release);
295
+ return await Wallet.#open(opts, seed, account, release);
266
296
  } catch (e) {
267
297
  release();
268
298
  throw e;
269
299
  }
270
300
  }
271
301
 
272
- 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> {
273
303
  const address = account.address;
274
304
  const chains = opts.network.chains.map((config) => ({
275
305
  config,
@@ -282,16 +312,23 @@ export class Wallet {
282
312
  if (code && code !== '0x' && !code.startsWith('0xef0100'))
283
313
  fail('unsupported-account', 'contract accounts cannot sign for a wallet');
284
314
  const signer: Signer = async (message) =>
285
- 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
+ );
286
322
  const storage = await CanisterClient.connect(opts.network.storage.host, opts.network.storage.canisterId);
287
- const publicKey = hexToBytes((core.spendKey(opts.seed) as SpendKey).publicKey);
323
+ const publicKey = hexToBytes((core.spendKey(seed) as SpendKey).publicKey);
288
324
  const saved = await opts.persist.load();
289
325
  // registered already: used before, here or on another device, so its past is looked for
290
326
  // ponytail: from genesis (~4 MB per million commitments); ask for a birthday once the chain has many
291
327
  const restoring =
292
328
  !saved?.length && opts.birthday === undefined && !!(await storage.registration(hexToBytes(address)));
293
- const view = await registeredViewKey(storage, opts.seed, signer, hexToBytes(address), publicKey);
294
- 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;
295
332
  // never started over in place: the first save would overwrite what it held, operations in flight included
296
333
  if (saved?.length && !loaded) {
297
334
  fail(
@@ -304,26 +341,60 @@ export class Wallet {
304
341
  const state: WalletState = loaded ?? emptyWalletState(address);
305
342
  const ctx: Ctx = {
306
343
  network: opts.network,
307
- seed: opts.seed,
344
+ seed,
345
+ owners: new Map(),
308
346
  address,
309
- walletClient: opts.walletClient,
347
+ account,
348
+ walletClient: opts.walletClient ?? null,
310
349
  storage,
311
350
  view,
312
351
  chains,
313
352
  state,
314
- save: () => opts.persist.save(sealState(opts.seed, state)),
353
+ save: () => opts.persist.save(sealState(key, state)),
315
354
  lastFullRefreshMs: 0,
316
355
  };
317
356
  if (!loaded) await start(ctx, restoring ? 0n : opts.birthday);
318
- const wallet = new Wallet(ctx);
357
+ const wallet = new Wallet(ctx, typeof opts.seed === 'function' ? opts.seed : undefined);
319
358
  wallet.#release = release;
359
+ wallet.#referrer = opts.referrer;
320
360
  // a saved state names its tokens by id, and its coins stand by the chains' roots: read both now, so balances, coins
321
361
  // and history work before the first sync
322
362
  const [tokens] = await Promise.all([Promise.all(chains.map((c) => c.chain.tokens())), wallet.#readRoots()]);
323
363
  wallet.#tokens = tokens.flat();
364
+ keyAll(ctx, wallet.#tokens);
324
365
  return wallet;
325
366
  }
326
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
+
327
398
  /** Runs `f` after everything queued before it: one state, changed by one task at a time. */
328
399
  #exclusive<T>(f: () => Promise<T>): Promise<T> {
329
400
  this.#busy++;
@@ -359,13 +430,14 @@ export class Wallet {
359
430
 
360
431
  /** Catches up: new notes, deposits, spends and roots, then advances every operation in flight. A chain or server that
361
432
  * fails holds back only what needs it, and the first error is thrown once the rest is done. Settles before the burn
362
- * addresses are topped up, which can take a phone tens of seconds. */
433
+ * addresses are topped up, which can take a phone some seconds. */
363
434
  sync(): Promise<void> {
364
435
  return new Promise((resolve, reject) => {
365
436
  this.#exclusive(async () => {
366
437
  await this.#catchUp().then(resolve, reject);
367
438
  // still this task's: a deposit queued meanwhile finds the one recorded ahead, rather than writing at handout
368
439
  await topUpBurns(this.#ctx, this.#closed.signal).catch(() => {}); // tried again after the next sync
440
+ this.#dropSeed();
369
441
  }).catch(reject); // closed before it ran
370
442
  });
371
443
  }
@@ -411,7 +483,7 @@ export class Wallet {
411
483
  if (errors.length > 0) throw errors[0];
412
484
  }
413
485
 
414
- /** 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. */
415
487
  autoSync(onChange?: () => void, onError?: (e: unknown) => void): () => void {
416
488
  let stopped = false;
417
489
  let timer: ReturnType<typeof setTimeout> | undefined;
@@ -424,7 +496,7 @@ export class Wallet {
424
496
  }
425
497
  if (stopped || this.#closed.signal.aborted) return;
426
498
  const busy = this.#ctx.state.ops.some((o) => o.status === 'pending' && Date.now() - o.createdMs < WAIT_MS * 4);
427
- timer = setTimeout(tick, busy ? 15_000 : 300_000);
499
+ timer = setTimeout(tick, busy ? 3_000 : 10_000);
428
500
  };
429
501
  void tick();
430
502
  return () => {
@@ -447,18 +519,22 @@ export class Wallet {
447
519
 
448
520
  balances(): Balance[] {
449
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
+ );
450
531
  return this.#tokens.map((token) => {
451
532
  const { chainId, tokenId } = token;
452
533
  const b: Balance = { token, spendable: 0n, pending: [], blocked: 0n };
453
- // a merge that never landed (another device spent an input first) stops counting after a day; a zero-value
454
- // change, there to carry its payment's receipt, never counts. A note is a coin once its commitment is read, so
455
- // until then it is incoming, as a payment's change is between its relay and the next sync
456
534
  for (const n of this.#ctx.state.notes.filter(
457
- (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),
458
536
  )) {
459
- const stale =
460
- n.receipt?.kind === 'merged' && n.atNs !== null && Date.now() - Number(n.atNs / 1_000_000n) > DAY_MS;
461
- 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' });
462
538
  }
463
539
  for (const c of coins.filter((c) => c.token === token)) {
464
540
  if (c.state === 'spendable') b.spendable += c.amount;
@@ -470,13 +546,14 @@ export class Wallet {
470
546
  }
471
547
 
472
548
  /** Every unspent note and deposit on chain, and where each stands, as of opening or the last sync. A note announced
473
- * 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. */
474
550
  coins(): CoinInfo[] {
475
551
  const size = (chainId: bigint) => this.#status.get(chainId)?.latestRoot?.size ?? 0;
476
552
  const unproved = (chainId: bigint) => (this.#status.get(chainId)?.halted ? 'roots-halted' : 'awaiting-root');
477
553
  const spending = new Set(this.#ctx.state.ops.flatMap((o) => inputsOf(o).map((c) => `${o.chainId}:${c.index}`)));
478
- // spent by a JoinSplit in a block, which waits only to be final: gone, as activity() counts it (a reorg that drops
479
- // 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)
480
557
  const landed = new Set(
481
558
  this.#ctx.state.ops.flatMap((o) =>
482
559
  o.kind !== 'deposit' && o.data.step?.stage === 'relayed'
@@ -487,7 +564,7 @@ export class Wallet {
487
564
  const out: CoinInfo[] = [];
488
565
  // a zero-value change carries its payment's receipt, and is no coin
489
566
  for (const n of this.#ctx.state.notes.filter((n) => n.value > 0n)) {
490
- for (const f of n.found.filter((f) => !f.spent)) {
567
+ for (const f of n.found.filter(unspent)) {
491
568
  const id = `${n.chainId}:${f.index}`;
492
569
  // a 4-byte tag alone matches someone else's now and then: a coin once its commitment is read
493
570
  if (!f.confirmed || landed.has(id)) continue;
@@ -505,7 +582,7 @@ export class Wallet {
505
582
  }
506
583
  for (const d of this.#ctx.state.deposits) {
507
584
  // a coin once its commitment is read, which names the amount
508
- 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)) {
509
586
  const id = `${d.chainId}:${f.index}`;
510
587
  if (landed.has(id)) continue;
511
588
  const state =
@@ -755,7 +832,7 @@ export class Wallet {
755
832
  /** Refunds deposit `id` to its sender without the ASP, the attester or a proved root: for a deposit the ASP
756
833
  * rejected, or when those services stop. The account's wallet sends the transactions and pays their gas. */
757
834
  rageExit(id: string): Promise<Hex[]> {
758
- return this.#exclusive(async () => {
835
+ return this.#seeded(async () => {
759
836
  const [chain, index] = id.split(':');
760
837
  const record =
761
838
  this.#ctx.state.deposits.find((d) => `${d.chainId}` === chain && d.found.some((f) => `${f.index}` === index)) ??
@@ -861,22 +938,26 @@ export class Wallet {
861
938
  * With `wrapped`, it pays from the account's zERC20 itself, as a withdrawal pays out, rather than its underlying
862
939
  * asset: a plain transfer into the burn address, with no wrap and no approval.
863
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
+ *
864
944
  * Returns once the wrap is sent: the operation is done once it is mined, which `wait()` or the next sync finds.
865
945
  * `onStep` hears what it waits on meanwhile, for an app to show. */
866
- deposit(opts: {
946
+ async deposit(opts: {
867
947
  token: Token;
868
948
  amount: bigint;
869
949
  to?: Address;
870
950
  wrapped?: boolean;
951
+ from?: WalletClient;
871
952
  onStep?: (step: DepositStep) => void;
872
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;
873
956
  const step = opts.onStep ?? (() => {});
874
957
  if (this.#busy > 0) step('queued');
875
958
  return this.#exclusive(async () => {
876
959
  const { token, amount } = opts;
877
- if (amount <= 0n) fail('invalid', 'the amount must be positive');
878
- const wc = this.#ctx.walletClient;
879
- const account = wc.account!;
960
+ const account = payer ?? this.#ctx.account;
880
961
  const client = chainCtx(this.#ctx, token.chainId).chain.client;
881
962
  const { wrapped } = opts;
882
963
  const native = !wrapped && token.underlying.toLowerCase() === NATIVE.toLowerCase();
@@ -893,7 +974,7 @@ export class Wallet {
893
974
  const of = wrapped ? token.symbol : `${token.symbol}'s underlying asset`;
894
975
  fail(
895
976
  'insufficient-funds',
896
- `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)}`,
897
978
  );
898
979
  }
899
980
  const address = opts.to
@@ -907,8 +988,7 @@ export class Wallet {
907
988
  );
908
989
  await this.#ctx.save();
909
990
  try {
910
- if ((await wc.getChainId()) !== Number(token.chainId))
911
- 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);
912
992
  if (!native && !wrapped) {
913
993
  const allowance = await client.readContract({
914
994
  address: token.underlying,
@@ -1002,8 +1082,8 @@ export class Wallet {
1002
1082
 
1003
1083
  /** An address that takes `token`'s underlying asset from anywhere (an exchange withdrawal, another wallet) and
1004
1084
  * deposits it privately, less `fee` (in the underlying's base units) to the deposit relayer each time: what arrives
1005
- * must exceed it. Watched for 7 days by the deposit relayer; refunds go back to this account. `onStep` hears what it
1006
- * waits on (`queued`, `burn-address`). */
1085
+ * must exceed it. A wallet opened with a `referrer` also pays it its `bps` of the rest. Watched for 7 days by the
1086
+ * deposit relayer; refunds go back to this account. `onStep` hears what it waits on (`queued`, `burn-address`). */
1007
1087
  depositAddress(
1008
1088
  token: Token,
1009
1089
  opts: { onStep?: (step: DepositStep) => void } = {},
@@ -1013,7 +1093,13 @@ export class Wallet {
1013
1093
  return this.#exclusive(async () => {
1014
1094
  const c = chainCtx(this.#ctx, token.chainId);
1015
1095
  const record = await this.#burnAddress(token, null, () => step('burn-address'));
1016
- const { depositAddress, fee } = await registerDeposit(c.chain, token.address, record.address, this.address);
1096
+ const { depositAddress, fee } = await registerDeposit(
1097
+ c.chain,
1098
+ token.address,
1099
+ record.address,
1100
+ this.address,
1101
+ this.#referrer,
1102
+ );
1017
1103
  record.depositAddress = depositAddress;
1018
1104
  // for the wallet's other devices, to show what comes through it: written by the next sync
1019
1105
  const burnAddress = record.address.toLowerCase() as Hex;
@@ -1037,19 +1123,21 @@ export class Wallet {
1037
1123
  * nothing; it names neither this address nor its public key. Anyone who gets the code can pay it, and see what its
1038
1124
  * other payers paid privately: give each payer its own. It is checksummed text (bech32m, `zerc201…`). */
1039
1125
  receiveCode(token: Token): Promise<string> {
1040
- return this.#exclusive(async () => {
1041
- 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 () => {
1042
1131
  const { burnIndex, address } = await this.#burnAddress(token);
1132
+ if (!owner(ctx, code(burnIndex))) await this.#seed(); // calls queued before took the next ones
1043
1133
  return encodeCode({
1044
1134
  chainId: token.chainId,
1045
1135
  tokenId: token.tokenId,
1046
1136
  burnAddress: hexToBytes(address),
1047
- publicKey: hexToBytes(
1048
- spendKey(ctx, { chainId: token.chainId, tokenId: token.tokenId, index: burnIndex }).publicKey,
1049
- ),
1137
+ publicKey: hexToBytes(owner(ctx, code(burnIndex))!.publicKey),
1050
1138
  key: codeKey(ctx.view.seed, token.chainId, token.tokenId, burnIndex),
1051
1139
  });
1052
- });
1140
+ }, !derived);
1053
1141
  }
1054
1142
 
1055
1143
  /** What a send or withdrawal of `amount` would cost now, or a consolidation of what the wallet holds. A send or
@@ -1134,10 +1222,11 @@ export class Wallet {
1134
1222
  * what is spendable now, or it fails here: `needs-consolidation`, `funds-settling` or `insufficient-funds`.
1135
1223
  * `amount` is what the recipient gets, and `maxFee` caps what it pays on top, `fees({ kind: 'send' }).fee`: the
1136
1224
  * relayer names its fee, so a quote the user saw is the cap. */
1137
- send(opts: { to: string; token: Token; amount: bigint; maxFee: bigint }): Promise<Operation> {
1138
- return this.#exclusive(async () => {
1139
- const { token, amount } = opts;
1140
- 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 () => {
1141
1230
  let to: Destination;
1142
1231
  if (isAddress(opts.to)) {
1143
1232
  const recipient = await lookupRecipient(this.#ctx.storage, hexToBytes(opts.to));
@@ -1169,7 +1258,7 @@ export class Wallet {
1169
1258
  * touched; one still settling is waited for. `maxFee` caps the fees of every merge together,
1170
1259
  * `fees({ kind: 'consolidate' }).fee`. */
1171
1260
  consolidate(opts: { token: Token; coins?: string[]; maxFee: bigint }): Promise<Operation> {
1172
- return this.#exclusive(async () => {
1261
+ return this.#seeded(async () => {
1173
1262
  if (!this.#ctx.network.mergeProver) fail('invalid', 'this network has no merge prover');
1174
1263
  const to: Destination = { kind: 'self' };
1175
1264
  const data: SpendData = { to, maxFee: opts.maxFee, step: null, paid: 0n };
@@ -1196,10 +1285,16 @@ export class Wallet {
1196
1285
  /** Withdraws `amount` publicly to `to`, as zERC20 or with `unwrap` the underlying asset itself, so `to` needs no gas
1197
1286
  * to unwrap. One JoinSplit, as `send`, and as there `to` gets all of `amount` and `maxFee` caps what is paid on top
1198
1287
  * (`fees({ kind: 'withdraw' }).fee`). */
1199
- withdraw(opts: { to: Address; token: Token; amount: bigint; maxFee: bigint; unwrap?: boolean }): Promise<Operation> {
1200
- return this.#exclusive(async () => {
1201
- if (!isAddress(opts.to)) fail('invalid', `${String(opts.to)} is not an address`);
1202
- 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 () => {
1203
1298
  const to: Destination = { kind: 'public', address: opts.to, unwrap: opts.unwrap };
1204
1299
  return this.#newPayment('withdraw', opts.token, opts.amount, { to, maxFee: opts.maxFee, step: null, paid: 0n });
1205
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];