@zerc20/wallet-sdk 0.3.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.
- package/README.md +1 -1
- package/dist/accountId.d.ts +5 -0
- package/dist/accountId.js +19 -0
- package/dist/accountId.js.map +1 -0
- package/dist/chain.d.ts +85 -9
- package/dist/chain.js +60 -33
- package/dist/chain.js.map +1 -1
- package/dist/context.d.ts +14 -6
- package/dist/context.js +17 -3
- package/dist/context.js.map +1 -1
- package/dist/core.d.ts +35 -5
- package/dist/core.js +95 -23
- package/dist/core.js.map +1 -1
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/pos.d.ts +7 -1
- package/dist/pos.js +213 -55
- package/dist/pos.js.map +1 -1
- package/dist/rageExit.d.ts +27 -4
- package/dist/rageExit.js +160 -49
- package/dist/rageExit.js.map +1 -1
- package/dist/seed.d.ts +4 -0
- package/dist/seed.js +13 -1
- package/dist/seed.js.map +1 -1
- package/dist/servers.d.ts +2 -0
- package/dist/servers.js +1 -0
- package/dist/servers.js.map +1 -1
- package/dist/services.d.ts +11 -4
- package/dist/services.js +26 -7
- package/dist/services.js.map +1 -1
- package/dist/spend.d.ts +30 -9
- package/dist/spend.js +46 -17
- package/dist/spend.js.map +1 -1
- package/dist/state.d.ts +21 -0
- package/dist/state.js.map +1 -1
- package/dist/sync.d.ts +10 -5
- package/dist/sync.js +53 -19
- package/dist/sync.js.map +1 -1
- package/dist/viewKey.d.ts +14 -0
- package/dist/viewKey.js +45 -0
- package/dist/viewKey.js.map +1 -0
- package/dist/wallet.d.ts +109 -16
- package/dist/wallet.js +291 -85
- package/dist/wallet.js.map +1 -1
- package/package.json +3 -2
- package/src/accountId.ts +22 -0
- package/src/chain.ts +95 -49
- package/src/context.ts +35 -8
- package/src/core.ts +109 -24
- package/src/errors.ts +2 -0
- package/src/index.ts +18 -2
- package/src/pos.ts +240 -61
- package/src/rageExit.ts +237 -61
- package/src/seed.ts +24 -1
- package/src/servers.ts +4 -1
- package/src/services.ts +28 -15
- package/src/spend.ts +58 -23
- package/src/state.ts +25 -1
- package/src/sync.ts +57 -20
- package/src/viewKey.ts +49 -0
- package/src/wallet.ts +387 -106
- package/wasm/zerc20_wallet_core.d.ts +8 -1
- package/wasm/zerc20_wallet_core.js +31 -1
- package/wasm/zerc20_wallet_core_bg.wasm +0 -0
- package/wasm/zerc20_wallet_core_bg.wasm.d.ts +1 -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 {
|
|
6
|
-
|
|
7
|
-
|
|
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)
|
|
22
|
-
async function commitments(c: ChainCtx, from: number, to: number): Promise<{
|
|
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
|
-
|
|
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
|
|
35
|
-
|
|
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 (
|
|
75
|
+
if (list.length !== to - from)
|
|
43
76
|
return fail('unavailable', `the indexer does not serve commitments ${from}..${to} yet`);
|
|
44
|
-
return {
|
|
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
|
-
|
|
77
|
-
address: Address
|
|
78
|
-
abi: readonly unknown[]
|
|
79
|
-
functionName: string
|
|
80
|
-
args: unknown[]
|
|
81
|
-
|
|
121
|
+
interface Call {
|
|
122
|
+
address: Address;
|
|
123
|
+
abi: readonly unknown[];
|
|
124
|
+
functionName: string;
|
|
125
|
+
args: readonly unknown[];
|
|
126
|
+
}
|
|
82
127
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
//
|
|
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
|
|
149
|
-
const
|
|
150
|
-
|
|
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: (
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
}
|