@kohaku-eth/tornado-cash 0.0.1 → 0.0.2-alpha.9

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/dist/index.js ADDED
@@ -0,0 +1,778 @@
1
+ import { createSelector, createAsyncThunk } from '@reduxjs/toolkit';
2
+ import { createTx } from '@kohaku-eth/provider';
3
+ import { encodeFunctionData, toHex, erc20Abi } from 'viem';
4
+ import { proxy } from 'comlink';
5
+ import { loadStateManagerWorker } from '#worker-loader';
6
+ import { BundlerClient, TornadoBuilder } from '@privacy-paymasters/sdk';
7
+ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
8
+ import { numberToBytesLE, bytesToNumberLE, concatBytes } from '@noble/curves/utils.js';
9
+ import { keccak_256 } from '@noble/hashes/sha3.js';
10
+ import { pedersenHash as pedersenHash$1, Point } from 'micro-zk-proofs/pedersen.js';
11
+ import 'micro-zk-proofs/mimcsponge.js';
12
+ import 'fixed-merkle-tree';
13
+
14
+ // src/state/thunks/getDepositPayloadThunk.ts
15
+
16
+ // src/data/abis/pool.abi.ts
17
+ var poolAbi = [
18
+ {
19
+ type: "function",
20
+ name: "getLastRoot",
21
+ inputs: [],
22
+ outputs: [
23
+ {
24
+ name: "",
25
+ type: "bytes32",
26
+ internalType: "bytes32"
27
+ }
28
+ ],
29
+ stateMutability: "view"
30
+ },
31
+ {
32
+ type: "function",
33
+ name: "currentRootIndex",
34
+ inputs: [],
35
+ outputs: [
36
+ {
37
+ name: "",
38
+ type: "uint32",
39
+ internalType: "uint32"
40
+ }
41
+ ],
42
+ stateMutability: "view"
43
+ },
44
+ {
45
+ type: "function",
46
+ name: "roots",
47
+ inputs: [{
48
+ type: "uint256"
49
+ }],
50
+ outputs: [
51
+ {
52
+ name: "",
53
+ type: "bytes32",
54
+ internalType: "bytes32"
55
+ }
56
+ ],
57
+ stateMutability: "view"
58
+ },
59
+ {
60
+ type: "function",
61
+ name: "denomination",
62
+ inputs: [],
63
+ outputs: [
64
+ {
65
+ type: "uint256",
66
+ internalType: "uint256"
67
+ }
68
+ ],
69
+ stateMutability: "view"
70
+ },
71
+ {
72
+ type: "function",
73
+ name: "deposit",
74
+ inputs: [
75
+ {
76
+ name: "_commitment",
77
+ type: "bytes32",
78
+ internalType: "bytes32"
79
+ }
80
+ ],
81
+ outputs: [],
82
+ stateMutability: "payable"
83
+ },
84
+ {
85
+ type: "function",
86
+ name: "withdraw",
87
+ inputs: [
88
+ { name: "_proof", type: "bytes", internalType: "bytes" },
89
+ { name: "_root", type: "bytes32", internalType: "bytes32" },
90
+ { name: "_nullifierHash", type: "bytes32", internalType: "bytes32" },
91
+ { name: "_recipient", type: "address", internalType: "address" },
92
+ { name: "_relayer", type: "address", internalType: "address" },
93
+ { name: "_fee", type: "uint256", internalType: "uint256" },
94
+ { name: "_refund", type: "uint256", internalType: "uint256" }
95
+ ],
96
+ outputs: [],
97
+ stateMutability: "payable"
98
+ },
99
+ {
100
+ type: "function",
101
+ name: "ROOT_HISTORY_SIZE",
102
+ inputs: [],
103
+ outputs: [{
104
+ type: "uint32"
105
+ }],
106
+ stateMutability: "view"
107
+ },
108
+ {
109
+ type: "function",
110
+ name: "isKnownRoot",
111
+ inputs: [{
112
+ name: "_root",
113
+ type: "bytes32"
114
+ }],
115
+ outputs: [{
116
+ type: "bool"
117
+ }],
118
+ stateMutability: "view"
119
+ }
120
+ ];
121
+ function prepareNativeShield({ commitment, poolAddress, poolDenomination }) {
122
+ const data = encodeFunctionData({
123
+ abi: poolAbi,
124
+ functionName: "deposit",
125
+ args: [toHex(commitment, { size: 32 })]
126
+ });
127
+ return createTx(poolAddress, data, poolDenomination);
128
+ }
129
+ function prepareErc20Shield({ commitment, tokenAddress, poolAddress, denomination }) {
130
+ const approveData = encodeFunctionData({
131
+ abi: erc20Abi,
132
+ functionName: "approve",
133
+ args: [poolAddress, denomination]
134
+ });
135
+ const depositData = encodeFunctionData({
136
+ abi: poolAbi,
137
+ functionName: "deposit",
138
+ args: [toHex(commitment, { size: 32 })]
139
+ });
140
+ return [
141
+ createTx(tokenAddress, approveData),
142
+ createTx(poolAddress, depositData)
143
+ ];
144
+ }
145
+ function addressToHex(address, padding = 40) {
146
+ return `0x${address.toString(16).padStart(padding, "0")}`;
147
+ }
148
+
149
+ // src/state/utils/serialize.utils.ts
150
+ var deserialize = (data, strings) => {
151
+ switch (typeof data) {
152
+ case "object": {
153
+ if (data === null) {
154
+ return data;
155
+ }
156
+ if (data instanceof Array) {
157
+ if (data.length === 2) {
158
+ const stringsData = strings || [];
159
+ return [
160
+ deserialize(data[0], stringsData[0]),
161
+ deserialize(data[1], stringsData[1])
162
+ ];
163
+ }
164
+ return data.map(
165
+ (d) => deserialize(d, strings)
166
+ );
167
+ }
168
+ return Object.entries(data).reduce(
169
+ (deserializedObject, [key, value]) => {
170
+ deserializedObject[key] = deserialize(
171
+ value,
172
+ (strings || {})[key]
173
+ );
174
+ return deserializedObject;
175
+ },
176
+ {}
177
+ );
178
+ }
179
+ case "string":
180
+ return strings === true ? data : BigInt(data);
181
+ default:
182
+ return data;
183
+ }
184
+ };
185
+
186
+ // src/state/utils/selectors.utils.ts
187
+ var selectEntityMap = (mapFn, transform = deserialize) => createSelector([mapFn], (tuple) => new Map(tuple.map(transform)));
188
+ createSelector(
189
+ [(s) => s.deposits.depositsTuples],
190
+ (tuples) => {
191
+ const deserialized = deserialize(tuples);
192
+ return new Map(
193
+ deserialized.map(
194
+ ([poolAddress, innerTuples]) => [poolAddress, new Map(innerTuples)]
195
+ )
196
+ );
197
+ }
198
+ );
199
+ var instanceRegistryInfoSelector = createSelector(
200
+ [(state) => state.instanceRegistryInfo],
201
+ (poolInfo) => deserialize(poolInfo, { ensSubdomainKey: true })
202
+ );
203
+ var poolsSelector = selectEntityMap(
204
+ (s) => s.pools.poolsTuples,
205
+ deserialize
206
+ );
207
+ selectEntityMap(
208
+ (s) => s.withdrawals.withdrawalsTuples,
209
+ deserialize
210
+ );
211
+ selectEntityMap(
212
+ (s) => s.assets.assetsTuples,
213
+ (assetsTuple) => deserialize(assetsTuple, [void 0, { name: true, symbol: true }])
214
+ );
215
+ createSelector(
216
+ [(state) => state.relayers.relayersTuples],
217
+ (tuples) => tuples.map(
218
+ ([, relayer]) => deserialize(relayer, { ensName: true, hostname: true })
219
+ )
220
+ );
221
+ var userSecretsSelector = selectEntityMap(
222
+ (s) => s.userSecrets.byPool,
223
+ deserialize
224
+ );
225
+
226
+ // src/state/thunks/getDepositPayloadThunk.ts
227
+ var DepositStrategy = /* @__PURE__ */ ((DepositStrategy2) => {
228
+ DepositStrategy2[DepositStrategy2["MaxAnonimitySet"] = 0] = "MaxAnonimitySet";
229
+ DepositStrategy2[DepositStrategy2["MinFee"] = 1] = "MinFee";
230
+ return DepositStrategy2;
231
+ })(DepositStrategy || {});
232
+ var calculatePoolsDeposits = (pools, value, strategy) => {
233
+ if (!pools.length) {
234
+ throw new Error("No pools to deposit into");
235
+ }
236
+ const poolsToDepositInto = [];
237
+ switch (strategy) {
238
+ case 0 /* MaxAnonimitySet */: {
239
+ const lowToHighDenominationPools = pools.sort((a, b) => Number(a.denomination - b.denomination));
240
+ const lowestDenominationPool = lowToHighDenominationPools[0];
241
+ const depositsCount = Number(value / lowestDenominationPool.denomination);
242
+ poolsToDepositInto.push({
243
+ ...lowestDenominationPool,
244
+ depositsCount
245
+ });
246
+ break;
247
+ }
248
+ case 1 /* MinFee */: {
249
+ const highToLowDenominationPools = pools.sort((a, b) => Number(b.denomination - a.denomination));
250
+ highToLowDenominationPools.reduce((remainingAmount, pool) => {
251
+ const depositsForThisPool = remainingAmount / pool.denomination;
252
+ if (depositsForThisPool > 0n) {
253
+ poolsToDepositInto.push({
254
+ ...pool,
255
+ depositsCount: Number(depositsForThisPool)
256
+ });
257
+ }
258
+ return remainingAmount - depositsForThisPool * pool.denomination;
259
+ }, value);
260
+ break;
261
+ }
262
+ }
263
+ return poolsToDepositInto;
264
+ };
265
+ createAsyncThunk(
266
+ "deposits/getPayload",
267
+ async ({ secretManager, asset, amount, strategy }, { getState }) => {
268
+ const state = getState();
269
+ const pools = poolsSelector(state);
270
+ const { chainId } = instanceRegistryInfoSelector(state);
271
+ const userSecrets = userSecretsSelector(state);
272
+ const poolsCandidates = Array.from(pools.values()).filter((p) => p.asset === asset);
273
+ if (poolsCandidates.length === 0) throw new Error(`No pools available for asset ${addressToHex(asset)}`);
274
+ const poolsToDepositInto = calculatePoolsDeposits(poolsCandidates, amount, strategy);
275
+ const depositInfo = await Promise.all(poolsToDepositInto.map(async (pool) => {
276
+ const startIndex = userSecrets.get(pool.address)?.length ?? 0;
277
+ const count = pool.depositsCount;
278
+ const secrets = await Promise.all(
279
+ Array.from(
280
+ { length: count },
281
+ (_, i) => secretManager.getDepositSecrets({
282
+ chainId,
283
+ depositIndex: startIndex + i,
284
+ poolAddress: pool.address
285
+ })
286
+ )
287
+ );
288
+ return {
289
+ ...pool,
290
+ secrets
291
+ };
292
+ }));
293
+ const newDeposits = depositInfo.reduce((txData, { secrets, ...pool }) => {
294
+ const poolAddressHex = addressToHex(pool.address);
295
+ if (!pool.isERC20) {
296
+ const newDeposits3 = secrets.map(
297
+ ({ commitment }) => prepareNativeShield({
298
+ commitment,
299
+ poolAddress: poolAddressHex,
300
+ poolDenomination: pool.denomination
301
+ })
302
+ );
303
+ return txData.concat(newDeposits3);
304
+ }
305
+ const assetHex = addressToHex(pool.asset);
306
+ const newDeposits2 = secrets.flatMap(
307
+ ({ commitment }) => prepareErc20Shield({
308
+ commitment,
309
+ tokenAddress: assetHex,
310
+ poolAddress: poolAddressHex,
311
+ denomination: pool.denomination
312
+ })
313
+ );
314
+ return txData.concat(newDeposits2);
315
+ }, []);
316
+ return newDeposits;
317
+ }
318
+ );
319
+
320
+ // src/relayer/relayer-client.ts
321
+ var RelayerError = class extends Error {
322
+ };
323
+ var RelayerClient = class {
324
+ fetch;
325
+ constructor({ network: { fetch: fetch2 } }) {
326
+ this.fetch = fetch2;
327
+ }
328
+ async getStatus(hostname) {
329
+ const statusResponse = await this.fetch(`https://${hostname}/status`);
330
+ return statusResponse.json();
331
+ }
332
+ async withdraw(relayerUrl, body) {
333
+ const response = await this.fetch(`${relayerUrl}v1/tornadoWithdraw`, {
334
+ method: "POST",
335
+ headers: { "Content-Type": "application/json" },
336
+ body: JSON.stringify(body)
337
+ });
338
+ if (!response.ok) {
339
+ const { error } = await response.json();
340
+ throw new RelayerError(error);
341
+ }
342
+ return response.json();
343
+ }
344
+ };
345
+
346
+ // src/config.ts
347
+ var E_ADDRESS = "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
348
+ var E_ADDRESS_BIGINT = BigInt(E_ADDRESS);
349
+ var TornadoPaymasterConfigs = {
350
+ 11155111: {
351
+ bundlerUrl: "https://public.pimlico.io/v2/11155111/rpc",
352
+ entryPointAddress: "0x4337084D9E255Ff0702461CF8895CE9E3b5Ff108",
353
+ paymasterAddress: "0xA0e2667AcdECb9bCDa6Fcf87638a962e2E9dB20D",
354
+ poolsAccountsMap: {
355
+ "0x8c4a04d872a6c1be37964a21ba3a138525dff50b": "0x8A70E3dE493B8599447013f08d8b35719C8ED0ec",
356
+ "0x8cc930096b4df705a007c4a039bdfa1320ed2508": "0x191e3De3cF7f61de7EbEf4Bf45cfBA4f652d1C6e",
357
+ "0x6921fd1a97441dd603a997ed6ddf388658daf754": "0x060d053A6595630851ab873d6C95401b0C964d26"
358
+ }
359
+ },
360
+ 1: {}
361
+ };
362
+ var TornadoCashConfigs = {
363
+ 1: {
364
+ ensSubdomainKey: "mainnet-tornado",
365
+ instanceRegistry: {
366
+ address: 0xB20c66C4DE72433F3cE747b58B86830c459CA911n,
367
+ deploymentBlock: 14173395n
368
+ },
369
+ relayerRegistry: {
370
+ address: 0x58E8dCC13BE9780fC42E8723D8EaD4CF46943dF2n,
371
+ deploymentBlock: 14173129n
372
+ },
373
+ aggregator: {
374
+ address: 0xE8F47A78A6D52D317D0D2FFFac56739fE14D1b49n
375
+ },
376
+ weth: 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2n,
377
+ uniswapQuoterV2: 0x61fFE014bA17989E743c5F6cB21bF9697530B21en
378
+ },
379
+ 11155111: {
380
+ ensSubdomainKey: "sepolia-tornado",
381
+ instanceRegistry: {
382
+ address: 0x4e69fD587118dFb64957d18654E3894118E9B1BFn,
383
+ deploymentBlock: 5594611n
384
+ },
385
+ relayerRegistry: {
386
+ address: 0xD6663593E71e4916eCb6f6606e1A6FbfA1634ffAn,
387
+ deploymentBlock: 5594660n
388
+ },
389
+ aggregator: {
390
+ address: 0x4088712AC9fad39ea133cdb9130E465d235e9642n
391
+ },
392
+ weth: 0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14n,
393
+ uniswapQuoterV2: 0xEd1f6473345F45b75F8179591dd5bA1888cf2FB3n
394
+ }
395
+ };
396
+
397
+ // src/utils/circuit-loader.ts
398
+ async function loadCircuitFiles(circuitUrl, provingKeyUrl) {
399
+ const [circuitRes, provingKeyRes] = await Promise.all([
400
+ fetch(circuitUrl),
401
+ fetch(provingKeyUrl)
402
+ ]);
403
+ const [circuitText, provingKey] = await Promise.all([
404
+ circuitRes.text(),
405
+ provingKeyRes.arrayBuffer()
406
+ ]);
407
+ return { circuitText, provingKey };
408
+ }
409
+
410
+ // src/utils/default-artifacts-loader.ts
411
+ var TC_CIRCUIT_URL = "https://raw.githubusercontent.com/tornadocash/tornado-cli/refs/heads/master/build/circuits/tornado.json";
412
+ var TC_PROVING_KEY_URL = "https://github.com/tornadocash/tornado-cli/raw/refs/heads/master/build/circuits/tornadoProvingKey.bin";
413
+ var defaultArtifactsLoader = () => loadCircuitFiles(TC_CIRCUIT_URL, TC_PROVING_KEY_URL);
414
+
415
+ // src/plugin/base.ts
416
+ var TornadoCashProtocol = class {
417
+ constructor(host, {
418
+ accountIndex = 0,
419
+ initialState = async () => ({}),
420
+ protocolConfig,
421
+ artifactsLoader = defaultArtifactsLoader,
422
+ stateManagerWorkerUrl,
423
+ relayerConfig,
424
+ paymasterConfig = TornadoPaymasterConfigs,
425
+ relayerClientFactory = () => new RelayerClient(host)
426
+ }) {
427
+ this.host = host;
428
+ this.stateManager = (async () => {
429
+ const { remote, onError } = loadStateManagerWorker(stateManagerWorkerUrl);
430
+ const workerReady = new Promise((_resolve, reject) => {
431
+ onError((err) => {
432
+ console.error("[worker crash]", err);
433
+ reject(err);
434
+ });
435
+ });
436
+ await Promise.race([
437
+ remote.init(
438
+ proxy(host.provider),
439
+ proxy(relayerClientFactory()),
440
+ proxy(host.keystore),
441
+ proxy(host.storage),
442
+ proxy(initialState),
443
+ proxy(artifactsLoader),
444
+ { protocolConfig, accountIndex, relayerConfig, paymasterConfig }
445
+ ),
446
+ workerReady
447
+ ]);
448
+ return {
449
+ sync: () => remote.sync(),
450
+ getBalances: ((assets) => remote.getBalances(assets)),
451
+ getDepositPayload: (params) => remote.getDepositPayload(params),
452
+ getWithdrawalPayloads: (params) => remote.getWithdrawalPayloads(params),
453
+ dumpState: (() => remote.dumpState())
454
+ };
455
+ })();
456
+ }
457
+ host;
458
+ stateManager;
459
+ instanceId = () => Promise.resolve("0x1");
460
+ /**
461
+ * Only process supported assets or error out?
462
+ * Returns the balances of the requested assets.
463
+ * The assets retain the provided order. If an asset is not supported its balance will be 0
464
+ */
465
+ async balance(assets = []) {
466
+ const stateManager = await this.stateManager;
467
+ await stateManager.sync();
468
+ const parsedDesiredAssets = assets.map(({ contract }) => {
469
+ const parsedAddress = BigInt(contract);
470
+ return parsedAddress === E_ADDRESS_BIGINT ? 0n : parsedAddress;
471
+ });
472
+ const balances = await stateManager.getBalances(
473
+ assets.length > 0 ? parsedDesiredAssets : void 0
474
+ );
475
+ const actuallySelectedAssets = assets.length > 0 ? assets.map((a) => a.contract) : [...balances.keys()].map((a) => addressToHex(a));
476
+ return actuallySelectedAssets.map((assetAddress, index) => {
477
+ const parsedSelectedAsset = BigInt(actuallySelectedAssets[index]);
478
+ const balance = balances.get(parsedSelectedAsset === E_ADDRESS_BIGINT ? 0n : parsedSelectedAsset) || 0n;
479
+ const asset = {
480
+ contract: assetAddress,
481
+ __type: "erc20"
482
+ };
483
+ return {
484
+ asset,
485
+ amount: balance
486
+ };
487
+ });
488
+ }
489
+ async prepareShield(assets, options) {
490
+ const { asset, amount } = assets;
491
+ const strategy = typeof options === "string" ? 1 /* MinFee */ : options?.strategy || 1 /* MinFee */;
492
+ const stateManager = await this.stateManager;
493
+ await stateManager.sync();
494
+ const parsedAsset = BigInt(asset.contract);
495
+ const tx = await stateManager.getDepositPayload({
496
+ asset: parsedAsset === E_ADDRESS_BIGINT ? 0n : parsedAsset,
497
+ amount,
498
+ strategy
499
+ });
500
+ return { txns: tx };
501
+ }
502
+ async prepareUnshield(assets, to, options) {
503
+ const { asset, amount } = assets;
504
+ const parsedAsset = BigInt(asset.contract || E_ADDRESS_BIGINT);
505
+ const stateManager = await this.stateManager;
506
+ await stateManager.sync();
507
+ const baseParams = {
508
+ asset: parsedAsset === E_ADDRESS_BIGINT ? 0n : parsedAsset,
509
+ amount,
510
+ recipient: BigInt(to)
511
+ };
512
+ let withdrawals;
513
+ if (options && options.mode === "paymaster") {
514
+ withdrawals = await stateManager.getWithdrawalPayloads({
515
+ ...baseParams,
516
+ mode: "paymaster",
517
+ delegation: options.delegation
518
+ });
519
+ } else {
520
+ withdrawals = await stateManager.getWithdrawalPayloads({
521
+ ...baseParams,
522
+ mode: "relayer",
523
+ preferredRelayersEns: options?.preferredRelayersEns
524
+ });
525
+ }
526
+ return {
527
+ __type: "privateOperation",
528
+ withdrawals
529
+ };
530
+ }
531
+ async sync() {
532
+ const stateManager = await this.stateManager;
533
+ return stateManager.sync();
534
+ }
535
+ async dumpState() {
536
+ const stateManager = await this.stateManager;
537
+ return stateManager.dumpState();
538
+ }
539
+ };
540
+
541
+ // src/paymaster/fee.ts
542
+ var ERC20_TRANSFER_GAS = 100000n;
543
+ var baseGasUnits = {
544
+ preVerificationGas: 80000n,
545
+ verificationGasLimit: 50000n,
546
+ callGasLimit: 300000n,
547
+ paymasterVerificationGasLimit: 350000n,
548
+ paymasterPostOpGasLimit: 10000n
549
+ };
550
+ function reasonableGasUnits(isERC20) {
551
+ if (!isERC20) return baseGasUnits;
552
+ return {
553
+ ...baseGasUnits,
554
+ callGasLimit: baseGasUnits.callGasLimit + ERC20_TRANSFER_GAS
555
+ };
556
+ }
557
+ async function signDelegationAuthorization({
558
+ privateKey,
559
+ accountAddress,
560
+ chainId,
561
+ nonce
562
+ }) {
563
+ const account = privateKeyToAccount(privateKey);
564
+ const authorization = await account.signAuthorization({
565
+ contractAddress: accountAddress,
566
+ chainId,
567
+ nonce
568
+ });
569
+ return { authorization, senderAddress: account.address };
570
+ }
571
+
572
+ // src/paymaster/paymaster-broadcaster.ts
573
+ var PaymasterBroadcaster = class _PaymasterBroadcaster {
574
+ constructor(provider, options) {
575
+ this.provider = provider;
576
+ this.options = options;
577
+ }
578
+ provider;
579
+ options;
580
+ async broadcast(withdrawals) {
581
+ const chainId = Number(await this.provider.getChainId());
582
+ const {
583
+ poolsAccountsMap: rawPoolsAccountsMap,
584
+ bundlerUrl,
585
+ paymasterAddress,
586
+ entryPointAddress
587
+ } = this.options[chainId];
588
+ const poolAcountsMap = new Map(
589
+ Object.entries(rawPoolsAccountsMap).map(([poolAccount, tornadoAccount]) => [
590
+ BigInt(poolAccount),
591
+ tornadoAccount
592
+ ])
593
+ );
594
+ const results = await Promise.allSettled(
595
+ withdrawals.map((w) => _PaymasterBroadcaster.broadcastOne({
596
+ ...w,
597
+ bundlerUrl,
598
+ paymasterAddress,
599
+ entryPointAddress,
600
+ accountAddress: poolAcountsMap.get(w.poolAddress)
601
+ }, this.provider))
602
+ );
603
+ const failed = results.filter((r) => r.status === "rejected");
604
+ if (failed.length > 0) {
605
+ console.warn(
606
+ `Some paymaster withdrawals failed.`,
607
+ failed.map((e) => e.reason).join("\n")
608
+ );
609
+ }
610
+ return results.filter((r) => r.status === "fulfilled").map((r) => r.value);
611
+ }
612
+ static async broadcastOne(withdrawal, provider) {
613
+ const {
614
+ proof: { proof: proofHex, args: proofArgs },
615
+ paymasterAddress,
616
+ entryPointAddress,
617
+ bundlerUrl,
618
+ accountAddress,
619
+ isERC20
620
+ } = withdrawal;
621
+ const [root, nullifierHash, recipient, _paymasterAddress, feeHex] = proofArgs;
622
+ if (BigInt(paymasterAddress) !== BigInt(_paymasterAddress)) {
623
+ throw new Error(`relayer must be paymaster when using the 4337 paymaster flow: ${paymasterAddress} != ${_paymasterAddress}`);
624
+ }
625
+ const delegation = withdrawal.delegation ?? await _PaymasterBroadcaster.generateRandomDelegation(accountAddress, provider);
626
+ const { senderAddress, authorization } = delegation;
627
+ const bundlerClient = new BundlerClient(bundlerUrl, entryPointAddress);
628
+ const { standard: { maxFeePerGas, maxPriorityFeePerGas } } = await bundlerClient.getUserOperationGasPrice();
629
+ let gas;
630
+ {
631
+ gas = {
632
+ type: "manual",
633
+ ...reasonableGasUnits(isERC20),
634
+ maxFeePerGas,
635
+ maxPriorityFeePerGas
636
+ };
637
+ }
638
+ const op = await new TornadoBuilder(senderAddress).withPaymaster(paymasterAddress).withAuthorization(authorization).withWithdraw(
639
+ proofHex,
640
+ root,
641
+ nullifierHash,
642
+ recipient,
643
+ _paymasterAddress,
644
+ BigInt(feeHex)
645
+ ).withGas(gas).build(provider, bundlerClient);
646
+ const userOpHash = await bundlerClient.sendUserOperation(op);
647
+ await bundlerClient.waitForUserOperationReceipt(userOpHash);
648
+ return { userOpHash };
649
+ }
650
+ static async generateRandomDelegation(accountAddress, provider) {
651
+ const privateKey = generatePrivateKey();
652
+ const chainId = Number(await provider.getChainId());
653
+ return signDelegationAuthorization({
654
+ privateKey,
655
+ accountAddress,
656
+ chainId,
657
+ nonce: 0
658
+ });
659
+ }
660
+ };
661
+
662
+ // src/plugin/broadcaster.ts
663
+ var TornadoCashBroadcaster = class {
664
+ relayerClient;
665
+ provider;
666
+ paymasterBroadcaster;
667
+ constructor({
668
+ host,
669
+ relayerClientFactory = () => new RelayerClient({ network: host.network }),
670
+ paymasterConfig,
671
+ paymasterClientFactory = () => new PaymasterBroadcaster(host.provider, paymasterConfig)
672
+ }) {
673
+ this.provider = host.provider;
674
+ this.relayerClient = relayerClientFactory();
675
+ this.paymasterBroadcaster = paymasterClientFactory();
676
+ }
677
+ async broadcast({
678
+ withdrawals
679
+ }) {
680
+ const relayerWithdrawals = withdrawals.filter((w) => w.mode === "relayer");
681
+ const paymasterWithdrawals = withdrawals.filter((w) => w.mode === "paymaster");
682
+ const [relayerResults, paymasterResults] = await Promise.all([
683
+ this.broadcastViaRelayer(relayerWithdrawals),
684
+ paymasterWithdrawals.length > 0 ? this.broadcastViaPaymaster(paymasterWithdrawals) : Promise.resolve([])
685
+ ]);
686
+ const normalizedPaymasterResults = paymasterResults.map(
687
+ ({ userOpHash }) => ({ id: userOpHash })
688
+ );
689
+ return [...relayerResults, ...normalizedPaymasterResults];
690
+ }
691
+ async broadcastViaRelayer(withdrawals) {
692
+ if (withdrawals.length === 0) return [];
693
+ const successes = [];
694
+ const failures = [];
695
+ for (const { proof: { args, proof }, poolAddress, relayerUrl } of withdrawals) {
696
+ try {
697
+ successes.push(await this.relayerClient.withdraw(relayerUrl, {
698
+ proof,
699
+ args,
700
+ contract: addressToHex(poolAddress)
701
+ }));
702
+ } catch (error) {
703
+ failures.push(error);
704
+ }
705
+ }
706
+ if (failures.length > 0) {
707
+ console.warn(`Some withdrawals failed.`, failures.map((e) => e.message).join("\n"));
708
+ }
709
+ return successes;
710
+ }
711
+ async broadcastViaPaymaster(withdrawals) {
712
+ return this.paymasterBroadcaster.broadcast(withdrawals);
713
+ }
714
+ };
715
+
716
+ // src/v1/factory.ts
717
+ var createTCBroadcaster = (host, params) => {
718
+ const paymasterConfig = params?.paymasterConfig || TornadoPaymasterConfigs;
719
+ return new TornadoCashBroadcaster({ host, ...params, paymasterConfig });
720
+ };
721
+ var createTCPlugin = ((host, params) => new TornadoCashProtocol(host, params));
722
+ function pedersenHash(msg) {
723
+ const hash = pedersenHash$1(msg);
724
+ return Point.decode(hash).x;
725
+ }
726
+
727
+ // src/account/keys.ts
728
+ var TORNADO_CASH_PATH = "m/29795'/1'";
729
+ var MASK_248 = (1n << 248n) - 1n;
730
+ function coalesceSecret({ secret, chainId, poolAddress }) {
731
+ return bytesToNumberLE(
732
+ keccak_256(
733
+ concatBytes(
734
+ numberToBytesLE(BigInt(secret), 32),
735
+ numberToBytesLE(chainId, 8),
736
+ numberToBytesLE(poolAddress, 20)
737
+ )
738
+ )
739
+ );
740
+ }
741
+ async function SecretManager({
742
+ host: { keystore },
743
+ accountIndex = 0
744
+ }) {
745
+ const deriveSecrets = async ({ chainId, poolAddress, depositIndex }) => {
746
+ const saltSecretRaw = await Promise.resolve(keystore.deriveAt(tcPath({ accountIndex, secretType: "salt", depositIndex })));
747
+ const nullifierSecretRaw = await Promise.resolve(keystore.deriveAt(tcPath({ accountIndex, secretType: "nullifier", depositIndex })));
748
+ const salt = coalesceSecret({ secret: saltSecretRaw, chainId, poolAddress }) & MASK_248;
749
+ const nullifier = coalesceSecret({ secret: nullifierSecretRaw, chainId, poolAddress }) & MASK_248;
750
+ const nullifierBytes = numberToBytesLE(nullifier, 31);
751
+ const preimage = new Uint8Array(62);
752
+ preimage.set(nullifierBytes, 0);
753
+ preimage.set(numberToBytesLE(salt, 31), 31);
754
+ const commitment = pedersenHash(preimage);
755
+ const nullifierHash = pedersenHash(nullifierBytes);
756
+ return { nullifier, salt, commitment, nullifierHash };
757
+ };
758
+ const deriveEphemeralSigner = async ({ chainId, poolAddress, depositIndex }) => {
759
+ const path = tcPath({ accountIndex, secretType: "signer", depositIndex });
760
+ const raw = await Promise.resolve(keystore.deriveAt(path));
761
+ const coalesced = coalesceSecret({ secret: raw, chainId, poolAddress });
762
+ return `0x${coalesced.toString(16).padStart(64, "0")}`;
763
+ };
764
+ return {
765
+ getDepositSecrets: (params) => deriveSecrets(params),
766
+ deriveEphemeralSigner
767
+ };
768
+ }
769
+ function tcPath({ accountIndex, secretType, depositIndex }) {
770
+ const _secretType = {
771
+ "nullifier": 0,
772
+ "salt": 1,
773
+ "signer": 2
774
+ }[secretType];
775
+ return `${TORNADO_CASH_PATH}/${accountIndex}'/${_secretType}'/${depositIndex}'`;
776
+ }
777
+
778
+ export { DepositStrategy, E_ADDRESS, SecretManager, TornadoCashConfigs, TornadoCashProtocol, createTCBroadcaster, createTCPlugin };