@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/rageExit.ts CHANGED
@@ -1,15 +1,46 @@
1
1
  // Rage exit: a burn deposit refunded to its sender with nothing but the chain and the indexer's public pages, for
2
2
  // when the ASP, the attester or root proving stop. The ledger authenticates the deposit itself, by re-hashing the
3
- // commitments from it up to a checkpoint; a long way is walked back one transaction of commitments at a time.
3
+ // commitments from it up to a checkpoint; a long way is walked back one transaction of commitments at a time. A deposit
4
+ // address's refunds, the rage exit's and any other, go where its refund signer, a key of this wallet's, signs for; and
5
+ // what reaches one is deposited without the deposit relayer by a flush of its own.
4
6
 
5
- import { type Address, type Hex, decodeEventLog, numberToHex, parseAbi } from 'viem';
6
- import { depositAddressAbi, ledgerAbi, verifierAbi } from './chain.js';
7
- import { type ChainCtx, type Ctx, chainCtx, spendKey, walletOn, walletSees } from './context.js';
7
+ import {
8
+ type Address,
9
+ type Hex,
10
+ type WalletClient,
11
+ decodeEventLog,
12
+ encodeFunctionData,
13
+ isAddressEqual,
14
+ multicall3Abi,
15
+ numberToHex,
16
+ parseAbi,
17
+ } from 'viem';
18
+ import {
19
+ MULTICALL3,
20
+ NATIVE,
21
+ depositAddressAbi,
22
+ depositFactoryAbi,
23
+ erc20Abi,
24
+ factoryArgs,
25
+ ledgerAbi,
26
+ refundDigest,
27
+ verifierAbi,
28
+ } from './chain.js';
29
+ import {
30
+ type ChainCtx,
31
+ type Ctx,
32
+ chainCtx,
33
+ refundAccount,
34
+ spendKey,
35
+ switched,
36
+ walletOn,
37
+ walletSees,
38
+ } from './context.js';
8
39
  import { core } from './core.js';
9
40
  import { fail, guard } from './errors.js';
10
41
  import { prove } from './prove.js';
11
42
  import { PAGE } from './servers.js';
12
- import type { DepositRecord } from './state.js';
43
+ import type { DepositAddressReceipt, DepositRecord } from './state.js';
13
44
 
14
45
  /** Commitments per transaction: about a thousand fit in the 128 KB transaction size limit. */
15
46
  const SEGMENT = 900;
@@ -18,10 +49,10 @@ const checkpointEvent = parseAbi(['event CheckpointRecorded(uint64 indexed index
18
49
 
19
50
  type Fields = [bigint, bigint, bigint, bigint];
20
51
 
21
- /** Commitments `[from, to)` and the hash chain before `from`, from the indexer's pages. */
22
- async function commitments(c: ChainCtx, from: number, to: number): Promise<{ prev: Hex; list: Fields[] }> {
52
+ /** Commitments `[from, to)`, each with the hash chain before it, from the indexer's pages. */
53
+ async function commitments(c: ChainCtx, from: number, to: number): Promise<{ list: Fields[]; before: Hex[] }> {
23
54
  const list: Fields[] = [];
24
- let prev: Hex | null = null;
55
+ const before: Hex[] = [];
25
56
  for (let p = Math.floor(from / PAGE); p * PAGE < to; p++) {
26
57
  const { hashChainBefore, entries } = await c.indexer.commitments(p);
27
58
  let chain = hashChainBefore;
@@ -31,21 +62,35 @@ async function commitments(c: ChainCtx, from: number, to: number): Promise<{ pre
31
62
  const fields = [0, 1, 2, 3].map((f) =>
32
63
  BigInt(`0x${hexOf(entries.subarray(j * 128 + 32 * f, j * 128 + 32 * (f + 1)))}`),
33
64
  ) as Fields;
34
- if (i === from) prev = chain;
35
- if (i >= from) list.push(fields);
65
+ if (i >= from) {
66
+ list.push(fields);
67
+ before.push(chain);
68
+ }
36
69
  chain = core.hashChain(
37
70
  chain,
38
71
  fields.map((x) => numberToHex(x, { size: 32 })),
39
72
  ) as Hex;
40
73
  }
41
74
  }
42
- if (prev === null || list.length !== to - from)
75
+ if (list.length !== to - from)
43
76
  return fail('unavailable', `the indexer does not serve commitments ${from}..${to} yet`);
44
- return { prev, list };
77
+ return { list, before };
45
78
  }
46
79
 
47
80
  const hexOf = (b: Uint8Array) => Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
48
81
 
82
+ /** The `checkpointBefore` calls that walk back from the checkpoint at `end` to one at most a segment after `index`,
83
+ * the nearest the checkpoint first, and the checkpoint they end at. Each records the hash chain before its segment as
84
+ * the checkpoint the next one ends at. `list[k]` is the commitment at `index + k` and `before[k]` the chain before it. */
85
+ export function walkBack(index: number, end: number, list: Fields[], before: Hex[]) {
86
+ const calls: [bigint, bigint, Fields[]][] = [];
87
+ for (; end - index > SEGMENT; end -= SEGMENT) {
88
+ const start = end - SEGMENT - index;
89
+ calls.push([BigInt(end), BigInt(before[start]), list.slice(start, end - index)]);
90
+ }
91
+ return { calls, end };
92
+ }
93
+
49
94
  /** The first index after `index` the ledger has a checkpoint at: a proved root's size, else a fresh reservation. */
50
95
  async function checkpointAfter(ctx: Ctx, c: ChainCtx, index: number, send: Sender): Promise<number> {
51
96
  const ledger = await c.chain.ledger();
@@ -63,7 +108,7 @@ async function checkpointAfter(ctx: Ctx, c: ChainCtx, index: number, send: Sende
63
108
  }
64
109
  if (roots.length < PAGE) break;
65
110
  }
66
- const receipt = await send(ledger, ledgerAbi, 'reserveHashChain', []);
111
+ const receipt = await send({ address: ledger, abi: ledgerAbi, functionName: 'reserveHashChain', args: [] });
67
112
  for (const log of receipt.logs) {
68
113
  try {
69
114
  const ev = decodeEventLog({ abi: checkpointEvent, data: log.data, topics: log.topics });
@@ -73,24 +118,29 @@ async function checkpointAfter(ctx: Ctx, c: ChainCtx, index: number, send: Sende
73
118
  return fail('unavailable', 'reserveHashChain recorded no checkpoint');
74
119
  }
75
120
 
76
- type Sender = (
77
- address: Address,
78
- abi: readonly unknown[],
79
- functionName: string,
80
- args: unknown[],
81
- ) => Promise<{ logs: { data: Hex; topics: [Hex, ...Hex[]] }[]; status: string }>;
121
+ interface Call {
122
+ address: Address;
123
+ abi: readonly unknown[];
124
+ functionName: string;
125
+ args: readonly unknown[];
126
+ }
82
127
 
83
- /** Refunds the deposit at `index` of `record` to its sender, from the account's own wallet (it pays the gas). */
84
- export async function rageExit(ctx: Ctx, record: DepositRecord, index: number): Promise<Hex[]> {
85
- const c = chainCtx(ctx, record.chainId);
86
- const found = record.found.find((f) => f.index === index) ?? fail('invalid', `no deposit at ${index}`);
87
- if (found.spent) fail('invalid', 'the deposit is already spent');
88
- const wc = await walletOn(ctx, record.chainId);
89
- const account = ctx.account;
90
- const hashes: Hex[] = [];
91
- const send: Sender = async (address, abi, functionName, args) => {
128
+ type Sender = (call: Call) => Promise<{ logs: { data: Hex; topics: [Hex, ...Hex[]] }[]; status: string }>;
129
+
130
+ /** Who sends a refund's transactions and pays their gas (null: the account's own wallet), and where a refund from one
131
+ * of this wallet's deposit addresses goes. */
132
+ export interface Route {
133
+ from: WalletClient | null;
134
+ to: Address;
135
+ }
136
+
137
+ /** Sends from `from`, or from the account's own wallet, and waits for each transaction to land; `hashes` collects them. */
138
+ async function sender(ctx: Ctx, c: ChainCtx, from: WalletClient | null, hashes: Hex[]) {
139
+ const wc = from ? await switched(from, c.config.chainId) : await walletOn(ctx, c.config.chainId);
140
+ const account = from?.account ?? ctx.account;
141
+ const send: Sender = async ({ address, abi, functionName, args }) => {
92
142
  const hash = await guard(
93
- wc.writeContract({ account, chain: null, address, abi: abi as never, functionName: functionName, args: args }),
143
+ wc.writeContract({ account, chain: null, address, abi: abi as never, functionName, args }),
94
144
  );
95
145
  hashes.push(hash);
96
146
  const receipt = await c.chain.client.waitForTransactionReceipt({ hash });
@@ -98,24 +148,66 @@ export async function rageExit(ctx: Ctx, record: DepositRecord, index: number):
98
148
  await walletSees(wc, hash);
99
149
  return receipt as never;
100
150
  };
151
+ return { send, account };
152
+ }
101
153
 
102
- let end = await checkpointAfter(ctx, c, index, send);
103
- const { prev, list } = await commitments(c, index, end);
104
- const ledger = await c.chain.ledger();
105
- // walk a long way back first: each step records the chain before its segment as a new checkpoint
106
- while (end - index > SEGMENT) {
107
- const start = end - SEGMENT;
108
- const before = list.slice(0, start - index).reduce<Hex>(
109
- (chain, f) =>
110
- core.hashChain(
111
- chain,
112
- f.map((x) => numberToHex(x, { size: 32 })),
113
- ) as Hex,
114
- prev,
115
- );
116
- await send(ledger, ledgerAbi, 'checkpointBefore', [BigInt(end), BigInt(before), list.slice(start - index)]);
117
- end = start;
154
+ /** The call that sends on what `address` holds of `asset`, if it is a deposit address of `token` into `record`'s burn
155
+ * address, and where it goes: to `to`, signed by `record`'s refund signer, from one that names it; to its refund
156
+ * address, from one that names one. Null for anything else, a deposit address that names another signer included. */
157
+ async function refundCall(
158
+ ctx: Ctx,
159
+ c: ChainCtx,
160
+ record: DepositRecord,
161
+ address: Address,
162
+ token: Address,
163
+ asset: Address,
164
+ to: Address,
165
+ ): Promise<{ call: Call; recipient: Address } | null> {
166
+ const refund = await c.chain.depositAddressRefund(address, token, record.address);
167
+ if (!refund) return null;
168
+ if ('address' in refund) {
169
+ return {
170
+ call: { address, abi: depositAddressAbi, functionName: 'refund', args: [asset] },
171
+ recipient: refund.address,
172
+ };
118
173
  }
174
+ // only the burn addresses of this wallet's own lane have refund signers
175
+ if (record.burnIndex < 0) return null;
176
+ const signer = refundAccount(ctx, { chainId: record.chainId, tokenId: record.tokenId, index: record.burnIndex });
177
+ if (signer.address.toLowerCase() !== refund.signer.toLowerCase()) return null;
178
+ // long enough for any wallet's confirmation; sent again before it, the signature moves only more to the same `to`
179
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + 86_400);
180
+ const signature = await signer.signMessage({
181
+ message: { raw: refundDigest(c.config.chainId, address, asset, to, deadline) },
182
+ });
183
+ return {
184
+ call: { address, abi: depositAddressAbi, functionName: 'refundTo', args: [asset, to, deadline, signature] },
185
+ recipient: to,
186
+ };
187
+ }
188
+
189
+ const call3 = ({ address, abi, functionName, args }: Call) => ({
190
+ target: address,
191
+ allowFailure: false,
192
+ callData: encodeFunctionData({ abi, functionName, args }),
193
+ });
194
+
195
+ /** Refunds the deposit at `index` of `record` to its sender, sent by `route.from` or the account's own wallet, which
196
+ * pays the gas. A deposit address sends it on in the same transaction (Multicall3), so that nothing is left on it
197
+ * between two: one of this wallet's to `route.to`, a point of sale's to the refund address its payer named. */
198
+ export async function rageExit(ctx: Ctx, record: DepositRecord, index: number, route: Route): Promise<Hex[]> {
199
+ const c = chainCtx(ctx, record.chainId);
200
+ const found = record.found.find((f) => f.index === index) ?? fail('invalid', `no deposit at ${index}`);
201
+ if (found.spent) fail('invalid', 'the deposit is already spent');
202
+ const hashes: Hex[] = [];
203
+ const { send, account } = await sender(ctx, c, route.from, hashes);
204
+
205
+ const after = await checkpointAfter(ctx, c, index, send);
206
+ const { list, before } = await commitments(c, index, after);
207
+ const prev = before[0];
208
+ const ledger = await c.chain.ledger();
209
+ const { calls, end } = walkBack(index, after, list, before);
210
+ for (const args of calls) await send({ address: ledger, abi: ledgerAbi, functionName: 'checkpointBefore', args });
119
211
  const segment = list.slice(0, end - index);
120
212
  // checked first, so a wrong page from the indexer costs no gas
121
213
  await c.chain.client.readContract({
@@ -134,34 +226,118 @@ export async function rageExit(ctx: Ctx, record: DepositRecord, index: number):
134
226
  auth_key_hash: key.authKeyHash,
135
227
  burn_salt: record.salt,
136
228
  });
137
- const nullifier = signals[0];
138
- await send(c.config.verifier, verifierAbi, 'rageExit', [
139
- { checkpointIndex: BigInt(end), prevHashChain: BigInt(prev), commitments: segment, nullifier, proof },
140
- ]);
141
- found.spent = true;
142
- // a deposit address's refund lands on the address itself; forward it to the address's refund address: this account,
143
- // or for a point of sale's sale the payer's or the merchant's. A point of sale registers its deposit addresses without
144
- // telling this wallet, and a record restored from storage no longer knows its own, so the sender is checked on chain.
145
- const token = (await c.chain.tokens()).find((t) => t.tokenId === found.tokenId)!;
146
- // the deposit's commitment, which the ledger just authenticated: `tag, sender, burn address, amount`
229
+ const request = {
230
+ checkpointIndex: BigInt(end),
231
+ prevHashChain: BigInt(prev),
232
+ commitments: segment,
233
+ nullifier: signals[0],
234
+ proof,
235
+ };
236
+ const exit: Call = { address: c.config.verifier, abi: verifierAbi, functionName: 'rageExit', args: [request] };
237
+ // the deposit's commitment, which the ledger just authenticated: `tag, sender, burn address, amount`. A point of sale
238
+ // registers its deposit addresses without telling this wallet, and a record restored from storage no longer knows
239
+ // its own, so whether the sender is one is read on chain.
147
240
  const [, from, , amount] = list[0];
148
- const sender = numberToHex(from, { size: 20 });
149
- const refundTo = await c.chain.depositAddressRefund(sender, token.address, record.address);
150
- // for every device of the wallet, written by the next sync; kept now, before the forward, which may fail
241
+ const depositor = numberToHex(from, { size: 20 });
242
+ const token = (await c.chain.tokens()).find((t) => t.tokenId === found.tokenId)!;
243
+ const forward = await refundCall(ctx, c, record, depositor, token.address, token.address, route.to);
244
+ if (forward) {
245
+ // alone first, so that a failing rage exit says why rather than that the batch failed
246
+ await c.chain.client.simulateContract({
247
+ account,
248
+ address: c.config.verifier,
249
+ abi: verifierAbi,
250
+ functionName: 'rageExit',
251
+ args: [request],
252
+ });
253
+ await send({
254
+ address: MULTICALL3,
255
+ abi: multicall3Abi,
256
+ functionName: 'aggregate3',
257
+ args: [[call3(exit), call3(forward.call)]],
258
+ });
259
+ } else {
260
+ await send(exit);
261
+ }
262
+ found.spent = true;
263
+ // for every device of the wallet, written by the next sync
151
264
  ctx.state.receipts.push({
152
265
  kind: 'returned',
153
266
  chainId: record.chainId,
154
267
  tokenId: found.tokenId,
155
268
  burnAddress: record.address.toLowerCase() as Hex,
156
269
  index,
157
- recipient: (refundTo ?? sender).toLowerCase() as Hex,
270
+ recipient: (forward?.recipient ?? depositor).toLowerCase() as Hex,
158
271
  amount,
159
272
  txHash: hashes.at(-1)!,
160
273
  atMs: Date.now(),
161
274
  unwritten: true,
162
275
  });
163
276
  await ctx.save();
164
- if (refundTo) await send(sender, depositAddressAbi, 'refund', [token.address]);
165
- await ctx.save();
166
277
  return hashes;
167
278
  }
279
+
280
+ /** Sends what `record`'s deposit address `address` holds of `asset` to `route.to` (to its refund address, if it names
281
+ * one), sent by `route.from` or the account's own wallet. */
282
+ export async function refundDepositAddress(
283
+ ctx: Ctx,
284
+ record: DepositRecord,
285
+ address: Address,
286
+ asset: Address,
287
+ route: Route,
288
+ ): Promise<Hex> {
289
+ const c = chainCtx(ctx, record.chainId);
290
+ const token = (await c.chain.tokens()).find((t) => t.tokenId === record.tokenId)!;
291
+ const refund =
292
+ (await refundCall(ctx, c, record, address, token.address, asset, route.to)) ??
293
+ fail(
294
+ 'invalid',
295
+ `${address} is not deployed yet, or not one of this wallet's deposit addresses: it deploys on its first flush`,
296
+ );
297
+ const hashes: Hex[] = [];
298
+ const { send } = await sender(ctx, c, route.from, hashes);
299
+ await send(refund.call);
300
+ return hashes[0];
301
+ }
302
+
303
+ /** Deposits what deposit address `receipt.depositAddress` holds, as the deposit relayer would, by a flush with all its
304
+ * receipt says it was registered with, sent by `from`. */
305
+ export async function flushDepositAddress(ctx: Ctx, receipt: DepositAddressReceipt, from: WalletClient): Promise<Hex> {
306
+ const { depositAddress } = receipt;
307
+ const registered =
308
+ receipt.args ??
309
+ fail(
310
+ 'invalid',
311
+ `${depositAddress}'s receipt does not say what it was registered with: only the relayer flushes it`,
312
+ );
313
+ const c = chainCtx(ctx, receipt.chainId);
314
+ const token = (await c.chain.tokens()).find((t) => t.tokenId === receipt.tokenId)!;
315
+ const { fee, feeRecipient, refundSigner, referrer, referralBps } = registered;
316
+ const args = factoryArgs(
317
+ token.address,
318
+ receipt.burnAddress,
319
+ { signer: refundSigner },
320
+ feeRecipient,
321
+ fee,
322
+ referrer,
323
+ referralBps,
324
+ );
325
+ // the flush takes the very arguments checked here: others make another address, empty
326
+ if (!isAddressEqual(await c.chain.predictDepositAddress(args), depositAddress))
327
+ fail('invalid', `${depositAddress} is not the factory's address for what its receipt says`);
328
+ const held =
329
+ token.underlying.toLowerCase() === NATIVE.toLowerCase()
330
+ ? await c.chain.client.getBalance({ address: depositAddress })
331
+ : await c.chain.client.readContract({
332
+ address: token.underlying,
333
+ abi: erc20Abi,
334
+ functionName: 'balanceOf',
335
+ args: [depositAddress],
336
+ });
337
+ // its flush would revert: nothing reached it since, or the relayer flushed it first
338
+ if (held <= fee) fail('insufficient-funds', `${depositAddress} holds ${held}, not more than its fee of ${fee}`);
339
+ const hashes: Hex[] = [];
340
+ const { send } = await sender(ctx, c, from, hashes);
341
+ await send({ address: c.config.depositAddressFactory, abi: depositFactoryAbi, functionName: 'flush', args });
342
+ return hashes[0];
343
+ }
package/src/seed.ts CHANGED
@@ -7,7 +7,15 @@
7
7
  import { secp256k1 } from '@noble/curves/secp256k1.js';
8
8
  import { entropyToMnemonic } from '@scure/bip39';
9
9
  import { wordlist } from '@scure/bip39/wordlists/english.js';
10
- import { type Hex, type WalletClient, bytesToHex, hexToBytes, keccak256 } from 'viem';
10
+ import {
11
+ type Hex,
12
+ type WalletClient,
13
+ bytesToHex,
14
+ encodeAbiParameters,
15
+ hexToBytes,
16
+ keccak256,
17
+ parseAbiParameters,
18
+ } from 'viem';
11
19
  import { mnemonicToAccount } from 'viem/accounts';
12
20
  import { fail, guard } from './errors.js';
13
21
 
@@ -19,6 +27,21 @@ export function accountKey(seed: Uint8Array): Hex {
19
27
  return bytesToHex(key ?? fail('invalid', 'the seed derives no account key'));
20
28
  }
21
29
 
30
+ /** The private key that signs where the refunds of the deposit address handed out with burn address `index` of
31
+ * `tokenId` on `chainId` go (`DepositAddress.refundTo`). One per address, so that the addresses name neither one another
32
+ * nor the account; from the seed itself, not the view key, since whoever holds it moves those refunds. */
33
+ export function refundKey(seed: Uint8Array, chainId: bigint, tokenId: number, index: number): Hex {
34
+ return keccak256(
35
+ encodeAbiParameters(parseAbiParameters('string, bytes32, uint64, uint32, uint32'), [
36
+ 'zerc20/wallet/refund-signer/v1',
37
+ bytesToHex(seed),
38
+ chainId,
39
+ tokenId,
40
+ index,
41
+ ]),
42
+ );
43
+ }
44
+
22
45
  const message = (address: Hex) =>
23
46
  [
24
47
  'zERC20 wallet seed',
package/src/servers.ts CHANGED
@@ -62,6 +62,8 @@ export interface IndexerStatus {
62
62
  mismatch: { size: number; root: Hex } | null;
63
63
  /** The newest block whose entries cannot reorg, the one the final counts stand at. */
64
64
  finalBlock: number | null;
65
+ /** The last block it has indexed. Read after the counts: they may stand before it. */
66
+ cursor: number;
65
67
  }
66
68
 
67
69
  interface StatusJson {
@@ -77,7 +79,7 @@ interface StatusJson {
77
79
  partial_page_root: Hex | null;
78
80
  } | null;
79
81
  mismatch: { index: number; root: Hex } | null;
80
- indexer: { final_block: number | null };
82
+ indexer: { final_block: number | null; cursor: number };
81
83
  }
82
84
 
83
85
  export class Indexer {
@@ -97,6 +99,7 @@ export class Indexer {
97
99
  : null,
98
100
  mismatch: s.mismatch ? { size: s.mismatch.index, root: s.mismatch.root } : null,
99
101
  finalBlock: s.indexer.final_block,
102
+ cursor: s.indexer.cursor,
100
103
  };
101
104
  }
102
105
 
package/src/services.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  // see the public statement and a witness sealed to their enclave, the transact-relayer a finished request, the
3
3
  // deposit-relayer a burn address.
4
4
 
5
- import { type Address, type Hex, bytesToHex, hexToBytes, zeroAddress } from 'viem';
6
- import type { Chain, Referrer } from './chain.js';
5
+ import { type Address, type Hex, bytesToHex, getAddress, hexToBytes, zeroAddress } from 'viem';
6
+ import { type Chain, type Referrer, type Refund, factoryArgs } from './chain.js';
7
7
  import { core } from './core.js';
8
8
  import { WalletError, fail } from './errors.js';
9
9
  import { type AttesterTrust, type ChainConfig, LOCAL_CHAIN_ID, type Network } from './network.js';
@@ -101,6 +101,8 @@ export async function relayerFees(c: ChainConfig): Promise<RelayerFees> {
101
101
 
102
102
  export interface Relayed {
103
103
  txHash: Hex;
104
+ /** The block it was mined in. */
105
+ block: number;
104
106
  noteIndex1: number | null;
105
107
  noteIndex2: number | null;
106
108
  }
@@ -119,7 +121,7 @@ export const relay = (
119
121
  export const relayMerge = (
120
122
  c: ChainConfig,
121
123
  request: object,
122
- ): Promise<{ txHash: Hex; noteIndex: number | null } | { reverted: string }> =>
124
+ ): Promise<{ txHash: Hex; block: number; noteIndex: number | null } | { reverted: string }> =>
123
125
  relayed(`${c.transactRelayer}/merge`, { request });
124
126
 
125
127
  async function relayed<T>(url: string, body: object): Promise<T | { reverted: string }> {
@@ -163,33 +165,44 @@ export async function proofOf(
163
165
  return r.body;
164
166
  }
165
167
 
168
+ /** Has the deposit relayer watch `depositAddress` for another 7 days, at the fee it was registered with, which the
169
+ * address fixes: the fee the relayer names. */
170
+ export async function rewatchDeposit(chain: Chain, depositAddress: Address): Promise<bigint> {
171
+ const r = await post<{ fee: Hex }>(`${chain.config.depositRelayer}/rewatch`, { depositAddress });
172
+ if (typeof r.body === 'string') return fail('unavailable', `deposit-relayer: ${r.body}`);
173
+ return BigInt(r.body.fee);
174
+ }
175
+
166
176
  /** A deposit address that flushes whatever reaches it into `burnAddress`, less `fee` paid to `feeRecipient` and the
167
- * referrer's share of the rest each time; refunds go to `refundAddress`. What the relayer says is checked against the
168
- * factory: an address of its own would take the funds, and the fee it names is the one the address takes. */
177
+ * referrer's share of the rest each time; refunds go as `refund` says. What the relayer says is checked against the
178
+ * factory: an address of its own would take the funds, and the fee it names is the one the address takes. The address
179
+ * comes back checksummed, as every other address the SDK gives does: the relayer writes it in lower case. */
169
180
  export async function registerDeposit(
170
181
  chain: Chain,
171
182
  token: Address,
172
183
  burnAddress: Hex,
173
- refundAddress: Address,
184
+ refund: Refund,
174
185
  referrer: Referrer = { address: zeroAddress, bps: 0 },
175
186
  ): Promise<{ depositAddress: Address; feeRecipient: Address; fee: bigint }> {
176
187
  const r = await post<{ depositAddress: Address; feeRecipient: Address; fee: Hex }>(
177
188
  `${chain.config.depositRelayer}/register`,
178
- { token, burnAddress, refundAddress, referrer: referrer.address, referralBps: referrer.bps },
189
+ {
190
+ token,
191
+ burnAddress,
192
+ ...('address' in refund ? { refundAddress: refund.address } : { refundSigner: refund.signer }),
193
+ referrer: referrer.address,
194
+ referralBps: referrer.bps,
195
+ },
179
196
  );
180
197
  if (typeof r.body === 'string') return fail('unavailable', `deposit-relayer: ${r.body}`);
181
198
  const { depositAddress, feeRecipient } = r.body;
182
199
  const fee = BigInt(r.body.fee);
200
+ // no balance exceeds such a fee, and storage holds values below 2^248 only
201
+ if (fee >= 1n << 248n) fail('unavailable', `deposit-relayer: a fee of ${fee}`);
183
202
  const expected = await chain.predictDepositAddress(
184
- token,
185
- burnAddress,
186
- refundAddress,
187
- feeRecipient,
188
- fee,
189
- referrer.address,
190
- referrer.bps,
203
+ factoryArgs(token, burnAddress, refund, feeRecipient, fee, referrer.address, referrer.bps),
191
204
  );
192
205
  if (depositAddress.toLowerCase() !== expected.toLowerCase())
193
206
  fail('indexer-mismatch', `the deposit relayer gave ${depositAddress}, not the factory's ${expected}`);
194
- return { depositAddress, feeRecipient, fee };
207
+ return { depositAddress: getAddress(depositAddress), feeRecipient, fee };
195
208
  }