@kohaku-eth/tornado-cash 0.0.2-alpha.16 → 0.0.2-alpha.18

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
@@ -502,6 +502,12 @@ interface IPaymasterWithdrawParams extends IWithdrawBaseParams {
502
502
  mode: 'paymaster';
503
503
  delegation?: DelegationConfig;
504
504
  tailCalls?: (address: AccountId) => Promise<TxData[]>;
505
+ /**
506
+ * Optional baseline for the execution-phase gas of user `tailCalls` (not
507
+ * including per-note direct withdraws). Used when bundler estimation fails;
508
+ * when estimation succeeds the bundler estimate still wins.
509
+ */
510
+ tailCallsGasEstimate?: bigint;
505
511
  }
506
512
  type IWithdrawapOperationParams = IRelayerWithdrawParams | IPaymasterWithdrawParams;
507
513
  interface IGetNotesParams extends IBaseOperationParams {
@@ -567,6 +573,13 @@ interface TCRelayerUnshieldOptions extends UnshieldOptions {
567
573
  interface TCPaymasterUnshieldOptions extends UnshieldOptions {
568
574
  mode: 'paymaster';
569
575
  delegation?: DelegationConfig;
576
+ /**
577
+ * Optional baseline for the execution-phase gas of user `tailCalls` (not
578
+ * including per-note direct withdraws). Used as the batch execution-tail
579
+ * baseline when bundler estimation fails; when estimation succeeds the
580
+ * bundler estimate still wins. Defaults to the SDK's static callGasLimit.
581
+ */
582
+ tailCallsGasEstimate?: bigint;
570
583
  }
571
584
  type TCPrepareUnshieldOptions = TCRelayerUnshieldOptions | TCPaymasterUnshieldOptions;
572
585
  interface TCPrepareShieldOptions {
package/dist/index.js CHANGED
@@ -592,7 +592,8 @@ var TornadoCashProtocol = class {
592
592
  ...baseParams,
593
593
  mode: "paymaster",
594
594
  delegation: options.delegation,
595
- tailCalls: options.tailCalls
595
+ tailCalls: options.tailCalls,
596
+ tailCallsGasEstimate: options.tailCallsGasEstimate
596
597
  });
597
598
  } else {
598
599
  withdrawals = await stateManager.getWithdrawalPayloads({
@@ -22194,6 +22194,15 @@ var withdrawThunk = createAsyncThunk("withdraw/executeWithdrawals", async ({
22194
22194
  return getWithdrawalsProofs();
22195
22195
  });
22196
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
+
22197
22206
  // ../../node_modules/.pnpm/viem@2.48.4_typescript@5.9.3_zod@4.4.3/node_modules/viem/_esm/index.js
22198
22207
  init_exports();
22199
22208
  init_encodeAbiParameters2();
@@ -22368,14 +22377,20 @@ function privateKeyToAccount(privateKey, options = {}) {
22368
22377
  };
22369
22378
  }
22370
22379
 
22380
+ // src/state/thunks/paymasterWithdrawThunk.ts
22381
+ init_esm();
22382
+
22371
22383
  // src/paymaster/fee.ts
22372
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;
22373
22388
  var baseGasUnits = {
22374
- preVerificationGas: 80000n,
22389
+ preVerificationGas: 85000n,
22375
22390
  verificationGasLimit: 50000n,
22376
22391
  callGasLimit: 300000n,
22377
22392
  paymasterVerificationGasLimit: 350000n,
22378
- paymasterPostOpGasLimit: 10000n
22393
+ paymasterPostOpGasLimit: 50000n
22379
22394
  };
22380
22395
  function reasonableGasUnits(isERC20) {
22381
22396
  if (!isERC20) return baseGasUnits;
@@ -22384,6 +22399,15 @@ function reasonableGasUnits(isERC20) {
22384
22399
  paymasterVerificationGasLimit: baseGasUnits.paymasterVerificationGasLimit + ERC20_TRANSFER_GAS
22385
22400
  };
22386
22401
  }
22402
+ function reasonableGasUnitsForBatch(isERC20, extraWithdrawals, hasUserTailCalls, tailCallsGasEstimate) {
22403
+ const base = reasonableGasUnits(isERC20);
22404
+ const perWithdraw = isERC20 ? PER_DIRECT_WITHDRAW_GAS_ERC20 : PER_DIRECT_WITHDRAW_GAS;
22405
+ const executionTail = hasUserTailCalls ? tailCallsGasEstimate ?? base.callGasLimit : FORWARD_GAS;
22406
+ return {
22407
+ ...base,
22408
+ callGasLimit: BigInt(extraWithdrawals) * perWithdraw + executionTail
22409
+ };
22410
+ }
22387
22411
  function computeMinimumViableFee(reasonableGasUnits2, maxFeePerGas) {
22388
22412
  const baseFeeMultiplier = 1.2;
22389
22413
  const decimals = baseFeeMultiplier.toString().split(".")[1]?.length ?? 0;
@@ -22391,10 +22415,6 @@ function computeMinimumViableFee(reasonableGasUnits2, maxFeePerGas) {
22391
22415
  const multiply = (base) => base * BigInt(Math.ceil(baseFeeMultiplier * denominator)) / BigInt(denominator);
22392
22416
  const requiredGas = reasonableGasUnits2.verificationGasLimit + reasonableGasUnits2.callGasLimit + reasonableGasUnits2.paymasterVerificationGasLimit + reasonableGasUnits2.preVerificationGas + reasonableGasUnits2.paymasterPostOpGasLimit;
22393
22417
  const requiredPrefund = requiredGas * maxFeePerGas;
22394
- console.log("totalGasUnits", requiredGas);
22395
- console.log("maxFeePerGas", maxFeePerGas);
22396
- console.log("requiredPrefund", requiredPrefund);
22397
- console.log("multiply", 1.2);
22398
22418
  return multiply(requiredPrefund);
22399
22419
  }
22400
22420
 
@@ -24091,6 +24111,19 @@ async function getUserOperationGasPrice(client) {
24091
24111
  fast: parse(result.fast)
24092
24112
  };
24093
24113
  }
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
+ };
24126
+ }
24094
24127
  async function buildSignedTornadoUserOp({
24095
24128
  signer,
24096
24129
  chainId,
@@ -24191,6 +24224,7 @@ var paymasterWithdrawThunk = createAsyncThunk("withdraw/executePaymasterWithdraw
24191
24224
  },
24192
24225
  secretManager,
24193
24226
  tailCalls,
24227
+ tailCallsGasEstimate,
24194
24228
  ...rest
24195
24229
  }, { getState, dispatch }) => {
24196
24230
  const state = getState();
@@ -24221,12 +24255,32 @@ var paymasterWithdrawThunk = createAsyncThunk("withdraw/executePaymasterWithdraw
24221
24255
  );
24222
24256
  const bundlerClient = createPaymasterBundlerClient(bundlerUrl);
24223
24257
  const { standard: { maxFeePerGas, maxPriorityFeePerGas } } = await getUserOperationGasPrice(bundlerClient);
24224
- const gasUnits = reasonableGasUnits(poolInfo.isERC20);
24225
- const ethFee = computeMinimumViableFee(gasUnits, maxFeePerGas);
24226
- const fee = poolInfo.isERC20 ? await dataService.quoteWeiInToken(BigInt(paymasterAddress), poolInfo.asset, ethFee) : ethFee;
24227
24258
  const relayerAddress = BigInt(paymasterAddress);
24228
24259
  const bigintChainId = await dataService.getChainId();
24229
24260
  const { recipient: originalRecipient, ...restWithoutRecipient } = rest;
24261
+ const quoteFee = async (ethFee) => poolInfo.isERC20 ? dataService.quoteWeiInToken(BigInt(paymasterAddress), poolInfo.asset, ethFee) : ethFee;
24262
+ const prove = async (deposit, recipient, relayer, depositFee) => unwrapResult(await dispatch(withdrawalsProofThunk({
24263
+ ...restWithoutRecipient,
24264
+ recipient,
24265
+ deposit,
24266
+ relayerAddress: relayer,
24267
+ fee: depositFee
24268
+ })));
24269
+ const buildPaymasterData = (poolAddress, proof) => {
24270
+ const [root, nullifierHash, proofRecipient, relayerArg, feeArg, refundArg] = proof.args;
24271
+ return encodePaymasterData(
24272
+ poolAcountsMap.get(poolAddress),
24273
+ encodeTornadoAdapterData(
24274
+ proof.proof,
24275
+ root,
24276
+ nullifierHash,
24277
+ proofRecipient,
24278
+ relayerArg,
24279
+ BigInt(feeArg),
24280
+ BigInt(refundArg)
24281
+ )
24282
+ );
24283
+ };
24230
24284
  const ephemeralSigner = async (deposit) => privateKeyToAccount(await secretManager.deriveEphemeralSigner({
24231
24285
  depositIndex: deposit.index,
24232
24286
  chainId: bigintChainId,
@@ -24241,78 +24295,120 @@ var paymasterWithdrawThunk = createAsyncThunk("withdraw/executePaymasterWithdraw
24241
24295
  return ephemeralSigner(deposits[0]);
24242
24296
  };
24243
24297
  const resolveIndependentSigner = (deposit) => delegation?.mode === "deterministic" ? ephemeralSigner(deposit) : Promise.resolve(privateKeyToAccount(generatePrivateKey()));
24244
- const sharedDelegator = tailCalls ? await resolveBatchDelegator() : null;
24245
- const proofOutputs = [];
24246
- const userOperations = [];
24247
- for (let i = 0; i < deposits.length; i++) {
24248
- const deposit = deposits[i];
24249
- const isLast = i === deposits.length - 1;
24250
- const signer = sharedDelegator ?? await resolveIndependentSigner(deposit);
24251
- const recipient = sharedDelegator ? BigInt(signer.address) : originalRecipient;
24252
- const effectiveTailCalls = isLast ? tailCalls : void 0;
24253
- const gas = { ...gasUnits, callGasLimit: effectiveTailCalls ? gasUnits.callGasLimit : 0n };
24254
- const withdrawResultAction = await dispatch(
24255
- withdrawalsProofThunk({
24256
- ...restWithoutRecipient,
24257
- recipient,
24258
- deposit,
24259
- relayerAddress,
24260
- fee
24261
- })
24262
- );
24263
- const proof = { ...unwrapResult(withdrawResultAction), poolAddress: deposit.pool };
24264
- proofOutputs.push(proof);
24265
- const { poolAddress, ...proofArgs } = proof;
24266
- const [root, nullifierHash, proofRecipient, relayerArg, feeArg, refundArg] = proofArgs.args;
24267
- const paymasterData = encodePaymasterData(
24268
- poolAcountsMap.get(poolAddress),
24269
- encodeTornadoAdapterData(
24270
- proofArgs.proof,
24271
- root,
24272
- nullifierHash,
24273
- proofRecipient,
24274
- relayerArg,
24275
- BigInt(feeArg),
24276
- BigInt(refundArg)
24277
- )
24278
- );
24279
- userOperations.push(
24280
- await buildSignedTornadoUserOp({
24281
- signer,
24282
- chainId,
24283
- paymasterAddress,
24284
- paymasterData,
24285
- gas,
24286
- maxFeePerGas,
24287
- maxPriorityFeePerGas,
24288
- tailCalls: effectiveTailCalls,
24289
- // When sharing one delegator across multiple deposits each userOp needs
24290
- // a distinct nonce (0, 1, 2 …) on the shared sender.
24291
- nonce: sharedDelegator ? BigInt(i) : 0n
24292
- })
24293
- );
24298
+ const needsConsolidation = deposits.length > 1 || tailCalls != null;
24299
+ if (!needsConsolidation) {
24300
+ const deposit = deposits[0];
24301
+ const signer2 = await resolveIndependentSigner(deposit);
24302
+ const gasUnits = reasonableGasUnits(poolInfo.isERC20);
24303
+ const fee2 = await quoteFee(computeMinimumViableFee(gasUnits, maxFeePerGas));
24304
+ const proof = await prove(deposit, originalRecipient, relayerAddress, fee2);
24305
+ const userOperation2 = await buildSignedTornadoUserOp({
24306
+ signer: signer2,
24307
+ chainId,
24308
+ paymasterAddress,
24309
+ paymasterData: buildPaymasterData(deposit.pool, proof),
24310
+ gas: { ...gasUnits, callGasLimit: 0n },
24311
+ maxFeePerGas,
24312
+ maxPriorityFeePerGas,
24313
+ nonce: 0n
24314
+ });
24315
+ return [{
24316
+ mode: "paymaster",
24317
+ proof,
24318
+ poolAddress: deposit.pool,
24319
+ isERC20: poolInfo.isERC20,
24320
+ paymasterAddress,
24321
+ entryPointAddress,
24322
+ bundlerUrl,
24323
+ userOperation: userOperation2
24324
+ }];
24325
+ }
24326
+ const signer = await resolveBatchDelegator();
24327
+ const senderAddress = BigInt(signer.address);
24328
+ const sponsoringDeposit = deposits[0];
24329
+ const directDeposits = deposits.slice(1);
24330
+ const ZERO_ADDRESS = 0n;
24331
+ const directWithdrawCalls = [];
24332
+ for (const deposit of directDeposits) {
24333
+ const proof = await prove(deposit, senderAddress, ZERO_ADDRESS, 0n);
24334
+ const [root, nullifierHash, proofRecipient, relayerArg, feeArg, refundArg] = proof.args;
24335
+ const data = encodeFunctionData({
24336
+ abi: poolAbi,
24337
+ functionName: "withdraw",
24338
+ args: [proof.proof, root, nullifierHash, proofRecipient, relayerArg, BigInt(feeArg), BigInt(refundArg)]
24339
+ });
24340
+ directWithdrawCalls.push(createTx(addressToHex(deposit.pool), data, 0n));
24294
24341
  }
24295
- return proofOutputs.map(({ poolAddress, ...proof }, i) => ({
24342
+ const totalDenomination = deposits.reduce(
24343
+ (sum, d) => sum + pools.get(d.pool).denomination,
24344
+ 0n
24345
+ );
24346
+ const forwardCalls = (fee2) => {
24347
+ const amount2 = totalDenomination - fee2;
24348
+ if (poolInfo.isERC20) {
24349
+ const data = encodeFunctionData({
24350
+ abi: erc20Abi,
24351
+ functionName: "transfer",
24352
+ args: [addressToHex(originalRecipient), amount2]
24353
+ });
24354
+ return [createTx(addressToHex(poolInfo.asset), data, 0n)];
24355
+ }
24356
+ return [createTx(addressToHex(originalRecipient), "0x", amount2)];
24357
+ };
24358
+ const composeTailCalls = (fee2) => async (addr) => [
24359
+ ...directWithdrawCalls,
24360
+ ...tailCalls ? await tailCalls(addr) : forwardCalls(fee2)
24361
+ ];
24362
+ const buildOp = (gas2, fee2, paymasterData) => buildSignedTornadoUserOp({
24363
+ signer,
24364
+ chainId,
24365
+ paymasterAddress,
24366
+ paymasterData,
24367
+ gas: gas2,
24368
+ maxFeePerGas,
24369
+ maxPriorityFeePerGas,
24370
+ tailCalls: composeTailCalls(fee2),
24371
+ nonce: 0n
24372
+ });
24373
+ const baselineGas = reasonableGasUnitsForBatch(
24374
+ poolInfo.isERC20,
24375
+ directDeposits.length,
24376
+ tailCalls != null,
24377
+ tailCallsGasEstimate
24378
+ );
24379
+ let fee = await quoteFee(computeMinimumViableFee(baselineGas, maxFeePerGas));
24380
+ let gas = baselineGas;
24381
+ let sponsoringProof = await prove(sponsoringDeposit, senderAddress, relayerAddress, fee);
24382
+ let userOperation = await buildOp(gas, fee, buildPaymasterData(sponsoringDeposit.pool, sponsoringProof));
24383
+ try {
24384
+ const estimated = await estimateUserOperationGas2(bundlerClient, userOperation, entryPointAddress);
24385
+ const buffer2 = (x) => x * 12n / 10n;
24386
+ const orBaseline = (e, b) => e > 0n ? buffer2(e) : b;
24387
+ const refinedGas = {
24388
+ callGasLimit: orBaseline(estimated.callGasLimit, baselineGas.callGasLimit),
24389
+ verificationGasLimit: orBaseline(estimated.verificationGasLimit, baselineGas.verificationGasLimit),
24390
+ preVerificationGas: orBaseline(estimated.preVerificationGas, baselineGas.preVerificationGas),
24391
+ paymasterVerificationGasLimit: orBaseline(estimated.paymasterVerificationGasLimit, baselineGas.paymasterVerificationGasLimit),
24392
+ paymasterPostOpGasLimit: orBaseline(estimated.paymasterPostOpGasLimit, baselineGas.paymasterPostOpGasLimit)
24393
+ };
24394
+ fee = await quoteFee(computeMinimumViableFee(refinedGas, maxFeePerGas));
24395
+ gas = refinedGas;
24396
+ sponsoringProof = await prove(sponsoringDeposit, senderAddress, relayerAddress, fee);
24397
+ userOperation = await buildOp(gas, fee, buildPaymasterData(sponsoringDeposit.pool, sponsoringProof));
24398
+ } catch {
24399
+ }
24400
+ return [{
24296
24401
  mode: "paymaster",
24297
- proof,
24298
- poolAddress,
24402
+ proof: sponsoringProof,
24403
+ poolAddress: sponsoringDeposit.pool,
24299
24404
  isERC20: poolInfo.isERC20,
24300
24405
  paymasterAddress,
24301
24406
  entryPointAddress,
24302
24407
  bundlerUrl,
24303
- userOperation: userOperations[i]
24304
- }));
24408
+ userOperation
24409
+ }];
24305
24410
  });
24306
24411
 
24307
- // ../provider/dist/index.js
24308
- var createTx = (address, payload, value = BigInt(0)) => {
24309
- return {
24310
- to: address,
24311
- data: payload,
24312
- value
24313
- };
24314
- };
24315
-
24316
24412
  // src/account/tx/shield.ts
24317
24413
  init_esm();
24318
24414
  init_esm();
@@ -24595,7 +24691,8 @@ var storeStateManager = async ({
24595
24691
  delegation: params.delegation
24596
24692
  },
24597
24693
  secretManager,
24598
- tailCalls: params.tailCalls
24694
+ tailCalls: params.tailCalls,
24695
+ tailCallsGasEstimate: params.tailCallsGasEstimate
24599
24696
  })
24600
24697
  )
24601
24698
  );
@@ -21420,6 +21420,15 @@ var withdrawThunk = createAsyncThunk("withdraw/executeWithdrawals", async ({
21420
21420
  return getWithdrawalsProofs();
21421
21421
  });
21422
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
+
21423
21432
  // ../../node_modules/.pnpm/viem@2.48.4_typescript@5.9.3_zod@4.4.3/node_modules/viem/_esm/index.js
21424
21433
  init_exports();
21425
21434
  init_encodeAbiParameters2();
@@ -21594,14 +21603,20 @@ function privateKeyToAccount(privateKey, options = {}) {
21594
21603
  };
21595
21604
  }
21596
21605
 
21606
+ // src/state/thunks/paymasterWithdrawThunk.ts
21607
+ init_esm();
21608
+
21597
21609
  // src/paymaster/fee.ts
21598
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;
21599
21614
  var baseGasUnits = {
21600
- preVerificationGas: 80000n,
21615
+ preVerificationGas: 85000n,
21601
21616
  verificationGasLimit: 50000n,
21602
21617
  callGasLimit: 300000n,
21603
21618
  paymasterVerificationGasLimit: 350000n,
21604
- paymasterPostOpGasLimit: 10000n
21619
+ paymasterPostOpGasLimit: 50000n
21605
21620
  };
21606
21621
  function reasonableGasUnits(isERC20) {
21607
21622
  if (!isERC20) return baseGasUnits;
@@ -21610,6 +21625,15 @@ function reasonableGasUnits(isERC20) {
21610
21625
  paymasterVerificationGasLimit: baseGasUnits.paymasterVerificationGasLimit + ERC20_TRANSFER_GAS
21611
21626
  };
21612
21627
  }
21628
+ function reasonableGasUnitsForBatch(isERC20, extraWithdrawals, hasUserTailCalls, tailCallsGasEstimate) {
21629
+ const base = reasonableGasUnits(isERC20);
21630
+ const perWithdraw = isERC20 ? PER_DIRECT_WITHDRAW_GAS_ERC20 : PER_DIRECT_WITHDRAW_GAS;
21631
+ const executionTail = hasUserTailCalls ? tailCallsGasEstimate ?? base.callGasLimit : FORWARD_GAS;
21632
+ return {
21633
+ ...base,
21634
+ callGasLimit: BigInt(extraWithdrawals) * perWithdraw + executionTail
21635
+ };
21636
+ }
21613
21637
  function computeMinimumViableFee(reasonableGasUnits2, maxFeePerGas) {
21614
21638
  const baseFeeMultiplier = 1.2;
21615
21639
  const decimals = baseFeeMultiplier.toString().split(".")[1]?.length ?? 0;
@@ -21617,10 +21641,6 @@ function computeMinimumViableFee(reasonableGasUnits2, maxFeePerGas) {
21617
21641
  const multiply = (base) => base * BigInt(Math.ceil(baseFeeMultiplier * denominator)) / BigInt(denominator);
21618
21642
  const requiredGas = reasonableGasUnits2.verificationGasLimit + reasonableGasUnits2.callGasLimit + reasonableGasUnits2.paymasterVerificationGasLimit + reasonableGasUnits2.preVerificationGas + reasonableGasUnits2.paymasterPostOpGasLimit;
21619
21643
  const requiredPrefund = requiredGas * maxFeePerGas;
21620
- console.log("totalGasUnits", requiredGas);
21621
- console.log("maxFeePerGas", maxFeePerGas);
21622
- console.log("requiredPrefund", requiredPrefund);
21623
- console.log("multiply", 1.2);
21624
21644
  return multiply(requiredPrefund);
21625
21645
  }
21626
21646
 
@@ -23317,6 +23337,19 @@ async function getUserOperationGasPrice(client) {
23317
23337
  fast: parse(result.fast)
23318
23338
  };
23319
23339
  }
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
+ };
23352
+ }
23320
23353
  async function buildSignedTornadoUserOp({
23321
23354
  signer,
23322
23355
  chainId,
@@ -23417,6 +23450,7 @@ var paymasterWithdrawThunk = createAsyncThunk("withdraw/executePaymasterWithdraw
23417
23450
  },
23418
23451
  secretManager,
23419
23452
  tailCalls,
23453
+ tailCallsGasEstimate,
23420
23454
  ...rest
23421
23455
  }, { getState, dispatch }) => {
23422
23456
  const state = getState();
@@ -23447,12 +23481,32 @@ var paymasterWithdrawThunk = createAsyncThunk("withdraw/executePaymasterWithdraw
23447
23481
  );
23448
23482
  const bundlerClient = createPaymasterBundlerClient(bundlerUrl);
23449
23483
  const { standard: { maxFeePerGas, maxPriorityFeePerGas } } = await getUserOperationGasPrice(bundlerClient);
23450
- const gasUnits = reasonableGasUnits(poolInfo.isERC20);
23451
- const ethFee = computeMinimumViableFee(gasUnits, maxFeePerGas);
23452
- const fee = poolInfo.isERC20 ? await dataService.quoteWeiInToken(BigInt(paymasterAddress), poolInfo.asset, ethFee) : ethFee;
23453
23484
  const relayerAddress = BigInt(paymasterAddress);
23454
23485
  const bigintChainId = await dataService.getChainId();
23455
23486
  const { recipient: originalRecipient, ...restWithoutRecipient } = rest;
23487
+ const quoteFee = async (ethFee) => poolInfo.isERC20 ? dataService.quoteWeiInToken(BigInt(paymasterAddress), poolInfo.asset, ethFee) : ethFee;
23488
+ const prove = async (deposit, recipient, relayer, depositFee) => unwrapResult(await dispatch(withdrawalsProofThunk({
23489
+ ...restWithoutRecipient,
23490
+ recipient,
23491
+ deposit,
23492
+ relayerAddress: relayer,
23493
+ fee: depositFee
23494
+ })));
23495
+ const buildPaymasterData = (poolAddress, proof) => {
23496
+ const [root, nullifierHash, proofRecipient, relayerArg, feeArg, refundArg] = proof.args;
23497
+ return encodePaymasterData(
23498
+ poolAcountsMap.get(poolAddress),
23499
+ encodeTornadoAdapterData(
23500
+ proof.proof,
23501
+ root,
23502
+ nullifierHash,
23503
+ proofRecipient,
23504
+ relayerArg,
23505
+ BigInt(feeArg),
23506
+ BigInt(refundArg)
23507
+ )
23508
+ );
23509
+ };
23456
23510
  const ephemeralSigner = async (deposit) => privateKeyToAccount(await secretManager.deriveEphemeralSigner({
23457
23511
  depositIndex: deposit.index,
23458
23512
  chainId: bigintChainId,
@@ -23467,78 +23521,120 @@ var paymasterWithdrawThunk = createAsyncThunk("withdraw/executePaymasterWithdraw
23467
23521
  return ephemeralSigner(deposits[0]);
23468
23522
  };
23469
23523
  const resolveIndependentSigner = (deposit) => delegation?.mode === "deterministic" ? ephemeralSigner(deposit) : Promise.resolve(privateKeyToAccount(generatePrivateKey()));
23470
- const sharedDelegator = tailCalls ? await resolveBatchDelegator() : null;
23471
- const proofOutputs = [];
23472
- const userOperations = [];
23473
- for (let i = 0; i < deposits.length; i++) {
23474
- const deposit = deposits[i];
23475
- const isLast = i === deposits.length - 1;
23476
- const signer = sharedDelegator ?? await resolveIndependentSigner(deposit);
23477
- const recipient = sharedDelegator ? BigInt(signer.address) : originalRecipient;
23478
- const effectiveTailCalls = isLast ? tailCalls : void 0;
23479
- const gas = { ...gasUnits, callGasLimit: effectiveTailCalls ? gasUnits.callGasLimit : 0n };
23480
- const withdrawResultAction = await dispatch(
23481
- withdrawalsProofThunk({
23482
- ...restWithoutRecipient,
23483
- recipient,
23484
- deposit,
23485
- relayerAddress,
23486
- fee
23487
- })
23488
- );
23489
- const proof = { ...unwrapResult(withdrawResultAction), poolAddress: deposit.pool };
23490
- proofOutputs.push(proof);
23491
- const { poolAddress, ...proofArgs } = proof;
23492
- const [root, nullifierHash, proofRecipient, relayerArg, feeArg, refundArg] = proofArgs.args;
23493
- const paymasterData = encodePaymasterData(
23494
- poolAcountsMap.get(poolAddress),
23495
- encodeTornadoAdapterData(
23496
- proofArgs.proof,
23497
- root,
23498
- nullifierHash,
23499
- proofRecipient,
23500
- relayerArg,
23501
- BigInt(feeArg),
23502
- BigInt(refundArg)
23503
- )
23504
- );
23505
- userOperations.push(
23506
- await buildSignedTornadoUserOp({
23507
- signer,
23508
- chainId,
23509
- paymasterAddress,
23510
- paymasterData,
23511
- gas,
23512
- maxFeePerGas,
23513
- maxPriorityFeePerGas,
23514
- tailCalls: effectiveTailCalls,
23515
- // When sharing one delegator across multiple deposits each userOp needs
23516
- // a distinct nonce (0, 1, 2 …) on the shared sender.
23517
- nonce: sharedDelegator ? BigInt(i) : 0n
23518
- })
23519
- );
23524
+ const needsConsolidation = deposits.length > 1 || tailCalls != null;
23525
+ if (!needsConsolidation) {
23526
+ const deposit = deposits[0];
23527
+ const signer2 = await resolveIndependentSigner(deposit);
23528
+ const gasUnits = reasonableGasUnits(poolInfo.isERC20);
23529
+ const fee2 = await quoteFee(computeMinimumViableFee(gasUnits, maxFeePerGas));
23530
+ const proof = await prove(deposit, originalRecipient, relayerAddress, fee2);
23531
+ const userOperation2 = await buildSignedTornadoUserOp({
23532
+ signer: signer2,
23533
+ chainId,
23534
+ paymasterAddress,
23535
+ paymasterData: buildPaymasterData(deposit.pool, proof),
23536
+ gas: { ...gasUnits, callGasLimit: 0n },
23537
+ maxFeePerGas,
23538
+ maxPriorityFeePerGas,
23539
+ nonce: 0n
23540
+ });
23541
+ return [{
23542
+ mode: "paymaster",
23543
+ proof,
23544
+ poolAddress: deposit.pool,
23545
+ isERC20: poolInfo.isERC20,
23546
+ paymasterAddress,
23547
+ entryPointAddress,
23548
+ bundlerUrl,
23549
+ userOperation: userOperation2
23550
+ }];
23551
+ }
23552
+ const signer = await resolveBatchDelegator();
23553
+ const senderAddress = BigInt(signer.address);
23554
+ const sponsoringDeposit = deposits[0];
23555
+ const directDeposits = deposits.slice(1);
23556
+ const ZERO_ADDRESS = 0n;
23557
+ const directWithdrawCalls = [];
23558
+ for (const deposit of directDeposits) {
23559
+ const proof = await prove(deposit, senderAddress, ZERO_ADDRESS, 0n);
23560
+ const [root, nullifierHash, proofRecipient, relayerArg, feeArg, refundArg] = proof.args;
23561
+ const data = encodeFunctionData({
23562
+ abi: poolAbi,
23563
+ functionName: "withdraw",
23564
+ args: [proof.proof, root, nullifierHash, proofRecipient, relayerArg, BigInt(feeArg), BigInt(refundArg)]
23565
+ });
23566
+ directWithdrawCalls.push(createTx(addressToHex(deposit.pool), data, 0n));
23520
23567
  }
23521
- return proofOutputs.map(({ poolAddress, ...proof }, i) => ({
23568
+ const totalDenomination = deposits.reduce(
23569
+ (sum, d) => sum + pools.get(d.pool).denomination,
23570
+ 0n
23571
+ );
23572
+ const forwardCalls = (fee2) => {
23573
+ const amount2 = totalDenomination - fee2;
23574
+ if (poolInfo.isERC20) {
23575
+ const data = encodeFunctionData({
23576
+ abi: erc20Abi,
23577
+ functionName: "transfer",
23578
+ args: [addressToHex(originalRecipient), amount2]
23579
+ });
23580
+ return [createTx(addressToHex(poolInfo.asset), data, 0n)];
23581
+ }
23582
+ return [createTx(addressToHex(originalRecipient), "0x", amount2)];
23583
+ };
23584
+ const composeTailCalls = (fee2) => async (addr) => [
23585
+ ...directWithdrawCalls,
23586
+ ...tailCalls ? await tailCalls(addr) : forwardCalls(fee2)
23587
+ ];
23588
+ const buildOp = (gas2, fee2, paymasterData) => buildSignedTornadoUserOp({
23589
+ signer,
23590
+ chainId,
23591
+ paymasterAddress,
23592
+ paymasterData,
23593
+ gas: gas2,
23594
+ maxFeePerGas,
23595
+ maxPriorityFeePerGas,
23596
+ tailCalls: composeTailCalls(fee2),
23597
+ nonce: 0n
23598
+ });
23599
+ const baselineGas = reasonableGasUnitsForBatch(
23600
+ poolInfo.isERC20,
23601
+ directDeposits.length,
23602
+ tailCalls != null,
23603
+ tailCallsGasEstimate
23604
+ );
23605
+ let fee = await quoteFee(computeMinimumViableFee(baselineGas, maxFeePerGas));
23606
+ let gas = baselineGas;
23607
+ let sponsoringProof = await prove(sponsoringDeposit, senderAddress, relayerAddress, fee);
23608
+ let userOperation = await buildOp(gas, fee, buildPaymasterData(sponsoringDeposit.pool, sponsoringProof));
23609
+ try {
23610
+ const estimated = await estimateUserOperationGas2(bundlerClient, userOperation, entryPointAddress);
23611
+ const buffer2 = (x) => x * 12n / 10n;
23612
+ const orBaseline = (e, b) => e > 0n ? buffer2(e) : b;
23613
+ const refinedGas = {
23614
+ callGasLimit: orBaseline(estimated.callGasLimit, baselineGas.callGasLimit),
23615
+ verificationGasLimit: orBaseline(estimated.verificationGasLimit, baselineGas.verificationGasLimit),
23616
+ preVerificationGas: orBaseline(estimated.preVerificationGas, baselineGas.preVerificationGas),
23617
+ paymasterVerificationGasLimit: orBaseline(estimated.paymasterVerificationGasLimit, baselineGas.paymasterVerificationGasLimit),
23618
+ paymasterPostOpGasLimit: orBaseline(estimated.paymasterPostOpGasLimit, baselineGas.paymasterPostOpGasLimit)
23619
+ };
23620
+ fee = await quoteFee(computeMinimumViableFee(refinedGas, maxFeePerGas));
23621
+ gas = refinedGas;
23622
+ sponsoringProof = await prove(sponsoringDeposit, senderAddress, relayerAddress, fee);
23623
+ userOperation = await buildOp(gas, fee, buildPaymasterData(sponsoringDeposit.pool, sponsoringProof));
23624
+ } catch {
23625
+ }
23626
+ return [{
23522
23627
  mode: "paymaster",
23523
- proof,
23524
- poolAddress,
23628
+ proof: sponsoringProof,
23629
+ poolAddress: sponsoringDeposit.pool,
23525
23630
  isERC20: poolInfo.isERC20,
23526
23631
  paymasterAddress,
23527
23632
  entryPointAddress,
23528
23633
  bundlerUrl,
23529
- userOperation: userOperations[i]
23530
- }));
23634
+ userOperation
23635
+ }];
23531
23636
  });
23532
23637
 
23533
- // ../provider/dist/index.js
23534
- var createTx = (address, payload, value = BigInt(0)) => {
23535
- return {
23536
- to: address,
23537
- data: payload,
23538
- value
23539
- };
23540
- };
23541
-
23542
23638
  // src/account/tx/shield.ts
23543
23639
  init_esm();
23544
23640
  init_esm();
@@ -23821,7 +23917,8 @@ var storeStateManager = async ({
23821
23917
  delegation: params.delegation
23822
23918
  },
23823
23919
  secretManager,
23824
- tailCalls: params.tailCalls
23920
+ tailCalls: params.tailCalls,
23921
+ tailCallsGasEstimate: params.tailCallsGasEstimate
23825
23922
  })
23826
23923
  )
23827
23924
  );
@@ -211,6 +211,12 @@ interface IPaymasterWithdrawParams extends IWithdrawBaseParams {
211
211
  mode: 'paymaster';
212
212
  delegation?: DelegationConfig;
213
213
  tailCalls?: (address: AccountId) => Promise<TxData[]>;
214
+ /**
215
+ * Optional baseline for the execution-phase gas of user `tailCalls` (not
216
+ * including per-note direct withdraws). Used when bundler estimation fails;
217
+ * when estimation succeeds the bundler estimate still wins.
218
+ */
219
+ tailCallsGasEstimate?: bigint;
214
220
  }
215
221
  type IWithdrawapOperationParams = IRelayerWithdrawParams | IPaymasterWithdrawParams;
216
222
  interface IGetNotesParams extends IBaseOperationParams {
@@ -212,6 +212,12 @@ interface IPaymasterWithdrawParams extends IWithdrawBaseParams {
212
212
  mode: 'paymaster';
213
213
  delegation?: DelegationConfig;
214
214
  tailCalls?: (address: AccountId) => Promise<TxData[]>;
215
+ /**
216
+ * Optional baseline for the execution-phase gas of user `tailCalls` (not
217
+ * including per-note direct withdraws). Used when bundler estimation fails;
218
+ * when estimation succeeds the bundler estimate still wins.
219
+ */
220
+ tailCallsGasEstimate?: bigint;
215
221
  }
216
222
  type IWithdrawapOperationParams = IRelayerWithdrawParams | IPaymasterWithdrawParams;
217
223
  interface IGetNotesParams extends IBaseOperationParams {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kohaku-eth/tornado-cash",
3
- "version": "0.0.2-alpha.16",
3
+ "version": "0.0.2-alpha.18",
4
4
  "description": "Tornado Cash Shielded Pool Kohaku Plugin",
5
5
  "type": "module",
6
6
  "private": false,
@@ -75,8 +75,8 @@
75
75
  "ox": "^0.12.0",
76
76
  "viem": "^2.51.3",
77
77
  "@kohaku-eth/plugins": "0.0.1-alpha.11",
78
- "@kohaku-eth/mimc-tree": "0.0.3",
79
- "@kohaku-eth/provider": "0.1.0-alpha.8"
78
+ "@kohaku-eth/provider": "0.1.0-alpha.8",
79
+ "@kohaku-eth/mimc-tree": "0.0.3"
80
80
  },
81
81
  "devDependencies": {
82
82
  "@types/node": "^24.12.2",