@kohaku-eth/tornado-cash 0.0.2-alpha.15 → 0.0.2-alpha.17

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.d.ts CHANGED
@@ -33,6 +33,15 @@ type DeriveDepositSecretParams = BaseDeriveSecretParams & {
33
33
  interface ISecretManager {
34
34
  getDepositSecrets: (params: DeriveDepositSecretParams) => Promise<Secret>;
35
35
  deriveEphemeralSigner: (params: DeriveDepositSecretParams) => Promise<`0x${string}`>;
36
+ /**
37
+ * Derives a delegator key from a classical wallet BIP-32 `path`, WITHOUT the
38
+ * `coalesceSecret` privacy transform, so it resolves to a recoverable,
39
+ * classical Ethereum address. Used for the tail-call batch delegator when the
40
+ * integrator supplies a wallet path (see `paymasterWithdrawThunk`).
41
+ */
42
+ deriveDelegatorSigner: (params: {
43
+ path: string;
44
+ }) => Promise<`0x${string}`>;
36
45
  }
37
46
  interface SecretManagerParams {
38
47
  host: Pick<Host, 'keystore'>;
@@ -352,6 +361,24 @@ declare enum DepositStrategy {
352
361
  MinFee = 1
353
362
  }
354
363
 
364
+ /**
365
+ * How the batch delegator for a tail-call paymaster withdrawal is produced.
366
+ * Only honored when `tailCalls` are present (see `paymasterWithdrawThunk`).
367
+ *
368
+ * - `deterministic` (default): recoverable. With `path`, the delegator is a
369
+ * classical wallet-path address; without `path`, an ephemeral address keyed by
370
+ * the batch's first deposit.
371
+ * - `random`: a throwaway, unrecoverable EOA — an explicit opt-out of
372
+ * recoverability. Never the default, because in the tail-call path the
373
+ * delegator holds the withdrawn funds.
374
+ */
375
+ type DelegationConfig = {
376
+ mode: 'deterministic';
377
+ path?: string;
378
+ } | {
379
+ mode: 'random';
380
+ };
381
+
355
382
  interface SerializedAuth {
356
383
  address: Address$1;
357
384
  chainId: Hex;
@@ -410,12 +437,6 @@ interface IPaymasterBroadcasterClient {
410
437
  broadcast(withdrawals: IPaymasterWithdrawalPayload[]): Promise<PaymasterBroadcastResult[]>;
411
438
  }
412
439
 
413
- type DelegationConfig = {
414
- mode: 'deterministic';
415
- path?: string;
416
- } | {
417
- mode: 'random';
418
- };
419
440
  type StringAddress = `0x${string}`;
420
441
  interface IPaymasterConfig {
421
442
  paymasterAddress: StringAddress;
package/dist/index.js CHANGED
@@ -764,9 +764,13 @@ async function SecretManager({
764
764
  const coalesced = coalesceSecret({ secret: raw, chainId, poolAddress });
765
765
  return `0x${coalesced.toString(16).padStart(64, "0")}`;
766
766
  };
767
+ const deriveDelegatorSigner = async ({ path }) => {
768
+ return Promise.resolve(keystore.deriveAt(path));
769
+ };
767
770
  return {
768
771
  getDepositSecrets: (params) => deriveSecrets(params),
769
- deriveEphemeralSigner
772
+ deriveEphemeralSigner,
773
+ deriveDelegatorSigner
770
774
  };
771
775
  }
772
776
  function tcPath({ accountIndex, secretType, depositIndex }) {
@@ -17281,9 +17281,13 @@ async function SecretManager({
17281
17281
  const coalesced = coalesceSecret({ secret: raw, chainId, poolAddress });
17282
17282
  return `0x${coalesced.toString(16).padStart(64, "0")}`;
17283
17283
  };
17284
+ const deriveDelegatorSigner = async ({ path }) => {
17285
+ return Promise.resolve(keystore.deriveAt(path));
17286
+ };
17284
17287
  return {
17285
17288
  getDepositSecrets: (params) => deriveSecrets(params),
17286
- deriveEphemeralSigner
17289
+ deriveEphemeralSigner,
17290
+ deriveDelegatorSigner
17287
17291
  };
17288
17292
  }
17289
17293
  function tcPath({ accountIndex, secretType, depositIndex }) {
@@ -22190,6 +22194,15 @@ var withdrawThunk = createAsyncThunk("withdraw/executeWithdrawals", async ({
22190
22194
  return getWithdrawalsProofs();
22191
22195
  });
22192
22196
 
22197
+ // ../provider/dist/index.js
22198
+ var createTx = (address, payload, value = BigInt(0)) => {
22199
+ return {
22200
+ to: address,
22201
+ data: payload,
22202
+ value
22203
+ };
22204
+ };
22205
+
22193
22206
  // ../../node_modules/.pnpm/viem@2.48.4_typescript@5.9.3_zod@4.4.3/node_modules/viem/_esm/index.js
22194
22207
  init_exports();
22195
22208
  init_encodeAbiParameters2();
@@ -22364,8 +22377,14 @@ function privateKeyToAccount(privateKey, options = {}) {
22364
22377
  };
22365
22378
  }
22366
22379
 
22380
+ // src/state/thunks/paymasterWithdrawThunk.ts
22381
+ init_esm();
22382
+
22367
22383
  // src/paymaster/fee.ts
22368
22384
  var ERC20_TRANSFER_GAS = 100000n;
22385
+ var PER_DIRECT_WITHDRAW_GAS = 400000n;
22386
+ var PER_DIRECT_WITHDRAW_GAS_ERC20 = 500000n;
22387
+ var FORWARD_GAS = 60000n;
22369
22388
  var baseGasUnits = {
22370
22389
  preVerificationGas: 80000n,
22371
22390
  verificationGasLimit: 50000n,
@@ -22380,6 +22399,15 @@ function reasonableGasUnits(isERC20) {
22380
22399
  paymasterVerificationGasLimit: baseGasUnits.paymasterVerificationGasLimit + ERC20_TRANSFER_GAS
22381
22400
  };
22382
22401
  }
22402
+ function reasonableGasUnitsForBatch(isERC20, extraWithdrawals, hasUserTailCalls) {
22403
+ const base = reasonableGasUnits(isERC20);
22404
+ const perWithdraw = isERC20 ? PER_DIRECT_WITHDRAW_GAS_ERC20 : PER_DIRECT_WITHDRAW_GAS;
22405
+ const executionTail = hasUserTailCalls ? base.callGasLimit : FORWARD_GAS;
22406
+ return {
22407
+ ...base,
22408
+ callGasLimit: BigInt(extraWithdrawals) * perWithdraw + executionTail
22409
+ };
22410
+ }
22383
22411
  function computeMinimumViableFee(reasonableGasUnits2, maxFeePerGas) {
22384
22412
  const baseFeeMultiplier = 1.2;
22385
22413
  const decimals = baseFeeMultiplier.toString().split(".")[1]?.length ?? 0;
@@ -22387,10 +22415,6 @@ function computeMinimumViableFee(reasonableGasUnits2, maxFeePerGas) {
22387
22415
  const multiply = (base) => base * BigInt(Math.ceil(baseFeeMultiplier * denominator)) / BigInt(denominator);
22388
22416
  const requiredGas = reasonableGasUnits2.verificationGasLimit + reasonableGasUnits2.callGasLimit + reasonableGasUnits2.paymasterVerificationGasLimit + reasonableGasUnits2.preVerificationGas + reasonableGasUnits2.paymasterPostOpGasLimit;
22389
22417
  const requiredPrefund = requiredGas * maxFeePerGas;
22390
- console.log("totalGasUnits", requiredGas);
22391
- console.log("maxFeePerGas", maxFeePerGas);
22392
- console.log("requiredPrefund", requiredPrefund);
22393
- console.log("multiply", 1.2);
22394
22418
  return multiply(requiredPrefund);
22395
22419
  }
22396
22420
 
@@ -24087,11 +24111,21 @@ async function getUserOperationGasPrice(client) {
24087
24111
  fast: parse(result.fast)
24088
24112
  };
24089
24113
  }
24090
- function ephemeralSenderAddress(privateKey) {
24091
- return privateKeyToAccount(privateKey).address;
24114
+ async function estimateUserOperationGas2(client, op, entryPoint) {
24115
+ const result = await client.request({
24116
+ method: "eth_estimateUserOperationGas",
24117
+ params: [op, entryPoint]
24118
+ });
24119
+ return {
24120
+ callGasLimit: BigInt(result.callGasLimit),
24121
+ verificationGasLimit: BigInt(result.verificationGasLimit),
24122
+ preVerificationGas: BigInt(result.preVerificationGas),
24123
+ paymasterVerificationGasLimit: BigInt(result.paymasterVerificationGasLimit ?? 0),
24124
+ paymasterPostOpGasLimit: BigInt(result.paymasterPostOpGasLimit ?? 0)
24125
+ };
24092
24126
  }
24093
24127
  async function buildSignedTornadoUserOp({
24094
- privateKey,
24128
+ signer,
24095
24129
  chainId,
24096
24130
  paymasterAddress,
24097
24131
  paymasterData,
@@ -24101,7 +24135,7 @@ async function buildSignedTornadoUserOp({
24101
24135
  tailCalls = async () => [],
24102
24136
  nonce = 0n
24103
24137
  }) {
24104
- const owner = privateKeyToAccount(privateKey);
24138
+ const owner = signer;
24105
24139
  const calls = await tailCalls(owner.address);
24106
24140
  let callData = "0x";
24107
24141
  if (calls.length === 1) {
@@ -24174,6 +24208,12 @@ function serializeAuth(auth) {
24174
24208
  }
24175
24209
 
24176
24210
  // src/state/thunks/paymasterWithdrawThunk.ts
24211
+ var BIP32_PATH = /^m(\/\d+'?)+$/;
24212
+ function assertValidDelegatorPath(path) {
24213
+ if (!BIP32_PATH.test(path)) {
24214
+ throw new Error(`Invalid delegation path "${path}": expected a BIP-32 path like m/44'/60'/0'/0/0`);
24215
+ }
24216
+ }
24177
24217
  var paymasterWithdrawThunk = createAsyncThunk("withdraw/executePaymasterWithdrawals", async ({
24178
24218
  dataService,
24179
24219
  assetAddress,
@@ -24214,43 +24254,23 @@ var paymasterWithdrawThunk = createAsyncThunk("withdraw/executePaymasterWithdraw
24214
24254
  );
24215
24255
  const bundlerClient = createPaymasterBundlerClient(bundlerUrl);
24216
24256
  const { standard: { maxFeePerGas, maxPriorityFeePerGas } } = await getUserOperationGasPrice(bundlerClient);
24217
- const gasUnits = reasonableGasUnits(poolInfo.isERC20);
24218
- const ethFee = computeMinimumViableFee(gasUnits, maxFeePerGas);
24219
- const fee = poolInfo.isERC20 ? await dataService.quoteWeiInToken(BigInt(paymasterAddress), poolInfo.asset, ethFee) : ethFee;
24220
24257
  const relayerAddress = BigInt(paymasterAddress);
24221
24258
  const bigintChainId = await dataService.getChainId();
24222
24259
  const { recipient: originalRecipient, ...restWithoutRecipient } = rest;
24223
- const sharedPrivateKey = tailCalls ? generatePrivateKey() : null;
24224
- const proofOutputs = [];
24225
- const userOperations = [];
24226
- for (let i = 0; i < deposits.length; i++) {
24227
- const deposit = deposits[i];
24228
- const isLast = i === deposits.length - 1;
24229
- const privateKey = sharedPrivateKey ?? (delegation?.mode === "deterministic" ? await secretManager.deriveEphemeralSigner({
24230
- depositIndex: deposit.index,
24231
- chainId: bigintChainId,
24232
- poolAddress: deposit.pool
24233
- }) : generatePrivateKey());
24234
- const recipient = sharedPrivateKey ? BigInt(ephemeralSenderAddress(privateKey)) : originalRecipient;
24235
- const effectiveTailCalls = isLast ? tailCalls : void 0;
24236
- const gas = { ...gasUnits, callGasLimit: effectiveTailCalls ? gasUnits.callGasLimit : 0n };
24237
- const withdrawResultAction = await dispatch(
24238
- withdrawalsProofThunk({
24239
- ...restWithoutRecipient,
24240
- recipient,
24241
- deposit,
24242
- relayerAddress,
24243
- fee
24244
- })
24245
- );
24246
- const proof = { ...unwrapResult(withdrawResultAction), poolAddress: deposit.pool };
24247
- proofOutputs.push(proof);
24248
- const { poolAddress, ...proofArgs } = proof;
24249
- const [root, nullifierHash, proofRecipient, relayerArg, feeArg, refundArg] = proofArgs.args;
24250
- const paymasterData = encodePaymasterData(
24260
+ const quoteFee = async (ethFee) => poolInfo.isERC20 ? dataService.quoteWeiInToken(BigInt(paymasterAddress), poolInfo.asset, ethFee) : ethFee;
24261
+ const prove = async (deposit, recipient, relayer, depositFee) => unwrapResult(await dispatch(withdrawalsProofThunk({
24262
+ ...restWithoutRecipient,
24263
+ recipient,
24264
+ deposit,
24265
+ relayerAddress: relayer,
24266
+ fee: depositFee
24267
+ })));
24268
+ const buildPaymasterData = (poolAddress, proof) => {
24269
+ const [root, nullifierHash, proofRecipient, relayerArg, feeArg, refundArg] = proof.args;
24270
+ return encodePaymasterData(
24251
24271
  poolAcountsMap.get(poolAddress),
24252
24272
  encodeTornadoAdapterData(
24253
- proofArgs.proof,
24273
+ proof.proof,
24254
24274
  root,
24255
24275
  nullifierHash,
24256
24276
  proofRecipient,
@@ -24259,43 +24279,130 @@ var paymasterWithdrawThunk = createAsyncThunk("withdraw/executePaymasterWithdraw
24259
24279
  BigInt(refundArg)
24260
24280
  )
24261
24281
  );
24262
- userOperations.push(
24263
- await buildSignedTornadoUserOp({
24264
- privateKey,
24265
- chainId,
24266
- paymasterAddress,
24267
- paymasterData,
24268
- gas,
24269
- maxFeePerGas,
24270
- maxPriorityFeePerGas,
24271
- tailCalls: effectiveTailCalls,
24272
- // When sharing one ephemeral key across multiple deposits each userOp
24273
- // needs a distinct nonce (0, 1, 2 …) on the shared sender.
24274
- nonce: sharedPrivateKey ? BigInt(i) : 0n
24275
- })
24276
- );
24282
+ };
24283
+ const ephemeralSigner = async (deposit) => privateKeyToAccount(await secretManager.deriveEphemeralSigner({
24284
+ depositIndex: deposit.index,
24285
+ chainId: bigintChainId,
24286
+ poolAddress: deposit.pool
24287
+ }));
24288
+ const resolveBatchDelegator = async () => {
24289
+ if (delegation?.mode === "random") return privateKeyToAccount(generatePrivateKey());
24290
+ if (delegation?.mode === "deterministic" && delegation.path) {
24291
+ assertValidDelegatorPath(delegation.path);
24292
+ return privateKeyToAccount(await secretManager.deriveDelegatorSigner({ path: delegation.path }));
24293
+ }
24294
+ return ephemeralSigner(deposits[0]);
24295
+ };
24296
+ const resolveIndependentSigner = (deposit) => delegation?.mode === "deterministic" ? ephemeralSigner(deposit) : Promise.resolve(privateKeyToAccount(generatePrivateKey()));
24297
+ const needsConsolidation = deposits.length > 1 || tailCalls != null;
24298
+ if (!needsConsolidation) {
24299
+ const deposit = deposits[0];
24300
+ const signer2 = await resolveIndependentSigner(deposit);
24301
+ const gasUnits = reasonableGasUnits(poolInfo.isERC20);
24302
+ const fee2 = await quoteFee(computeMinimumViableFee(gasUnits, maxFeePerGas));
24303
+ const proof = await prove(deposit, originalRecipient, relayerAddress, fee2);
24304
+ const userOperation2 = await buildSignedTornadoUserOp({
24305
+ signer: signer2,
24306
+ chainId,
24307
+ paymasterAddress,
24308
+ paymasterData: buildPaymasterData(deposit.pool, proof),
24309
+ gas: { ...gasUnits, callGasLimit: 0n },
24310
+ maxFeePerGas,
24311
+ maxPriorityFeePerGas,
24312
+ nonce: 0n
24313
+ });
24314
+ return [{
24315
+ mode: "paymaster",
24316
+ proof,
24317
+ poolAddress: deposit.pool,
24318
+ isERC20: poolInfo.isERC20,
24319
+ paymasterAddress,
24320
+ entryPointAddress,
24321
+ bundlerUrl,
24322
+ userOperation: userOperation2
24323
+ }];
24324
+ }
24325
+ const signer = await resolveBatchDelegator();
24326
+ const senderAddress = BigInt(signer.address);
24327
+ const sponsoringDeposit = deposits[0];
24328
+ const directDeposits = deposits.slice(1);
24329
+ const ZERO_ADDRESS = 0n;
24330
+ const directWithdrawCalls = [];
24331
+ for (const deposit of directDeposits) {
24332
+ const proof = await prove(deposit, senderAddress, ZERO_ADDRESS, 0n);
24333
+ const [root, nullifierHash, proofRecipient, relayerArg, feeArg, refundArg] = proof.args;
24334
+ const data = encodeFunctionData({
24335
+ abi: poolAbi,
24336
+ functionName: "withdraw",
24337
+ args: [proof.proof, root, nullifierHash, proofRecipient, relayerArg, BigInt(feeArg), BigInt(refundArg)]
24338
+ });
24339
+ directWithdrawCalls.push(createTx(addressToHex(deposit.pool), data, 0n));
24277
24340
  }
24278
- return proofOutputs.map(({ poolAddress, ...proof }, i) => ({
24341
+ const totalDenomination = deposits.reduce(
24342
+ (sum, d) => sum + pools.get(d.pool).denomination,
24343
+ 0n
24344
+ );
24345
+ const forwardCalls = (fee2) => {
24346
+ const amount2 = totalDenomination - fee2;
24347
+ if (poolInfo.isERC20) {
24348
+ const data = encodeFunctionData({
24349
+ abi: erc20Abi,
24350
+ functionName: "transfer",
24351
+ args: [addressToHex(originalRecipient), amount2]
24352
+ });
24353
+ return [createTx(addressToHex(poolInfo.asset), data, 0n)];
24354
+ }
24355
+ return [createTx(addressToHex(originalRecipient), "0x", amount2)];
24356
+ };
24357
+ const composeTailCalls = (fee2) => async (addr) => [
24358
+ ...directWithdrawCalls,
24359
+ ...tailCalls ? await tailCalls(addr) : forwardCalls(fee2)
24360
+ ];
24361
+ const buildOp = (gas2, fee2, paymasterData) => buildSignedTornadoUserOp({
24362
+ signer,
24363
+ chainId,
24364
+ paymasterAddress,
24365
+ paymasterData,
24366
+ gas: gas2,
24367
+ maxFeePerGas,
24368
+ maxPriorityFeePerGas,
24369
+ tailCalls: composeTailCalls(fee2),
24370
+ nonce: 0n
24371
+ });
24372
+ const baselineGas = reasonableGasUnitsForBatch(poolInfo.isERC20, directDeposits.length, tailCalls != null);
24373
+ let fee = await quoteFee(computeMinimumViableFee(baselineGas, maxFeePerGas));
24374
+ let gas = baselineGas;
24375
+ let sponsoringProof = await prove(sponsoringDeposit, senderAddress, relayerAddress, fee);
24376
+ let userOperation = await buildOp(gas, fee, buildPaymasterData(sponsoringDeposit.pool, sponsoringProof));
24377
+ try {
24378
+ const estimated = await estimateUserOperationGas2(bundlerClient, userOperation, entryPointAddress);
24379
+ const buffer2 = (x) => x * 12n / 10n;
24380
+ const orBaseline = (e, b) => e > 0n ? buffer2(e) : b;
24381
+ const refinedGas = {
24382
+ callGasLimit: orBaseline(estimated.callGasLimit, baselineGas.callGasLimit),
24383
+ verificationGasLimit: orBaseline(estimated.verificationGasLimit, baselineGas.verificationGasLimit),
24384
+ preVerificationGas: orBaseline(estimated.preVerificationGas, baselineGas.preVerificationGas),
24385
+ paymasterVerificationGasLimit: orBaseline(estimated.paymasterVerificationGasLimit, baselineGas.paymasterVerificationGasLimit),
24386
+ paymasterPostOpGasLimit: orBaseline(estimated.paymasterPostOpGasLimit, baselineGas.paymasterPostOpGasLimit)
24387
+ };
24388
+ fee = await quoteFee(computeMinimumViableFee(refinedGas, maxFeePerGas));
24389
+ gas = refinedGas;
24390
+ sponsoringProof = await prove(sponsoringDeposit, senderAddress, relayerAddress, fee);
24391
+ userOperation = await buildOp(gas, fee, buildPaymasterData(sponsoringDeposit.pool, sponsoringProof));
24392
+ } catch {
24393
+ }
24394
+ return [{
24279
24395
  mode: "paymaster",
24280
- proof,
24281
- poolAddress,
24396
+ proof: sponsoringProof,
24397
+ poolAddress: sponsoringDeposit.pool,
24282
24398
  isERC20: poolInfo.isERC20,
24283
24399
  paymasterAddress,
24284
24400
  entryPointAddress,
24285
24401
  bundlerUrl,
24286
- userOperation: userOperations[i]
24287
- }));
24402
+ userOperation
24403
+ }];
24288
24404
  });
24289
24405
 
24290
- // ../provider/dist/index.js
24291
- var createTx = (address, payload, value = BigInt(0)) => {
24292
- return {
24293
- to: address,
24294
- data: payload,
24295
- value
24296
- };
24297
- };
24298
-
24299
24406
  // src/account/tx/shield.ts
24300
24407
  init_esm();
24301
24408
  init_esm();
@@ -16853,9 +16853,13 @@ async function SecretManager({
16853
16853
  const coalesced = coalesceSecret({ secret: raw, chainId, poolAddress });
16854
16854
  return `0x${coalesced.toString(16).padStart(64, "0")}`;
16855
16855
  };
16856
+ const deriveDelegatorSigner = async ({ path }) => {
16857
+ return Promise.resolve(keystore.deriveAt(path));
16858
+ };
16856
16859
  return {
16857
16860
  getDepositSecrets: (params) => deriveSecrets(params),
16858
- deriveEphemeralSigner
16861
+ deriveEphemeralSigner,
16862
+ deriveDelegatorSigner
16859
16863
  };
16860
16864
  }
16861
16865
  function tcPath({ accountIndex, secretType, depositIndex }) {
@@ -21416,6 +21420,15 @@ var withdrawThunk = createAsyncThunk("withdraw/executeWithdrawals", async ({
21416
21420
  return getWithdrawalsProofs();
21417
21421
  });
21418
21422
 
21423
+ // ../provider/dist/index.js
21424
+ var createTx = (address, payload, value = BigInt(0)) => {
21425
+ return {
21426
+ to: address,
21427
+ data: payload,
21428
+ value
21429
+ };
21430
+ };
21431
+
21419
21432
  // ../../node_modules/.pnpm/viem@2.48.4_typescript@5.9.3_zod@4.4.3/node_modules/viem/_esm/index.js
21420
21433
  init_exports();
21421
21434
  init_encodeAbiParameters2();
@@ -21590,8 +21603,14 @@ function privateKeyToAccount(privateKey, options = {}) {
21590
21603
  };
21591
21604
  }
21592
21605
 
21606
+ // src/state/thunks/paymasterWithdrawThunk.ts
21607
+ init_esm();
21608
+
21593
21609
  // src/paymaster/fee.ts
21594
21610
  var ERC20_TRANSFER_GAS = 100000n;
21611
+ var PER_DIRECT_WITHDRAW_GAS = 400000n;
21612
+ var PER_DIRECT_WITHDRAW_GAS_ERC20 = 500000n;
21613
+ var FORWARD_GAS = 60000n;
21595
21614
  var baseGasUnits = {
21596
21615
  preVerificationGas: 80000n,
21597
21616
  verificationGasLimit: 50000n,
@@ -21606,6 +21625,15 @@ function reasonableGasUnits(isERC20) {
21606
21625
  paymasterVerificationGasLimit: baseGasUnits.paymasterVerificationGasLimit + ERC20_TRANSFER_GAS
21607
21626
  };
21608
21627
  }
21628
+ function reasonableGasUnitsForBatch(isERC20, extraWithdrawals, hasUserTailCalls) {
21629
+ const base = reasonableGasUnits(isERC20);
21630
+ const perWithdraw = isERC20 ? PER_DIRECT_WITHDRAW_GAS_ERC20 : PER_DIRECT_WITHDRAW_GAS;
21631
+ const executionTail = hasUserTailCalls ? base.callGasLimit : FORWARD_GAS;
21632
+ return {
21633
+ ...base,
21634
+ callGasLimit: BigInt(extraWithdrawals) * perWithdraw + executionTail
21635
+ };
21636
+ }
21609
21637
  function computeMinimumViableFee(reasonableGasUnits2, maxFeePerGas) {
21610
21638
  const baseFeeMultiplier = 1.2;
21611
21639
  const decimals = baseFeeMultiplier.toString().split(".")[1]?.length ?? 0;
@@ -21613,10 +21641,6 @@ function computeMinimumViableFee(reasonableGasUnits2, maxFeePerGas) {
21613
21641
  const multiply = (base) => base * BigInt(Math.ceil(baseFeeMultiplier * denominator)) / BigInt(denominator);
21614
21642
  const requiredGas = reasonableGasUnits2.verificationGasLimit + reasonableGasUnits2.callGasLimit + reasonableGasUnits2.paymasterVerificationGasLimit + reasonableGasUnits2.preVerificationGas + reasonableGasUnits2.paymasterPostOpGasLimit;
21615
21643
  const requiredPrefund = requiredGas * maxFeePerGas;
21616
- console.log("totalGasUnits", requiredGas);
21617
- console.log("maxFeePerGas", maxFeePerGas);
21618
- console.log("requiredPrefund", requiredPrefund);
21619
- console.log("multiply", 1.2);
21620
21644
  return multiply(requiredPrefund);
21621
21645
  }
21622
21646
 
@@ -23313,11 +23337,21 @@ async function getUserOperationGasPrice(client) {
23313
23337
  fast: parse(result.fast)
23314
23338
  };
23315
23339
  }
23316
- function ephemeralSenderAddress(privateKey) {
23317
- return privateKeyToAccount(privateKey).address;
23340
+ async function estimateUserOperationGas2(client, op, entryPoint) {
23341
+ const result = await client.request({
23342
+ method: "eth_estimateUserOperationGas",
23343
+ params: [op, entryPoint]
23344
+ });
23345
+ return {
23346
+ callGasLimit: BigInt(result.callGasLimit),
23347
+ verificationGasLimit: BigInt(result.verificationGasLimit),
23348
+ preVerificationGas: BigInt(result.preVerificationGas),
23349
+ paymasterVerificationGasLimit: BigInt(result.paymasterVerificationGasLimit ?? 0),
23350
+ paymasterPostOpGasLimit: BigInt(result.paymasterPostOpGasLimit ?? 0)
23351
+ };
23318
23352
  }
23319
23353
  async function buildSignedTornadoUserOp({
23320
- privateKey,
23354
+ signer,
23321
23355
  chainId,
23322
23356
  paymasterAddress,
23323
23357
  paymasterData,
@@ -23327,7 +23361,7 @@ async function buildSignedTornadoUserOp({
23327
23361
  tailCalls = async () => [],
23328
23362
  nonce = 0n
23329
23363
  }) {
23330
- const owner = privateKeyToAccount(privateKey);
23364
+ const owner = signer;
23331
23365
  const calls = await tailCalls(owner.address);
23332
23366
  let callData = "0x";
23333
23367
  if (calls.length === 1) {
@@ -23400,6 +23434,12 @@ function serializeAuth(auth) {
23400
23434
  }
23401
23435
 
23402
23436
  // src/state/thunks/paymasterWithdrawThunk.ts
23437
+ var BIP32_PATH = /^m(\/\d+'?)+$/;
23438
+ function assertValidDelegatorPath(path) {
23439
+ if (!BIP32_PATH.test(path)) {
23440
+ throw new Error(`Invalid delegation path "${path}": expected a BIP-32 path like m/44'/60'/0'/0/0`);
23441
+ }
23442
+ }
23403
23443
  var paymasterWithdrawThunk = createAsyncThunk("withdraw/executePaymasterWithdrawals", async ({
23404
23444
  dataService,
23405
23445
  assetAddress,
@@ -23440,43 +23480,23 @@ var paymasterWithdrawThunk = createAsyncThunk("withdraw/executePaymasterWithdraw
23440
23480
  );
23441
23481
  const bundlerClient = createPaymasterBundlerClient(bundlerUrl);
23442
23482
  const { standard: { maxFeePerGas, maxPriorityFeePerGas } } = await getUserOperationGasPrice(bundlerClient);
23443
- const gasUnits = reasonableGasUnits(poolInfo.isERC20);
23444
- const ethFee = computeMinimumViableFee(gasUnits, maxFeePerGas);
23445
- const fee = poolInfo.isERC20 ? await dataService.quoteWeiInToken(BigInt(paymasterAddress), poolInfo.asset, ethFee) : ethFee;
23446
23483
  const relayerAddress = BigInt(paymasterAddress);
23447
23484
  const bigintChainId = await dataService.getChainId();
23448
23485
  const { recipient: originalRecipient, ...restWithoutRecipient } = rest;
23449
- const sharedPrivateKey = tailCalls ? generatePrivateKey() : null;
23450
- const proofOutputs = [];
23451
- const userOperations = [];
23452
- for (let i = 0; i < deposits.length; i++) {
23453
- const deposit = deposits[i];
23454
- const isLast = i === deposits.length - 1;
23455
- const privateKey = sharedPrivateKey ?? (delegation?.mode === "deterministic" ? await secretManager.deriveEphemeralSigner({
23456
- depositIndex: deposit.index,
23457
- chainId: bigintChainId,
23458
- poolAddress: deposit.pool
23459
- }) : generatePrivateKey());
23460
- const recipient = sharedPrivateKey ? BigInt(ephemeralSenderAddress(privateKey)) : originalRecipient;
23461
- const effectiveTailCalls = isLast ? tailCalls : void 0;
23462
- const gas = { ...gasUnits, callGasLimit: effectiveTailCalls ? gasUnits.callGasLimit : 0n };
23463
- const withdrawResultAction = await dispatch(
23464
- withdrawalsProofThunk({
23465
- ...restWithoutRecipient,
23466
- recipient,
23467
- deposit,
23468
- relayerAddress,
23469
- fee
23470
- })
23471
- );
23472
- const proof = { ...unwrapResult(withdrawResultAction), poolAddress: deposit.pool };
23473
- proofOutputs.push(proof);
23474
- const { poolAddress, ...proofArgs } = proof;
23475
- const [root, nullifierHash, proofRecipient, relayerArg, feeArg, refundArg] = proofArgs.args;
23476
- const paymasterData = encodePaymasterData(
23486
+ const quoteFee = async (ethFee) => poolInfo.isERC20 ? dataService.quoteWeiInToken(BigInt(paymasterAddress), poolInfo.asset, ethFee) : ethFee;
23487
+ const prove = async (deposit, recipient, relayer, depositFee) => unwrapResult(await dispatch(withdrawalsProofThunk({
23488
+ ...restWithoutRecipient,
23489
+ recipient,
23490
+ deposit,
23491
+ relayerAddress: relayer,
23492
+ fee: depositFee
23493
+ })));
23494
+ const buildPaymasterData = (poolAddress, proof) => {
23495
+ const [root, nullifierHash, proofRecipient, relayerArg, feeArg, refundArg] = proof.args;
23496
+ return encodePaymasterData(
23477
23497
  poolAcountsMap.get(poolAddress),
23478
23498
  encodeTornadoAdapterData(
23479
- proofArgs.proof,
23499
+ proof.proof,
23480
23500
  root,
23481
23501
  nullifierHash,
23482
23502
  proofRecipient,
@@ -23485,43 +23505,130 @@ var paymasterWithdrawThunk = createAsyncThunk("withdraw/executePaymasterWithdraw
23485
23505
  BigInt(refundArg)
23486
23506
  )
23487
23507
  );
23488
- userOperations.push(
23489
- await buildSignedTornadoUserOp({
23490
- privateKey,
23491
- chainId,
23492
- paymasterAddress,
23493
- paymasterData,
23494
- gas,
23495
- maxFeePerGas,
23496
- maxPriorityFeePerGas,
23497
- tailCalls: effectiveTailCalls,
23498
- // When sharing one ephemeral key across multiple deposits each userOp
23499
- // needs a distinct nonce (0, 1, 2 …) on the shared sender.
23500
- nonce: sharedPrivateKey ? BigInt(i) : 0n
23501
- })
23502
- );
23508
+ };
23509
+ const ephemeralSigner = async (deposit) => privateKeyToAccount(await secretManager.deriveEphemeralSigner({
23510
+ depositIndex: deposit.index,
23511
+ chainId: bigintChainId,
23512
+ poolAddress: deposit.pool
23513
+ }));
23514
+ const resolveBatchDelegator = async () => {
23515
+ if (delegation?.mode === "random") return privateKeyToAccount(generatePrivateKey());
23516
+ if (delegation?.mode === "deterministic" && delegation.path) {
23517
+ assertValidDelegatorPath(delegation.path);
23518
+ return privateKeyToAccount(await secretManager.deriveDelegatorSigner({ path: delegation.path }));
23519
+ }
23520
+ return ephemeralSigner(deposits[0]);
23521
+ };
23522
+ const resolveIndependentSigner = (deposit) => delegation?.mode === "deterministic" ? ephemeralSigner(deposit) : Promise.resolve(privateKeyToAccount(generatePrivateKey()));
23523
+ const needsConsolidation = deposits.length > 1 || tailCalls != null;
23524
+ if (!needsConsolidation) {
23525
+ const deposit = deposits[0];
23526
+ const signer2 = await resolveIndependentSigner(deposit);
23527
+ const gasUnits = reasonableGasUnits(poolInfo.isERC20);
23528
+ const fee2 = await quoteFee(computeMinimumViableFee(gasUnits, maxFeePerGas));
23529
+ const proof = await prove(deposit, originalRecipient, relayerAddress, fee2);
23530
+ const userOperation2 = await buildSignedTornadoUserOp({
23531
+ signer: signer2,
23532
+ chainId,
23533
+ paymasterAddress,
23534
+ paymasterData: buildPaymasterData(deposit.pool, proof),
23535
+ gas: { ...gasUnits, callGasLimit: 0n },
23536
+ maxFeePerGas,
23537
+ maxPriorityFeePerGas,
23538
+ nonce: 0n
23539
+ });
23540
+ return [{
23541
+ mode: "paymaster",
23542
+ proof,
23543
+ poolAddress: deposit.pool,
23544
+ isERC20: poolInfo.isERC20,
23545
+ paymasterAddress,
23546
+ entryPointAddress,
23547
+ bundlerUrl,
23548
+ userOperation: userOperation2
23549
+ }];
23550
+ }
23551
+ const signer = await resolveBatchDelegator();
23552
+ const senderAddress = BigInt(signer.address);
23553
+ const sponsoringDeposit = deposits[0];
23554
+ const directDeposits = deposits.slice(1);
23555
+ const ZERO_ADDRESS = 0n;
23556
+ const directWithdrawCalls = [];
23557
+ for (const deposit of directDeposits) {
23558
+ const proof = await prove(deposit, senderAddress, ZERO_ADDRESS, 0n);
23559
+ const [root, nullifierHash, proofRecipient, relayerArg, feeArg, refundArg] = proof.args;
23560
+ const data = encodeFunctionData({
23561
+ abi: poolAbi,
23562
+ functionName: "withdraw",
23563
+ args: [proof.proof, root, nullifierHash, proofRecipient, relayerArg, BigInt(feeArg), BigInt(refundArg)]
23564
+ });
23565
+ directWithdrawCalls.push(createTx(addressToHex(deposit.pool), data, 0n));
23503
23566
  }
23504
- return proofOutputs.map(({ poolAddress, ...proof }, i) => ({
23567
+ const totalDenomination = deposits.reduce(
23568
+ (sum, d) => sum + pools.get(d.pool).denomination,
23569
+ 0n
23570
+ );
23571
+ const forwardCalls = (fee2) => {
23572
+ const amount2 = totalDenomination - fee2;
23573
+ if (poolInfo.isERC20) {
23574
+ const data = encodeFunctionData({
23575
+ abi: erc20Abi,
23576
+ functionName: "transfer",
23577
+ args: [addressToHex(originalRecipient), amount2]
23578
+ });
23579
+ return [createTx(addressToHex(poolInfo.asset), data, 0n)];
23580
+ }
23581
+ return [createTx(addressToHex(originalRecipient), "0x", amount2)];
23582
+ };
23583
+ const composeTailCalls = (fee2) => async (addr) => [
23584
+ ...directWithdrawCalls,
23585
+ ...tailCalls ? await tailCalls(addr) : forwardCalls(fee2)
23586
+ ];
23587
+ const buildOp = (gas2, fee2, paymasterData) => buildSignedTornadoUserOp({
23588
+ signer,
23589
+ chainId,
23590
+ paymasterAddress,
23591
+ paymasterData,
23592
+ gas: gas2,
23593
+ maxFeePerGas,
23594
+ maxPriorityFeePerGas,
23595
+ tailCalls: composeTailCalls(fee2),
23596
+ nonce: 0n
23597
+ });
23598
+ const baselineGas = reasonableGasUnitsForBatch(poolInfo.isERC20, directDeposits.length, tailCalls != null);
23599
+ let fee = await quoteFee(computeMinimumViableFee(baselineGas, maxFeePerGas));
23600
+ let gas = baselineGas;
23601
+ let sponsoringProof = await prove(sponsoringDeposit, senderAddress, relayerAddress, fee);
23602
+ let userOperation = await buildOp(gas, fee, buildPaymasterData(sponsoringDeposit.pool, sponsoringProof));
23603
+ try {
23604
+ const estimated = await estimateUserOperationGas2(bundlerClient, userOperation, entryPointAddress);
23605
+ const buffer2 = (x) => x * 12n / 10n;
23606
+ const orBaseline = (e, b) => e > 0n ? buffer2(e) : b;
23607
+ const refinedGas = {
23608
+ callGasLimit: orBaseline(estimated.callGasLimit, baselineGas.callGasLimit),
23609
+ verificationGasLimit: orBaseline(estimated.verificationGasLimit, baselineGas.verificationGasLimit),
23610
+ preVerificationGas: orBaseline(estimated.preVerificationGas, baselineGas.preVerificationGas),
23611
+ paymasterVerificationGasLimit: orBaseline(estimated.paymasterVerificationGasLimit, baselineGas.paymasterVerificationGasLimit),
23612
+ paymasterPostOpGasLimit: orBaseline(estimated.paymasterPostOpGasLimit, baselineGas.paymasterPostOpGasLimit)
23613
+ };
23614
+ fee = await quoteFee(computeMinimumViableFee(refinedGas, maxFeePerGas));
23615
+ gas = refinedGas;
23616
+ sponsoringProof = await prove(sponsoringDeposit, senderAddress, relayerAddress, fee);
23617
+ userOperation = await buildOp(gas, fee, buildPaymasterData(sponsoringDeposit.pool, sponsoringProof));
23618
+ } catch {
23619
+ }
23620
+ return [{
23505
23621
  mode: "paymaster",
23506
- proof,
23507
- poolAddress,
23622
+ proof: sponsoringProof,
23623
+ poolAddress: sponsoringDeposit.pool,
23508
23624
  isERC20: poolInfo.isERC20,
23509
23625
  paymasterAddress,
23510
23626
  entryPointAddress,
23511
23627
  bundlerUrl,
23512
- userOperation: userOperations[i]
23513
- }));
23628
+ userOperation
23629
+ }];
23514
23630
  });
23515
23631
 
23516
- // ../provider/dist/index.js
23517
- var createTx = (address, payload, value = BigInt(0)) => {
23518
- return {
23519
- to: address,
23520
- data: payload,
23521
- value
23522
- };
23523
- };
23524
-
23525
23632
  // src/account/tx/shield.ts
23526
23633
  init_esm();
23527
23634
  init_esm();
@@ -113,6 +113,24 @@ declare enum DepositStrategy {
113
113
  MinFee = 1
114
114
  }
115
115
 
116
+ /**
117
+ * How the batch delegator for a tail-call paymaster withdrawal is produced.
118
+ * Only honored when `tailCalls` are present (see `paymasterWithdrawThunk`).
119
+ *
120
+ * - `deterministic` (default): recoverable. With `path`, the delegator is a
121
+ * classical wallet-path address; without `path`, an ephemeral address keyed by
122
+ * the batch's first deposit.
123
+ * - `random`: a throwaway, unrecoverable EOA — an explicit opt-out of
124
+ * recoverability. Never the default, because in the tail-call path the
125
+ * delegator holds the withdrawn funds.
126
+ */
127
+ type DelegationConfig = {
128
+ mode: 'deterministic';
129
+ path?: string;
130
+ } | {
131
+ mode: 'random';
132
+ };
133
+
116
134
  interface SerializedAuth {
117
135
  address: Address$1;
118
136
  chainId: Hex;
@@ -154,12 +172,6 @@ interface IGenericPaymasterWithdrawalPayload {
154
172
  userOperation: SerializedUserOperation;
155
173
  }
156
174
 
157
- type DelegationConfig = {
158
- mode: 'deterministic';
159
- path?: string;
160
- } | {
161
- mode: 'random';
162
- };
163
175
  type StringAddress = `0x${string}`;
164
176
  interface IPaymasterConfig {
165
177
  paymasterAddress: StringAddress;
@@ -114,6 +114,24 @@ declare enum DepositStrategy {
114
114
  MinFee = 1
115
115
  }
116
116
 
117
+ /**
118
+ * How the batch delegator for a tail-call paymaster withdrawal is produced.
119
+ * Only honored when `tailCalls` are present (see `paymasterWithdrawThunk`).
120
+ *
121
+ * - `deterministic` (default): recoverable. With `path`, the delegator is a
122
+ * classical wallet-path address; without `path`, an ephemeral address keyed by
123
+ * the batch's first deposit.
124
+ * - `random`: a throwaway, unrecoverable EOA — an explicit opt-out of
125
+ * recoverability. Never the default, because in the tail-call path the
126
+ * delegator holds the withdrawn funds.
127
+ */
128
+ type DelegationConfig = {
129
+ mode: 'deterministic';
130
+ path?: string;
131
+ } | {
132
+ mode: 'random';
133
+ };
134
+
117
135
  interface SerializedAuth {
118
136
  address: Address$1;
119
137
  chainId: Hex;
@@ -155,12 +173,6 @@ interface IGenericPaymasterWithdrawalPayload {
155
173
  userOperation: SerializedUserOperation;
156
174
  }
157
175
 
158
- type DelegationConfig = {
159
- mode: 'deterministic';
160
- path?: string;
161
- } | {
162
- mode: 'random';
163
- };
164
176
  type StringAddress = `0x${string}`;
165
177
  interface IPaymasterConfig {
166
178
  paymasterAddress: StringAddress;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kohaku-eth/tornado-cash",
3
- "version": "0.0.2-alpha.15",
3
+ "version": "0.0.2-alpha.17",
4
4
  "description": "Tornado Cash Shielded Pool Kohaku Plugin",
5
5
  "type": "module",
6
6
  "private": false,
@@ -74,9 +74,9 @@
74
74
  "micro-zk-proofs": "0.3.1",
75
75
  "ox": "^0.12.0",
76
76
  "viem": "^2.51.3",
77
+ "@kohaku-eth/plugins": "0.0.1-alpha.11",
77
78
  "@kohaku-eth/mimc-tree": "0.0.3",
78
- "@kohaku-eth/provider": "0.1.0-alpha.8",
79
- "@kohaku-eth/plugins": "0.0.1-alpha.11"
79
+ "@kohaku-eth/provider": "0.1.0-alpha.8"
80
80
  },
81
81
  "devDependencies": {
82
82
  "@types/node": "^24.12.2",