@zerc20/wallet-sdk 0.4.0 → 0.5.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 (60) hide show
  1. package/README.md +1 -1
  2. package/dist/accountId.d.ts +5 -0
  3. package/dist/accountId.js +19 -0
  4. package/dist/accountId.js.map +1 -0
  5. package/dist/chain.d.ts +85 -9
  6. package/dist/chain.js +60 -33
  7. package/dist/chain.js.map +1 -1
  8. package/dist/context.d.ts +8 -2
  9. package/dist/context.js +15 -2
  10. package/dist/context.js.map +1 -1
  11. package/dist/core.d.ts +35 -5
  12. package/dist/core.js +95 -23
  13. package/dist/core.js.map +1 -1
  14. package/dist/index.d.ts +4 -2
  15. package/dist/index.js +4 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/pos.d.ts +7 -1
  18. package/dist/pos.js +213 -55
  19. package/dist/pos.js.map +1 -1
  20. package/dist/rageExit.d.ts +27 -4
  21. package/dist/rageExit.js +160 -49
  22. package/dist/rageExit.js.map +1 -1
  23. package/dist/seed.d.ts +4 -0
  24. package/dist/seed.js +13 -1
  25. package/dist/seed.js.map +1 -1
  26. package/dist/servers.d.ts +2 -0
  27. package/dist/servers.js +1 -0
  28. package/dist/servers.js.map +1 -1
  29. package/dist/services.d.ts +11 -4
  30. package/dist/services.js +26 -7
  31. package/dist/services.js.map +1 -1
  32. package/dist/spend.d.ts +30 -9
  33. package/dist/spend.js +46 -17
  34. package/dist/spend.js.map +1 -1
  35. package/dist/state.d.ts +21 -0
  36. package/dist/state.js.map +1 -1
  37. package/dist/sync.d.ts +10 -5
  38. package/dist/sync.js +49 -18
  39. package/dist/sync.js.map +1 -1
  40. package/dist/wallet.d.ts +92 -14
  41. package/dist/wallet.js +191 -56
  42. package/dist/wallet.js.map +1 -1
  43. package/package.json +2 -2
  44. package/src/accountId.ts +22 -0
  45. package/src/chain.ts +95 -49
  46. package/src/context.ts +27 -3
  47. package/src/core.ts +109 -24
  48. package/src/index.ts +15 -2
  49. package/src/pos.ts +240 -61
  50. package/src/rageExit.ts +237 -61
  51. package/src/seed.ts +24 -1
  52. package/src/servers.ts +4 -1
  53. package/src/services.ts +28 -15
  54. package/src/spend.ts +58 -23
  55. package/src/state.ts +25 -1
  56. package/src/sync.ts +54 -19
  57. package/src/wallet.ts +246 -72
  58. package/wasm/zerc20_wallet_core.d.ts +1 -1
  59. package/wasm/zerc20_wallet_core.js +1 -1
  60. package/wasm/zerc20_wallet_core_bg.wasm +0 -0
package/src/wallet.ts CHANGED
@@ -27,10 +27,12 @@ import {
27
27
  getAddress,
28
28
  hexToBytes,
29
29
  isAddress,
30
+ isAddressEqual,
30
31
  numberToHex,
31
32
  zeroAddress,
32
33
  } from 'viem';
33
34
  import { toAccount } from 'viem/accounts';
35
+ import { idAccount } from './accountId.js';
34
36
  import { Chain, MAX_REFERRAL_BPS, NATIVE, type Referrer, type Token, erc20Abi, tokenAbi } from './chain.js';
35
37
  import {
36
38
  type ChainCtx,
@@ -39,6 +41,7 @@ import {
39
41
  chainCtx,
40
42
  chainState,
41
43
  owner,
44
+ refundSigner,
42
45
  switched,
43
46
  walletOn,
44
47
  walletSees,
@@ -47,14 +50,15 @@ import { burnAddressFor, core, initCore } from './core.js';
47
50
  import { type ErrorCode, WalletError, fail, guard } from './errors.js';
48
51
  import { type Network, checkNetwork } from './network.js';
49
52
  import { Asp, Indexer, type IndexerStatus, PAGE } from './servers.js';
50
- import { rageExit } from './rageExit.js';
51
- import { registerDeposit, relayerFees } from './services.js';
53
+ import { type Route, flushDepositAddress, rageExit, refundDepositAddress } from './rageExit.js';
54
+ import { registerDeposit, relayerFees, rewatchDeposit } from './services.js';
52
55
  import {
53
56
  type Destination,
54
57
  type MergeStep,
55
58
  type SpendData,
56
59
  type Step,
57
60
  advanceSpend,
61
+ bpsOf,
58
62
  chosenSettling,
59
63
  coins,
60
64
  grossed,
@@ -70,7 +74,9 @@ import {
70
74
  withdrawalOf,
71
75
  } from './spend.js';
72
76
  import {
77
+ type DepositAddressReceipt,
73
78
  type DepositOperation,
79
+ type DepositRecord,
74
80
  type OperationRecord,
75
81
  type WalletState,
76
82
  emptyWalletState,
@@ -170,7 +176,8 @@ export interface Activity {
170
176
  * paid none. */
171
177
  fee: bigint | null;
172
178
  /** The other side, where known: a payer's claimed address (unverified), where a payment or withdrawal went, or whom a
173
- * returned deposit went back to. */
179
+ * returned deposit went back to. A payment's other side, `sent`'s or that of a `received` with no `burnAddress`, is a
180
+ * zERC20 account, which an app shows by its zERC20 ID (`accountId`); any other is a public address. */
174
181
  counterparty: Address | null;
175
182
  /** What the counterparty alone does not say: the receive code a payment went to (it has no counterparty), or the
176
183
  * deposit address a deposit came through (its counterparty, which forwarded what was sent to it). */
@@ -204,8 +211,13 @@ export interface CoinInfo {
204
211
  state: 'spendable' | Exclude<PendingReason, 'incoming'> | 'spending' | 'blocked';
205
212
  /** A note's payer (as claimed, unverified) or a deposit's sender, where known. */
206
213
  from: Address | null;
214
+ /** The deposit address of this wallet's that a deposit came through (its `from`), as `Activity.via` names it: its
215
+ * `rageExit` goes where `to` says. */
216
+ via: { depositAddress: Address } | null;
207
217
  /** For a deposit in `screening`, when the ASP should decide it: an estimate, null until there is one. */
208
218
  screenedBy: Date | null;
219
+ /** When it landed: its block's time, null until read. */
220
+ at: Date | null;
209
221
  }
210
222
 
211
223
  export interface DepositInfo {
@@ -221,12 +233,25 @@ export interface DepositInfo {
221
233
  state: 'screening' | 'approved' | 'rejected' | 'spent';
222
234
  }
223
235
 
236
+ export interface DepositAddressInfo {
237
+ address: Address;
238
+ token: Token;
239
+ /** What the relayer takes out of each payment through it. Null where its receipt in storage does not say what it was
240
+ * registered with: `flushDepositAddress` needs that, so only the relayer flushes such an address. */
241
+ fee: bigint | null;
242
+ /** When it was handed out, or watched again (`rewatchDepositAddress`), from which the deposit relayer watches it for
243
+ * 7 days: when storage took its receipt. */
244
+ at: Date;
245
+ }
246
+
224
247
  /** A send's or withdrawal's recipient gets all of its amount, and the private balance pays `fee` on top. */
225
248
  export interface FeeQuote {
226
249
  /** To the relayer. */
227
250
  relayer: bigint;
228
251
  /** The protocol's cut of the public withdrawal: a withdrawal's, or the one that pays the relayer. */
229
252
  protocol: bigint;
253
+ /** `protocol` as a rate of that withdrawal, in basis points (35 is 0.35%): lower in tiers as the withdrawal grows. */
254
+ protocolBps: number;
230
255
  /** `relayer + protocol`: the `maxFee` this quote passes. */
231
256
  fee: bigint;
232
257
  /** Transactions needed: one JoinSplit for a send or a withdrawal, a consolidation's merges (each pays the relayer's
@@ -234,6 +259,16 @@ export interface FeeQuote {
234
259
  steps: number;
235
260
  }
236
261
 
262
+ /** The most a send or withdrawal can pay now, and what its fees keep back of what is spendable. */
263
+ export interface MaxAmount {
264
+ /** What the recipient would get: 0 where the fees take everything. */
265
+ amount: bigint;
266
+ /** The fees: the payment's, and those of the merges it needs first. Where they take everything, what they would be. */
267
+ fee: bigint;
268
+ /** Merges first, when one JoinSplit cannot spend all of it: each pays the relayer again. */
269
+ merges: number;
270
+ }
271
+
237
272
  export type OperationKind = OperationRecord['kind'];
238
273
  export type OperationStatus = OperationRecord['status'];
239
274
  export type CoinState = CoinInfo['state'];
@@ -256,7 +291,8 @@ export interface Operation {
256
291
  /** How far a send, withdrawal or consolidation round has come, for an app to show; null for a deposit, or between
257
292
  * rounds. It moves during a sync: read it while one runs. */
258
293
  readonly stage: Step['stage'] | MergeStep['stage'] | null;
259
- /** Whom a deposit pays, when it goes into their burn address (`deposit({ to })`); null otherwise. */
294
+ /** Whom a deposit pays, when `deposit({ to })` named an address; null otherwise. One to a receive code shows its code
295
+ * as `via` in `activity()`. */
260
296
  readonly to: Address | null;
261
297
  /** Syncs until the operation is done; throws if it fails. */
262
298
  wait(): Promise<void>;
@@ -359,6 +395,7 @@ export class Wallet {
359
395
  network: opts.network,
360
396
  seed,
361
397
  owners: new Map(),
398
+ refundSigners: new Map(),
362
399
  address,
363
400
  account,
364
401
  walletClient: opts.walletClient ?? null,
@@ -394,6 +431,7 @@ export class Wallet {
394
431
  network: opts.network,
395
432
  seed: null,
396
433
  owners: new Map([['', owner]]),
434
+ refundSigners: new Map(),
397
435
  address,
398
436
  // signs nothing: every call that would send is refused first
399
437
  account: toAccount(address),
@@ -539,14 +577,8 @@ export class Wallet {
539
577
  async #catchUp(): Promise<void> {
540
578
  const errors = await syncAll(this.#ctx);
541
579
  errors.push(...(await this.#readRoots()));
542
- const listed = await Promise.all(
543
- this.#ctx.chains.map((c) =>
544
- c.chain
545
- .tokens()
546
- .catch((e: unknown) => (errors.push(e), this.#tokens.filter((t) => t.chainId === c.config.chainId))),
547
- ),
548
- );
549
- this.#tokens = listed.flat();
580
+ // as syncAll read them: a chain it could not read keeps those read before
581
+ this.#tokens = this.#ctx.chains.flatMap((c) => c.chain.known);
550
582
  for (const op of this.#ctx.state.ops.filter((o) => o.status === 'pending')) {
551
583
  try {
552
584
  if (op.kind === 'send' || op.kind === 'withdraw' || op.kind === 'consolidate')
@@ -670,7 +702,9 @@ export class Wallet {
670
702
  amount: n.value,
671
703
  state,
672
704
  from: n.peer,
705
+ via: null,
673
706
  screenedBy: null,
707
+ at: f.atMs === undefined ? null : new Date(f.atMs),
674
708
  });
675
709
  }
676
710
  }
@@ -697,7 +731,9 @@ export class Wallet {
697
731
  amount: f.amount!,
698
732
  state,
699
733
  from: f.sender,
734
+ via: this.#via(d, f.sender),
700
735
  screenedBy,
736
+ at: f.atMs === undefined ? null : new Date(f.atMs),
701
737
  });
702
738
  }
703
739
  }
@@ -771,27 +807,17 @@ export class Wallet {
771
807
  }
772
808
  }
773
809
  for (const d of s.deposits) {
774
- // registered on this device, or on another, which says so in storage
775
- const registered =
776
- d.depositAddress ??
777
- s.receipts.flatMap((r) =>
778
- r.kind === 'deposit-address' && r.chainId === d.chainId && r.burnAddress === d.address.toLowerCase()
779
- ? [r.depositAddress]
780
- : [],
781
- )[0];
782
810
  // on chain once found; shown once its commitment is read, which names the amount
783
811
  for (const f of d.found.filter((f) => f.amount !== null)) {
784
- const forwarded = registered && f.sender?.toLowerCase() === registered.toLowerCase();
785
812
  // into a burn address a payer chose: theirs to this wallet, as a payment is
786
813
  const kind = d.managed ? 'sale' : d.payer ? 'received' : 'deposited';
787
- const via = forwarded ? { depositAddress: registered } : null;
788
814
  const entry = {
789
815
  ...at(kind, d.chainId, f, d.createdMs),
790
816
  token: this.#token(d.chainId, f.tokenId),
791
817
  amount: f.amount!,
792
818
  burnAddress: d.address,
793
819
  };
794
- out.push({ ...entry, fee: null, counterparty: f.sender, via, txHash: null });
820
+ out.push({ ...entry, fee: null, counterparty: f.sender, via: this.#via(d, f.sender), txHash: null });
795
821
  }
796
822
  }
797
823
  // a deposit that paid someone (`deposit({ to })`), once its transfer is found
@@ -836,8 +862,16 @@ export class Wallet {
836
862
  x.address === op.data.burnAddress.toLowerCase() &&
837
863
  x.found.some((f) => f.amount !== null),
838
864
  );
839
- if (!op.data.to || !op.data.txHash || op.status === 'pending' || found) continue;
840
- out.push({ ...base, kind: 'sent', fee: null, counterparty: op.data.to, via: null, txHash: op.data.txHash });
865
+ const { to, code, txHash } = op.data;
866
+ if ((!to && !code) || !txHash || op.status === 'pending' || found) continue;
867
+ out.push({
868
+ ...base,
869
+ kind: 'sent',
870
+ fee: null,
871
+ counterparty: to ?? null,
872
+ via: code ? { code } : null,
873
+ txHash,
874
+ });
841
875
  continue;
842
876
  }
843
877
  // under way (in operations()), or shown by its change
@@ -856,6 +890,21 @@ export class Wallet {
856
890
  return out.sort((a, b) => (b.at?.getTime() ?? 0) - (a.at?.getTime() ?? 0));
857
891
  }
858
892
 
893
+ /** The deposit address `d` registered (here, or on another device, which says so in storage) if `sender` is it: what
894
+ * a deposit from `sender` came through. */
895
+ #via(d: DepositRecord, sender: Hex | null): { depositAddress: Address } | null {
896
+ const registered =
897
+ d.depositAddress ??
898
+ this.#ctx.state.receipts.flatMap((r) =>
899
+ r.kind === 'deposit-address' && r.chainId === d.chainId && r.burnAddress === d.address.toLowerCase()
900
+ ? [r.depositAddress]
901
+ : [],
902
+ )[0];
903
+ return registered && sender && isAddressEqual(sender, registered)
904
+ ? { depositAddress: getAddress(registered) }
905
+ : null;
906
+ }
907
+
859
908
  /** The text of a receive code this wallet paid, by its burn address, from storage's record of paying it. */
860
909
  #paidCode(chainId: bigint, burnAddress: Hex): string | null {
861
910
  for (const c of this.#ctx.state.storage?.outgoing.lanes ?? []) {
@@ -900,6 +949,31 @@ export class Wallet {
900
949
  );
901
950
  }
902
951
 
952
+ /** The deposit addresses this wallet handed out (`depositAddress`), on any of its devices, newest first: for an app to
953
+ * show each while what is sent to it is on its way, which `activity()` shows once deposited. Each is dated, and its
954
+ * fee told, by its receipt in storage, alike on every device. */
955
+ depositAddresses(): DepositAddressInfo[] {
956
+ return this.#ctx.state.receipts
957
+ .flatMap((r) => {
958
+ if (r.kind !== 'deposit-address') return [];
959
+ const token = this.#token(r.chainId, r.tokenId);
960
+ return [{ address: getAddress(r.depositAddress), token, fee: r.args?.fee ?? null, at: new Date(r.atMs) }];
961
+ })
962
+ .sort((a, b) => b.at.getTime() - a.at.getTime());
963
+ }
964
+
965
+ /** What `address` is to this wallet on `chainId`: one of its own burn addresses (`deposit`), one it paid someone
966
+ * into (`payment`, `deposit({ to })` to an address), or neither (null), as is a receive code's that it paid. On chain
967
+ * either is a plain transfer to an address that names no one: this tells an app listing the account's public
968
+ * transfers which they were. Both are recorded before the transfer is sent. */
969
+ burnAddressOf(chainId: bigint, address: Address): { kind: 'deposit' } | { kind: 'payment'; to: Address } | null {
970
+ const s = this.#ctx.state;
971
+ const a = address.toLowerCase();
972
+ if (s.deposits.some((d) => d.chainId === chainId && d.address.toLowerCase() === a)) return { kind: 'deposit' };
973
+ const paid = s.sent.find((x) => x.chainId === chainId && x.address.toLowerCase() === a);
974
+ return paid ? { kind: 'payment', to: paid.to } : null;
975
+ }
976
+
903
977
  /** The payments to the burn addresses this wallet's points of sale handed out. Spent ones are, normally, in a
904
978
  * merged note. */
905
979
  payments(): DepositInfo[] {
@@ -925,8 +999,12 @@ export class Wallet {
925
999
  }
926
1000
 
927
1001
  /** Refunds deposit `id` to its sender without the ASP, the attester or a proved root: for a deposit the ASP
928
- * rejected, or when those services stop. The account's wallet sends the transactions and pays their gas. */
929
- rageExit(id: string): Promise<Hex[]> {
1002
+ * rejected, or when those services stop. The account's wallet sends the transactions and pays their gas, or with
1003
+ * `from` another wallet of the user's does, as `deposit({ from })` pays. One through a deposit address of this
1004
+ * wallet's (`CoinInfo.via`) goes to `to`, by default `from`'s account, else this account; any other goes back to its
1005
+ * sender, and one through a point of sale's deposit address to the refund address its payer named. */
1006
+ rageExit(id: string, opts: { from?: WalletClient; to?: Address } = {}): Promise<Hex[]> {
1007
+ const route = this.#route(opts);
930
1008
  return this.#seeded(async () => {
931
1009
  const [chain, index] = id.split(':');
932
1010
  const record =
@@ -937,10 +1015,39 @@ export class Wallet {
937
1015
  (o) => `${o.chainId}` === chain && inputsOf(o).some((c) => `${c.index}` === index),
938
1016
  );
939
1017
  if (op) fail('invalid', `deposit ${id} is an input of operation ${op.id}, which is in flight`);
940
- return rageExit(this.#ctx, record, Number(index));
1018
+ return rageExit(this.#ctx, record, Number(index), route);
941
1019
  });
942
1020
  }
943
1021
 
1022
+ /** Sends what deposit address `address` of this wallet's holds of `asset` (NATIVE for the chain's own currency) to
1023
+ * `to`: another token sent to it, which it cannot wrap. What it can wrap is wrapped instead, as a payment to it is.
1024
+ * It deploys on its first flush, and refunds nothing before. `from` and `to` as for `rageExit`. */
1025
+ refundDepositAddress(
1026
+ address: Address,
1027
+ asset: Address,
1028
+ opts: { from?: WalletClient; to?: Address } = {},
1029
+ ): Promise<Hex> {
1030
+ if (!isAddress(asset, { strict: false }) || asset.toLowerCase() === zeroAddress)
1031
+ fail('invalid', `${asset} is not an asset's address`);
1032
+ const route = this.#route(opts);
1033
+ return this.#seeded(async () => {
1034
+ const record =
1035
+ this.#ctx.state.deposits.find((d) => !d.managed && this.#via(d, address)) ??
1036
+ fail('invalid', `${address} is not a deposit address of this wallet's`);
1037
+ return refundDepositAddress(this.#ctx, record, getAddress(address), getAddress(asset), route);
1038
+ });
1039
+ }
1040
+
1041
+ /** Who sends a refund and where it goes (`rageExit`), checked before the seed is asked for. */
1042
+ #route(opts: { from?: WalletClient; to?: Address }): Route {
1043
+ const from = opts.from ?? null;
1044
+ if (from && !from.account) fail('invalid', 'the wallet client has no account');
1045
+ const to = opts.to ?? from?.account?.address ?? this.address;
1046
+ if (!isAddress(to, { strict: false }) || to.toLowerCase() === zeroAddress)
1047
+ fail('invalid', `${to} is not an address to refund to`);
1048
+ return { from, to: getAddress(to) };
1049
+ }
1050
+
944
1051
  operations(): Operation[] {
945
1052
  return this.#ctx.state.ops.map((op) => this.#operation(op));
946
1053
  }
@@ -1010,7 +1117,9 @@ export class Wallet {
1010
1117
  let record = unusedBurn(ctx, token.chainId, token.tokenId);
1011
1118
  if (!record) {
1012
1119
  recording();
1013
- record = await recordBurn(ctx, token.chainId, token.tokenId, await tokenIds(ctx), this.#closed.signal);
1120
+ record = await recordBurn(ctx, token.chainId, token.tokenId, await tokenIds(ctx), {
1121
+ signal: this.#closed.signal,
1122
+ });
1014
1123
  }
1015
1124
  const next = chainState(ctx, token.chainId).burnNext;
1016
1125
  // recording may have read another device's later ones
@@ -1025,10 +1134,12 @@ export class Wallet {
1025
1134
  * burn address (approving it first if needed). The deposit is spendable once the ASP has screened it. More than the
1026
1135
  * account holds fails at once (`insufficient-funds`), before anything is asked of the wallet.
1027
1136
  *
1028
- * With `to`, an address that registered, it pays them instead, privately: into a fresh burn address of theirs,
1029
- * under the public key they registered, told to them in storage first as a note is (`planDeposit`). They
1030
- * receive it as a deposit of their own, screened like any; the burn address names no one. It shows in `activity()`
1031
- * as sent. Finding the address takes about 2^16 hashes (`burn-address`).
1137
+ * With `to`, an account that registered, by its address or its zERC20 ID, it pays them instead, privately: into a
1138
+ * fresh burn address of theirs, under the public key they registered, told to them in storage first as a note is
1139
+ * (`planDeposit`). They receive it as a deposit of their own, screened like any; the burn address names no one. It
1140
+ * shows in `activity()` as sent. Finding the address takes about 2^16 hashes (`burn-address`). Or `to` is a receive
1141
+ * code (`receiveCode`), paid into its burn address, which its owner already watches: storage is told nothing, so
1142
+ * only this device's `activity()` shows it as sent.
1032
1143
  *
1033
1144
  * With `wrapped`, it pays from the account's zERC20 itself, as a withdrawal pays out, rather than its underlying
1034
1145
  * asset: a plain transfer into the burn address, with no wrap and no approval.
@@ -1041,7 +1152,7 @@ export class Wallet {
1041
1152
  async deposit(opts: {
1042
1153
  token: Token;
1043
1154
  amount: bigint;
1044
- to?: Address;
1155
+ to?: string;
1045
1156
  wrapped?: boolean;
1046
1157
  from?: WalletClient;
1047
1158
  onStep?: (step: DepositStep) => void;
@@ -1049,6 +1160,7 @@ export class Wallet {
1049
1160
  this.#writable();
1050
1161
  if (opts.amount <= 0n) fail('invalid', 'the amount must be positive');
1051
1162
  const payer = opts.from ? (opts.from.account ?? fail('invalid', 'the wallet client has no account')) : null;
1163
+ const to = opts.to === undefined ? null : recipientOf(opts.to, opts.token);
1052
1164
  const step = opts.onStep ?? (() => {});
1053
1165
  if (this.#busy > 0) step('queued');
1054
1166
  return this.#exclusive(async () => {
@@ -1073,14 +1185,21 @@ export class Wallet {
1073
1185
  `${payer ? 'the wallet' : 'the account'} holds ${formatUnits(held, token.decimals)} of ${of}, less than ${formatUnits(amount, token.decimals)}`,
1074
1186
  );
1075
1187
  }
1076
- const address = opts.to
1077
- ? await this.#burnFor(token, amount, opts.to, () => step('burn-address'))
1078
- : (await this.#burnAddress(token, null, () => step('burn-address'))).address;
1188
+ const address =
1189
+ to?.kind === 'code'
1190
+ ? to.burnAddress
1191
+ : to
1192
+ ? await this.#burnFor(token, amount, to.address, () => step('burn-address'))
1193
+ : (await this.#burnAddress(token, null, () => step('burn-address'))).address;
1079
1194
  const op = this.#newOp(
1080
1195
  'deposit',
1081
1196
  token,
1082
1197
  amount,
1083
- opts.to ? { burnAddress: address, to: opts.to } : { burnAddress: address },
1198
+ to?.kind === 'code'
1199
+ ? { burnAddress: address, code: to.code }
1200
+ : to
1201
+ ? { burnAddress: address, to: to.address }
1202
+ : { burnAddress: address },
1084
1203
  );
1085
1204
  await this.#ctx.save();
1086
1205
  try {
@@ -1153,11 +1272,7 @@ export class Wallet {
1153
1272
  async #burnFor(token: Token, amount: bigint, to: Address, searching: () => void): Promise<Hex> {
1154
1273
  const ctx = this.#ctx;
1155
1274
  const recipient = await lookupRecipient(ctx.storage, hexToBytes(to));
1156
- if (!recipient)
1157
- fail(
1158
- 'recipient-not-registered',
1159
- `${to} cannot be paid privately: withdraw to it publicly, or ask for a receive code`,
1160
- );
1275
+ if (!recipient) fail('recipient-not-registered', `${to} never opened a zERC20 wallet: ask for a receive code`);
1161
1276
  searching();
1162
1277
  const burn = await burnAddressFor(token.chainId, bytesToHex(recipient!.publicKey), this.#closed.signal);
1163
1278
  // with the address, so that every device of this wallet finds the transfer it then sends
@@ -1179,26 +1294,35 @@ export class Wallet {
1179
1294
  /** An address that takes `token`'s underlying asset from anywhere (an exchange withdrawal, another wallet) and
1180
1295
  * deposits it privately, less `fee` (in the underlying's base units) to the deposit relayer each time: what arrives
1181
1296
  * must exceed it. A wallet opened with a `referrer` also pays it its `bps` of the rest. Watched for 7 days by the
1182
- * deposit relayer; refunds go back to this account. `onStep` hears what it waits on (`queued`, `burn-address`). */
1297
+ * deposit relayer. It names no account: its refunds go where a key of its own signs for (`rageExit`'s `to`,
1298
+ * `refundDepositAddress`). `depositAddresses()` lists it. `onStep` hears what it waits on (`queued`,
1299
+ * `burn-address`). */
1183
1300
  depositAddress(
1184
1301
  token: Token,
1185
1302
  opts: { onStep?: (step: DepositStep) => void } = {},
1186
1303
  ): Promise<{ address: Address; fee: bigint }> {
1187
1304
  const step = opts.onStep ?? (() => {});
1305
+ const ctx = this.#ctx;
1306
+ const code = (index: number) => ({ chainId: token.chainId, tokenId: token.tokenId, index });
1307
+ // the seed is asked for only past the refund signers derived ahead
1308
+ const derived = !!refundSigner(ctx, code(nextBurn(ctx, token.chainId, token.tokenId)));
1188
1309
  if (this.#busy > 0) step('queued');
1189
- return this.#exclusive(async () => {
1190
- this.#writable();
1191
- const c = chainCtx(this.#ctx, token.chainId);
1310
+ return this.#seeded(async () => {
1311
+ const c = chainCtx(ctx, token.chainId);
1192
1312
  const record = await this.#burnAddress(token, null, () => step('burn-address'));
1193
- const { depositAddress, fee } = await registerDeposit(
1313
+ if (!refundSigner(ctx, code(record.burnIndex))) await this.#seed(); // calls queued before took the next ones
1314
+ const signer = refundSigner(ctx, code(record.burnIndex))!;
1315
+ const referrer = this.#referrer ?? { address: zeroAddress, bps: 0 };
1316
+ const { depositAddress, feeRecipient, fee } = await registerDeposit(
1194
1317
  c.chain,
1195
1318
  token.address,
1196
1319
  record.address,
1197
- this.address,
1198
- this.#referrer,
1320
+ { signer },
1321
+ referrer,
1199
1322
  );
1200
1323
  record.depositAddress = depositAddress;
1201
- // for the wallet's other devices, to show what comes through it: written by the next sync
1324
+ // for the wallet's other devices, to show what comes through it and what it takes, and for a flush without the
1325
+ // relayer: written by the next sync
1202
1326
  const burnAddress = record.address.toLowerCase() as Hex;
1203
1327
  this.#ctx.state.receipts.push({
1204
1328
  kind: 'deposit-address',
@@ -1206,12 +1330,48 @@ export class Wallet {
1206
1330
  tokenId: token.tokenId,
1207
1331
  burnAddress,
1208
1332
  depositAddress,
1333
+ args: { fee, feeRecipient, refundSigner: signer, referrer: referrer.address, referralBps: referrer.bps },
1209
1334
  atMs: Date.now(),
1210
1335
  unwritten: true,
1211
1336
  });
1212
1337
  await this.#ctx.save();
1213
1338
  return { address: depositAddress, fee };
1214
- });
1339
+ }, !derived);
1340
+ }
1341
+
1342
+ /** Has the deposit relayer watch deposit address `address` of this wallet's for another 7 days, at the fee it was
1343
+ * handed out with: what reached it after its 7 days is deposited then, and so is what reaches it within the next.
1344
+ * `depositAddresses()` dates it from now, on every device. */
1345
+ rewatchDepositAddress(address: Address): Promise<{ address: Address; fee: bigint }> {
1346
+ return this.#seeded(async () => {
1347
+ const receipt = this.#depositReceipt(address);
1348
+ const fee = await rewatchDeposit(chainCtx(this.#ctx, receipt.chainId).chain, getAddress(address));
1349
+ // for every device of the wallet, written by the next sync: storage dates it anew
1350
+ receipt.atMs = Date.now();
1351
+ receipt.unwritten = true;
1352
+ await this.#ctx.save();
1353
+ return { address: getAddress(address), fee };
1354
+ }, false);
1355
+ }
1356
+
1357
+ /** Deposits what deposit address `address` of this wallet's holds, as the deposit relayer would, for when it does
1358
+ * not: down, or no longer watching the address. Its receipt says all the address was registered with, wherever its
1359
+ * `fee` is known (`depositAddresses()`), so no relayer is asked. `from`, another wallet of the user's, sends the
1360
+ * flush and pays its gas, and shows on chain as the depositor: never the account's own, which the address does not
1361
+ * name. What it holds must exceed the fee (`insufficient-funds`). It shows in `activity()` as any deposit through
1362
+ * the address. */
1363
+ flushDepositAddress(address: Address, opts: { from: WalletClient }): Promise<Hex> {
1364
+ if (!opts.from.account) fail('invalid', 'the wallet client has no account');
1365
+ return this.#seeded(() => flushDepositAddress(this.#ctx, this.#depositReceipt(address), opts.from), false);
1366
+ }
1367
+
1368
+ /** The receipt of deposit address `address` of this wallet's. */
1369
+ #depositReceipt(address: Address): DepositAddressReceipt {
1370
+ return (
1371
+ this.#ctx.state.receipts.find(
1372
+ (r): r is DepositAddressReceipt => r.kind === 'deposit-address' && isAddressEqual(r.depositAddress, address),
1373
+ ) ?? fail('invalid', `${address} is not a deposit address of this wallet's`)
1374
+ );
1215
1375
  }
1216
1376
 
1217
1377
  /** A receive code for `token`, to hand to one payer: it pays any amount, as often as it likes, publicly by a transfer
@@ -1265,10 +1425,16 @@ export class Wallet {
1265
1425
  const steps = BigInt(roundsToMerge(mergePool(all, chosen)));
1266
1426
  // a merge pays the relayer publicly, so the protocol takes its cut of that, grossed up as a payment's is
1267
1427
  const cut = steps === 0n ? 0n : (await grossed(this.#ctx, token.chainId, token.tokenId, relayer)) - relayer;
1268
- return { relayer: relayer * steps, protocol: cut * steps, fee: (relayer + cut) * steps, steps: Number(steps) };
1428
+ return {
1429
+ relayer: relayer * steps,
1430
+ protocol: cut * steps,
1431
+ protocolBps: bpsOf(cut, relayer + cut),
1432
+ fee: (relayer + cut) * steps,
1433
+ steps: Number(steps),
1434
+ };
1269
1435
  }
1270
1436
  // as the payment's plan reckons it, so that `fee` is a `maxFee` it passes
1271
- const { protocol } = await withdrawalOf(
1437
+ const { withdrawn, protocol } = await withdrawalOf(
1272
1438
  this.#ctx,
1273
1439
  token.chainId,
1274
1440
  token.tokenId,
@@ -1280,12 +1446,12 @@ export class Wallet {
1280
1446
  this.#ctx.state.ops.some((o) => o.status === 'pending') ||
1281
1447
  settling(this.#ctx, token.chainId, token.tokenId, size);
1282
1448
  payWith(all, amount + relayer + protocol, relayer, onTheWay);
1283
- return { relayer, protocol, fee: relayer + protocol, steps: 1 };
1449
+ return { relayer, protocol, protocolBps: bpsOf(protocol, withdrawn), fee: relayer + protocol, steps: 1 };
1284
1450
  }
1285
1451
 
1286
1452
  /** The most a send or withdrawal of `token` can pay: what is spendable, less the fees `fees` quotes on top of it, and
1287
1453
  * the merges first when one JoinSplit cannot spend it all, which `fees` then asks for (`needs-consolidation`). */
1288
- async maxAmount(opts: { kind: 'send' | 'withdraw'; token: Token }): Promise<bigint> {
1454
+ async maxAmount(opts: { kind: 'send' | 'withdraw'; token: Token }): Promise<MaxAmount> {
1289
1455
  const { token } = opts;
1290
1456
  const c = chainCtx(this.#ctx, token.chainId);
1291
1457
  const relayer =
@@ -1314,9 +1480,10 @@ export class Wallet {
1314
1480
  return merge && merge.map((m) => `${token.chainId}:${m.index}`);
1315
1481
  }
1316
1482
 
1317
- /** Pays `amount` privately: to an address that has registered (any wallet that ever opened), or to a receive code
1318
- * (`receiveCode`). An address that never registered fails with `recipient-not-registered`. One JoinSplit from
1319
- * what is spendable now, or it fails here: `needs-consolidation`, `funds-settling` or `insufficient-funds`.
1483
+ /** Pays `amount` privately: to an account that has registered (any wallet that ever opened), by its address or its
1484
+ * zERC20 ID (`accountId`), or to a receive code (`receiveCode`). An account that never registered fails with
1485
+ * `recipient-not-registered`. One JoinSplit from what is spendable now, or it fails here: `needs-consolidation`,
1486
+ * `funds-settling` or `insufficient-funds`.
1320
1487
  * `amount` is what the recipient gets, and `maxFee` caps what it pays on top, `fees({ kind: 'send' }).fee`: the
1321
1488
  * relayer names its fee, so a quote the user saw is the cap. */
1322
1489
  async send(opts: { to: string; token: Token; amount: bigint; maxFee: bigint }): Promise<Operation> {
@@ -1324,22 +1491,14 @@ export class Wallet {
1324
1491
  if (amount <= 0n) fail('invalid', 'the amount must be positive');
1325
1492
  // the JoinSplit is proved by a later sync, which holds the seed until the payment is done
1326
1493
  return this.#seeded(async () => {
1494
+ const r = recipientOf(opts.to, token);
1327
1495
  let to: Destination;
1328
- if (isAddress(opts.to)) {
1329
- const recipient = await lookupRecipient(this.#ctx.storage, hexToBytes(opts.to));
1496
+ if (r.kind === 'address') {
1497
+ const recipient = await lookupRecipient(this.#ctx.storage, hexToBytes(r.address));
1330
1498
  if (!recipient)
1331
- fail(
1332
- 'recipient-not-registered',
1333
- `${opts.to} cannot be paid privately: withdraw to it publicly, or ask for a receive code`,
1334
- );
1499
+ fail('recipient-not-registered', `${opts.to.trim()} never opened a zERC20 wallet: ask for a receive code`);
1335
1500
  to = { kind: 'address', recipient: recipient! };
1336
- } else {
1337
- const code = decodeCode(opts.to);
1338
- if (!code) return fail('invalid', 'not an address or a receive code');
1339
- if (code.chainId !== token.chainId || code.tokenId !== token.tokenId)
1340
- fail('invalid', 'the receive code is for another token');
1341
- to = { kind: 'code', code: opts.to.trim() };
1342
- }
1501
+ } else to = { kind: 'code', code: r.code };
1343
1502
  return this.#newPayment('send', token, amount, { to, maxFee: opts.maxFee, step: null, paid: 0n });
1344
1503
  });
1345
1504
  }
@@ -1412,6 +1571,21 @@ export class Wallet {
1412
1571
  }
1413
1572
  }
1414
1573
 
1574
+ /** Whom `to` names, as `send` and `deposit` take it: an account, by its address or its zERC20 ID, or a receive code of
1575
+ * `token`'s. */
1576
+ function recipientOf(
1577
+ to: string,
1578
+ token: Token,
1579
+ ): { kind: 'address'; address: Address } | { kind: 'code'; code: string; burnAddress: Hex } {
1580
+ const address = isAddress(to) ? to : idAccount(to);
1581
+ if (address) return { kind: 'address', address };
1582
+ const code = decodeCode(to);
1583
+ if (!code) return fail('invalid', 'not an address, a zERC20 ID or a receive code');
1584
+ if (code.chainId !== token.chainId || code.tokenId !== token.tokenId)
1585
+ fail('invalid', 'the receive code is for another token');
1586
+ return { kind: 'code', code: to.trim(), burnAddress: bytesToHex(code.burnAddress) };
1587
+ }
1588
+
1415
1589
  /** Each chain of `network`, with its servers. */
1416
1590
  const chainsOf = (network: Network): ChainCtx[] =>
1417
1591
  network.chains.map((config) => ({
@@ -105,7 +105,7 @@ export function signMerge(statement: string, witness: string): string;
105
105
  * `tokenId`, `chainId`, `commitmentRoot` and `aspRoot`; the rest is ignored), and the attester's witness for it:
106
106
  * `{statement, witness}`. The statement is the public signals the proof and the attestation cover. The attester's
107
107
  * witness has each input's private key replaced by its signing and nullifying keys, and its owner's signature on the
108
- * statement. Fails with the attester's own reason when the attester would refuse the witness.
108
+ * statement. Fails, with the reason, where the attester would refuse the witness.
109
109
  */
110
110
  export function signTransact(statement: string, witness: string): string;
111
111
 
@@ -436,7 +436,7 @@ export function signMerge(statement, witness) {
436
436
  * `tokenId`, `chainId`, `commitmentRoot` and `aspRoot`; the rest is ignored), and the attester's witness for it:
437
437
  * `{statement, witness}`. The statement is the public signals the proof and the attestation cover. The attester's
438
438
  * witness has each input's private key replaced by its signing and nullifying keys, and its owner's signature on the
439
- * statement. Fails with the attester's own reason when the attester would refuse the witness.
439
+ * statement. Fails, with the reason, where the attester would refuse the witness.
440
440
  * @param {string} statement
441
441
  * @param {string} witness
442
442
  * @returns {string}
Binary file