@zerc20/wallet-sdk 0.4.0 → 0.6.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/LICENSE +17 -20
- package/README.md +2 -2
- 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 +86 -9
- package/dist/chain.js +61 -33
- package/dist/chain.js.map +1 -1
- package/dist/context.d.ts +8 -2
- package/dist/context.js +15 -2
- package/dist/context.js.map +1 -1
- package/dist/core.d.ts +35 -5
- package/dist/core.js +98 -25
- package/dist/core.js.map +1 -1
- package/dist/errors.d.ts +20 -3
- package/dist/errors.js +7 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/network.d.ts +4 -2
- package/dist/network.js +3 -1
- package/dist/network.js.map +1 -1
- package/dist/pos.d.ts +10 -1
- package/dist/pos.js +219 -55
- package/dist/pos.js.map +1 -1
- package/dist/prove.d.ts +3 -0
- package/dist/prove.js +9 -0
- package/dist/prove.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 +5 -0
- package/dist/servers.js +2 -0
- package/dist/servers.js.map +1 -1
- package/dist/services.d.ts +14 -6
- package/dist/services.js +35 -14
- package/dist/services.js.map +1 -1
- package/dist/spend.d.ts +53 -11
- package/dist/spend.js +171 -65
- 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 +49 -18
- package/dist/sync.js.map +1 -1
- package/dist/wallet.d.ts +132 -21
- package/dist/wallet.js +342 -91
- package/dist/wallet.js.map +1 -1
- package/package.json +3 -3
- package/src/accountId.ts +22 -0
- package/src/chain.ts +101 -49
- package/src/context.ts +27 -3
- package/src/core.ts +112 -26
- package/src/errors.ts +15 -2
- package/src/index.ts +15 -2
- package/src/network.ts +6 -3
- package/src/pos.ts +247 -61
- package/src/prove.ts +10 -0
- package/src/rageExit.ts +237 -61
- package/src/seed.ts +24 -1
- package/src/servers.ts +9 -1
- package/src/services.ts +36 -22
- package/src/spend.ts +188 -75
- package/src/state.ts +25 -1
- package/src/sync.ts +54 -19
- package/src/wallet.ts +437 -109
- package/wasm/zerc20_wallet_core.d.ts +1 -1
- package/wasm/zerc20_wallet_core.js +1 -1
- 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 {
|
|
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,11 @@ 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;
|
|
67
|
+
/** How long a commitment usually waits for a proved root to cover it, in seconds, as measured on chain: anyone may
|
|
68
|
+
* prove roots, at any pace. Null until measured, and from an indexer that does not measure it. */
|
|
69
|
+
settleSecs: number | null;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
interface StatusJson {
|
|
@@ -77,7 +82,8 @@ interface StatusJson {
|
|
|
77
82
|
partial_page_root: Hex | null;
|
|
78
83
|
} | null;
|
|
79
84
|
mismatch: { index: number; root: Hex } | null;
|
|
80
|
-
|
|
85
|
+
settle_secs?: number | null;
|
|
86
|
+
indexer: { final_block: number | null; cursor: number };
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
export class Indexer {
|
|
@@ -97,6 +103,8 @@ export class Indexer {
|
|
|
97
103
|
: null,
|
|
98
104
|
mismatch: s.mismatch ? { size: s.mismatch.index, root: s.mismatch.root } : null,
|
|
99
105
|
finalBlock: s.indexer.final_block,
|
|
106
|
+
cursor: s.indexer.cursor,
|
|
107
|
+
settleSecs: s.settle_secs ?? null,
|
|
100
108
|
};
|
|
101
109
|
}
|
|
102
110
|
|
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,12 +101,15 @@ 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
|
}
|
|
107
109
|
|
|
108
|
-
/** What `relay` reports
|
|
109
|
-
|
|
110
|
+
/** What `relay` reports while the relayer is relaying a request with the same nullifiers: the request already there
|
|
111
|
+
* gets the answer, and one after it finds the transaction landed. */
|
|
112
|
+
export class Relaying extends WalletError {}
|
|
110
113
|
|
|
111
114
|
/** Has the relayer submit `request`. A 400 means the call reverts, which includes "already landed". */
|
|
112
115
|
export const relay = (
|
|
@@ -119,17 +122,17 @@ export const relay = (
|
|
|
119
122
|
export const relayMerge = (
|
|
120
123
|
c: ChainConfig,
|
|
121
124
|
request: object,
|
|
122
|
-
): Promise<{ txHash: Hex; noteIndex: number | null } | { reverted: string }> =>
|
|
125
|
+
): Promise<{ txHash: Hex; block: number; noteIndex: number | null } | { reverted: string }> =>
|
|
123
126
|
relayed(`${c.transactRelayer}/merge`, { request });
|
|
124
127
|
|
|
125
128
|
async function relayed<T>(url: string, body: object): Promise<T | { reverted: string }> {
|
|
126
|
-
// the relayer answers once mined, after fee bumping if need be
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
});
|
|
129
|
+
// the relayer answers once mined, after fee bumping if need be. Without an answer it may have sent it all the same:
|
|
130
|
+
// the same request goes again next sync, and lands at most once
|
|
131
|
+
const r = await post<T>(url, body, 15 * 60_000);
|
|
130
132
|
if (typeof r.body !== 'string') return r.body;
|
|
131
133
|
if (r.status === 400) return { reverted: r.body };
|
|
132
|
-
throw new
|
|
134
|
+
if (r.status === 409) throw new Relaying('unavailable', `transact-relayer: ${r.body}`);
|
|
135
|
+
return fail('unavailable', `transact-relayer: HTTP ${r.status}: ${r.body}`);
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
export type MergeProver = NonNullable<Network['mergeProver']>;
|
|
@@ -163,33 +166,44 @@ export async function proofOf(
|
|
|
163
166
|
return r.body;
|
|
164
167
|
}
|
|
165
168
|
|
|
169
|
+
/** Has the deposit relayer watch `depositAddress` for another 7 days, at the fee it was registered with, which the
|
|
170
|
+
* address fixes: the fee the relayer names. */
|
|
171
|
+
export async function rewatchDeposit(chain: Chain, depositAddress: Address): Promise<bigint> {
|
|
172
|
+
const r = await post<{ fee: Hex }>(`${chain.config.depositRelayer}/rewatch`, { depositAddress });
|
|
173
|
+
if (typeof r.body === 'string') return fail('unavailable', `deposit-relayer: ${r.body}`);
|
|
174
|
+
return BigInt(r.body.fee);
|
|
175
|
+
}
|
|
176
|
+
|
|
166
177
|
/** 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.
|
|
178
|
+
* referrer's share of the rest each time; refunds go as `refund` says. What the relayer says is checked against the
|
|
179
|
+
* factory: an address of its own would take the funds, and the fee it names is the one the address takes. The address
|
|
180
|
+
* comes back checksummed, as every other address the SDK gives does: the relayer writes it in lower case. */
|
|
169
181
|
export async function registerDeposit(
|
|
170
182
|
chain: Chain,
|
|
171
183
|
token: Address,
|
|
172
184
|
burnAddress: Hex,
|
|
173
|
-
|
|
185
|
+
refund: Refund,
|
|
174
186
|
referrer: Referrer = { address: zeroAddress, bps: 0 },
|
|
175
187
|
): Promise<{ depositAddress: Address; feeRecipient: Address; fee: bigint }> {
|
|
176
188
|
const r = await post<{ depositAddress: Address; feeRecipient: Address; fee: Hex }>(
|
|
177
189
|
`${chain.config.depositRelayer}/register`,
|
|
178
|
-
{
|
|
190
|
+
{
|
|
191
|
+
token,
|
|
192
|
+
burnAddress,
|
|
193
|
+
...('address' in refund ? { refundAddress: refund.address } : { refundSigner: refund.signer }),
|
|
194
|
+
referrer: referrer.address,
|
|
195
|
+
referralBps: referrer.bps,
|
|
196
|
+
},
|
|
179
197
|
);
|
|
180
198
|
if (typeof r.body === 'string') return fail('unavailable', `deposit-relayer: ${r.body}`);
|
|
181
199
|
const { depositAddress, feeRecipient } = r.body;
|
|
182
200
|
const fee = BigInt(r.body.fee);
|
|
201
|
+
// no balance exceeds such a fee, and storage holds values below 2^248 only
|
|
202
|
+
if (fee >= 1n << 248n) fail('unavailable', `deposit-relayer: a fee of ${fee}`);
|
|
183
203
|
const expected = await chain.predictDepositAddress(
|
|
184
|
-
token,
|
|
185
|
-
burnAddress,
|
|
186
|
-
refundAddress,
|
|
187
|
-
feeRecipient,
|
|
188
|
-
fee,
|
|
189
|
-
referrer.address,
|
|
190
|
-
referrer.bps,
|
|
204
|
+
factoryArgs(token, burnAddress, refund, feeRecipient, fee, referrer.address, referrer.bps),
|
|
191
205
|
);
|
|
192
206
|
if (depositAddress.toLowerCase() !== expected.toLowerCase())
|
|
193
207
|
fail('indexer-mismatch', `the deposit relayer gave ${depositAddress}, not the factory's ${expected}`);
|
|
194
|
-
return { depositAddress, feeRecipient, fee };
|
|
208
|
+
return { depositAddress: getAddress(depositAddress), feeRecipient, fee };
|
|
195
209
|
}
|