@continuumdao/ctm-mpc-defi 0.2.0 → 0.2.1

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.
Files changed (100) hide show
  1. package/README.md +20 -78
  2. package/dist/agent/catalog.cjs +511 -4
  3. package/dist/agent/catalog.cjs.map +1 -1
  4. package/dist/agent/catalog.d.ts +140 -20
  5. package/dist/agent/catalog.js +501 -6
  6. package/dist/agent/catalog.js.map +1 -1
  7. package/dist/agent/skills/aave-v4/SKILL.md +43 -0
  8. package/dist/agent/skills/curve-dao/SKILL.md +12 -0
  9. package/dist/agent/skills/ethena/SKILL.md +10 -0
  10. package/dist/agent/skills/euler-v2/SKILL.md +10 -0
  11. package/dist/agent/skills/lido/SKILL.md +22 -0
  12. package/dist/agent/skills/maple-syrup/SKILL.md +10 -0
  13. package/dist/agent/skills/sky/SKILL.md +10 -0
  14. package/dist/agent/skills/uniswap-v4/SKILL.md +22 -0
  15. package/dist/chains/evm/index.cjs +27 -226
  16. package/dist/chains/evm/index.cjs.map +1 -1
  17. package/dist/chains/evm/index.d.ts +14 -26
  18. package/dist/chains/evm/index.js +21 -211
  19. package/dist/chains/evm/index.js.map +1 -1
  20. package/dist/chains/near/index.d.ts +1 -1
  21. package/dist/chains/solana/index.d.ts +1 -1
  22. package/dist/core/index.cjs +8 -110
  23. package/dist/core/index.cjs.map +1 -1
  24. package/dist/core/index.d.ts +5 -39
  25. package/dist/core/index.js +6 -100
  26. package/dist/core/index.js.map +1 -1
  27. package/dist/{envelope-CcE5Cz_q.d.ts → envelope-CpBUh9eP.d.ts} +1 -1
  28. package/dist/index.cjs +238 -1868
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.d.ts +7 -11
  31. package/dist/index.js +227 -1839
  32. package/dist/index.js.map +1 -1
  33. package/dist/protocols/evm/aave-v4/index.cjs +385 -662
  34. package/dist/protocols/evm/aave-v4/index.cjs.map +1 -1
  35. package/dist/protocols/evm/aave-v4/index.d.ts +1 -2
  36. package/dist/protocols/evm/aave-v4/index.js +385 -662
  37. package/dist/protocols/evm/aave-v4/index.js.map +1 -1
  38. package/dist/protocols/evm/curve-dao/index.cjs +24 -124
  39. package/dist/protocols/evm/curve-dao/index.cjs.map +1 -1
  40. package/dist/protocols/evm/curve-dao/index.d.ts +3 -4
  41. package/dist/protocols/evm/curve-dao/index.js +15 -115
  42. package/dist/protocols/evm/curve-dao/index.js.map +1 -1
  43. package/dist/protocols/evm/ethena/index.cjs +290 -402
  44. package/dist/protocols/evm/ethena/index.cjs.map +1 -1
  45. package/dist/protocols/evm/ethena/index.d.ts +1 -2
  46. package/dist/protocols/evm/ethena/index.js +291 -403
  47. package/dist/protocols/evm/ethena/index.js.map +1 -1
  48. package/dist/protocols/evm/euler-v2/index.cjs +485 -1163
  49. package/dist/protocols/evm/euler-v2/index.cjs.map +1 -1
  50. package/dist/protocols/evm/euler-v2/index.d.ts +1 -2
  51. package/dist/protocols/evm/euler-v2/index.js +486 -1164
  52. package/dist/protocols/evm/euler-v2/index.js.map +1 -1
  53. package/dist/protocols/evm/lido/index.cjs +241 -236
  54. package/dist/protocols/evm/lido/index.cjs.map +1 -1
  55. package/dist/protocols/evm/lido/index.d.ts +1 -2
  56. package/dist/protocols/evm/lido/index.js +242 -237
  57. package/dist/protocols/evm/lido/index.js.map +1 -1
  58. package/dist/protocols/evm/maple/index.cjs +310 -398
  59. package/dist/protocols/evm/maple/index.cjs.map +1 -1
  60. package/dist/protocols/evm/maple/index.d.ts +1 -2
  61. package/dist/protocols/evm/maple/index.js +311 -399
  62. package/dist/protocols/evm/maple/index.js.map +1 -1
  63. package/dist/protocols/evm/sky/index.cjs +238 -233
  64. package/dist/protocols/evm/sky/index.cjs.map +1 -1
  65. package/dist/protocols/evm/sky/index.d.ts +1 -2
  66. package/dist/protocols/evm/sky/index.js +236 -231
  67. package/dist/protocols/evm/sky/index.js.map +1 -1
  68. package/dist/protocols/evm/uniswap-v4/index.cjs +423 -658
  69. package/dist/protocols/evm/uniswap-v4/index.cjs.map +1 -1
  70. package/dist/protocols/evm/uniswap-v4/index.d.ts +3 -4
  71. package/dist/protocols/evm/uniswap-v4/index.js +422 -657
  72. package/dist/protocols/evm/uniswap-v4/index.js.map +1 -1
  73. package/dist/{registry-oMKlO_5z.d.ts → registry-Bv5o37_w.d.ts} +1 -1
  74. package/dist/{types-Ce2qNHai.d.cts → types-BfjWdw1j.d.ts} +3 -1
  75. package/dist/{types-5u863Fd9.d.ts → types-DUeNJLr9.d.ts} +1 -1
  76. package/package.json +7 -6
  77. package/dist/agent/catalog.d.cts +0 -939
  78. package/dist/chains/evm/index.d.cts +0 -64
  79. package/dist/chains/near/index.d.cts +0 -37
  80. package/dist/chains/solana/index.d.cts +0 -40
  81. package/dist/core/index.d.cts +0 -43
  82. package/dist/envelope-DYDPnrHZ.d.cts +0 -35
  83. package/dist/index.d.cts +0 -16
  84. package/dist/keygen-CfNp8yKJ.d.cts +0 -9
  85. package/dist/keygen-DsINazx8.d.ts +0 -9
  86. package/dist/nodeRead-BnmSaMGO.d.cts +0 -8
  87. package/dist/nodeRead-BnmSaMGO.d.ts +0 -8
  88. package/dist/protocols/evm/aave-v4/index.d.cts +0 -500
  89. package/dist/protocols/evm/curve-dao/index.d.cts +0 -147
  90. package/dist/protocols/evm/ethena/index.d.cts +0 -161
  91. package/dist/protocols/evm/euler-v2/index.d.cts +0 -317
  92. package/dist/protocols/evm/lido/index.d.cts +0 -120
  93. package/dist/protocols/evm/maple/index.d.cts +0 -109
  94. package/dist/protocols/evm/sky/index.d.cts +0 -218
  95. package/dist/protocols/evm/uniswap-v4/index.d.cts +0 -324
  96. package/dist/registry-BwZoE668.d.cts +0 -8
  97. package/dist/txParams-BC7ogvdR.d.cts +0 -19
  98. package/dist/txParams-BC7ogvdR.d.ts +0 -19
  99. package/dist/types-B8idm_gu.d.cts +0 -34
  100. package/dist/types-Ce2qNHai.d.ts +0 -57
@@ -1,4 +1,5 @@
1
- import { getAddress, zeroAddress, formatUnits, parseUnits, encodeFunctionData, erc20Abi, defineChain, createPublicClient, http, parseGwei, serializeTransaction, keccak256, decodeFunctionData } from 'viem';
1
+ import { getAddress, zeroAddress, formatUnits, parseUnits, encodeFunctionData, erc20Abi, decodeFunctionData, defineChain, createPublicClient, http, parseGwei, serializeTransaction, keccak256 } from 'viem';
2
+ import { nodeFetchWithReadAuth, fetchChainFeeParams, gasLimitFromEstimateAndChainConfig, gweiToDecimalString, proposalTxParamsToFeeSnapshot, alignEip1559FeesWithLatestBase, getClientIdFromKeyGenResult } from '@continuumdao/continuum-node-sdk';
2
3
 
3
4
  // src/core/registry.ts
4
5
  var modules = [];
@@ -90,18 +91,6 @@ var UNISWAP_TRADE_BASE_DEFAULT = "https://trade-api.gateway.uniswap.org/v1";
90
91
  var UNISWAP_UNIVERSAL_ROUTER_VERSION_DEFAULT = "2.0";
91
92
  var UNISWAP_SWAP_DEFAULT_EXPIRY_MINUTES = 30;
92
93
  var UNISWAP_SWAP_DEFAULT_DEADLINE_SEC_OFFSET = UNISWAP_SWAP_DEFAULT_EXPIRY_MINUTES * 60;
93
-
94
- // src/core/nodeRead.ts
95
- function nodeFetchWithReadAuth(url, init, auth) {
96
- const method = (init?.method ?? "GET").toUpperCase();
97
- const headers = new Headers(init?.headers);
98
- if (auth.bearerOnGet && method === "GET" && auth.jwt && auth.jwt.trim()) {
99
- headers.set("Authorization", `Bearer ${auth.jwt.trim()}`);
100
- }
101
- return fetch(url, { ...init, headers });
102
- }
103
-
104
- // src/protocols/evm/uniswap-v4/quote.ts
105
94
  var DEFAULT_TRADE_BASE = "https://trade-api.gateway.uniswap.org/v1";
106
95
  var UNISWAP_QUOTE_HEADERS_BASE = {
107
96
  "Content-Type": "application/json",
@@ -455,23 +444,65 @@ function formatUniswapQuoteForDisplay(res, args) {
455
444
  return { title: "Quote received", lines, rawJson };
456
445
  }
457
446
 
458
- // src/core/keygen.ts
459
- function firstClientIdFromKeyGen(data) {
460
- const map = data?.ClientKeys;
461
- if (!map || typeof map !== "object") return null;
462
- for (const v of Object.values(map)) {
463
- if (typeof v === "string" && v.trim()) return v.trim();
464
- }
465
- return null;
447
+ // src/core/purpose.ts
448
+ function mergePurposeText(purposeText, purposeSuffix) {
449
+ const t = purposeText.trim();
450
+ const suffix = (purposeSuffix ?? "").trim();
451
+ if (!suffix) return t;
452
+ return t ? `${t}
453
+
454
+ ${suffix}` : suffix;
466
455
  }
467
456
 
468
- // src/chains/evm/txParams.ts
469
- function gasLimitFromEstimateAndChainConfig(estimatedGas, chainGasLimit) {
470
- if (chainGasLimit == null || !Number.isFinite(chainGasLimit) || chainGasLimit <= 0) {
471
- return estimatedGas;
457
+ // src/core/envelope.ts
458
+ function finalizeMultisign(input) {
459
+ const { keyGen, destinationChainID, legs } = input;
460
+ if (legs.length === 0) {
461
+ throw new Error("finalizeMultisign requires at least one leg");
462
+ }
463
+ const ph = (keyGen.pubkeyhex ?? "").trim();
464
+ if (!ph) throw new Error("keyGen pubKey (pubkeyhex) is required");
465
+ const keyList = keyGen.keylist ?? [];
466
+ const clientId = getClientIdFromKeyGenResult(keyGen);
467
+ const first = legs[0];
468
+ const messageHashes = legs.map((l) => l.msgHash);
469
+ const messageRawBatch = legs.map((l) => l.msgRaw);
470
+ const batchMeta = legs.map((l) => ({
471
+ destinationAddress: l.destinationAddress,
472
+ signatureText: l.signatureText,
473
+ ...l.audit
474
+ }));
475
+ const proposalTxParams = legs.map((l) => l.proposalTxParams).filter((p) => p != null && typeof p === "object");
476
+ const extraPayload = {
477
+ batchMeta,
478
+ ...input.extraJSON ?? {}
479
+ };
480
+ const extraJSON = JSON.stringify(extraPayload);
481
+ const bodyForSign = {
482
+ keyList,
483
+ pubKey: ph,
484
+ msgHash: messageHashes[0],
485
+ msgRaw: first.msgRaw,
486
+ destinationChainID,
487
+ destinationAddress: input.destinationAddress ?? first.destinationAddress,
488
+ extraJSON,
489
+ signatureText: first.signatureText,
490
+ purpose: mergePurposeText(input.purposeText, input.purposeSuffix),
491
+ ...first.feeSnapshot
492
+ };
493
+ if (legs.length > 1) {
494
+ bodyForSign.messageHashes = messageHashes;
495
+ bodyForSign.messageRawBatch = messageRawBatch;
496
+ }
497
+ if (proposalTxParams.length > 0) {
498
+ bodyForSign.proposalTxParams = proposalTxParams;
499
+ }
500
+ const valueWei = first.valueWei;
501
+ if (valueWei != null && valueWei > 0n) {
502
+ bodyForSign.value = valueWei.toString();
472
503
  }
473
- const cfg = BigInt(Math.floor(chainGasLimit));
474
- return cfg > estimatedGas ? cfg : estimatedGas;
504
+ if (clientId) bodyForSign.clientId = clientId;
505
+ return { bodyForSign, messageToSign: JSON.stringify(bodyForSign) };
475
506
  }
476
507
  function routerSwapGasLimitFromEstimate(estimatedGas, chainGasLimit) {
477
508
  if (chainGasLimit != null && Number.isFinite(chainGasLimit) && chainGasLimit > 0) {
@@ -479,76 +510,168 @@ function routerSwapGasLimitFromEstimate(estimatedGas, chainGasLimit) {
479
510
  }
480
511
  return (estimatedGas * 12n + 9n) / 10n;
481
512
  }
482
- async function fetchChainFeeParams(rpcUrl, chainId) {
483
- const url = rpcUrl.trim();
484
- if (!url) return { isEip1559: false };
485
- const chainIdNum = typeof chainId === "string" ? parseInt(chainId, 10) : chainId;
486
- if (Number.isNaN(chainIdNum)) return { isEip1559: false };
487
- const chain = defineChain({
488
- id: chainIdNum,
489
- name: "Discovery",
513
+
514
+ // src/chains/evm/buildBatch.ts
515
+ async function buildEvmMultisignBatch(args) {
516
+ const { context, steps } = args;
517
+ const {
518
+ chainId,
519
+ rpcUrl,
520
+ executorAddress,
521
+ chainDetail,
522
+ useCustomGas,
523
+ customGasChainDetails,
524
+ keyGen,
525
+ purposeText
526
+ } = context;
527
+ if (steps.length === 0) throw new Error("buildEvmMultisignBatch requires at least one step");
528
+ const ch = defineChain({
529
+ id: chainId,
530
+ name: "Destination",
490
531
  nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" },
491
- rpcUrls: { default: { http: [url] } }
532
+ rpcUrls: { default: { http: [rpcUrl] } }
492
533
  });
493
- const publicClient = createPublicClient({
494
- chain,
495
- transport: http(url)
496
- });
497
- const getGasPriceGwei = async () => {
498
- const gasPriceWei = await publicClient.getGasPrice();
499
- return parseFloat(formatUnits(gasPriceWei, 9));
500
- };
501
- try {
502
- const block = await publicClient.getBlock({ blockTag: "latest" });
503
- const baseFeePerGas = block?.baseFeePerGas;
504
- if (baseFeePerGas == null || baseFeePerGas === void 0) {
505
- const gasPriceGwei2 = await getGasPriceGwei();
506
- return { isEip1559: false, gasPriceGwei: gasPriceGwei2 };
534
+ const publicClient = createPublicClient({ chain: ch, transport: http(rpcUrl) });
535
+ const feeParams = await fetchChainFeeParams(rpcUrl, chainId);
536
+ const legacy = Boolean(chainDetail?.legacy) || !feeParams.isEip1559;
537
+ const latestBaseFeeWei = !legacy ? (await publicClient.getBlock({ blockTag: "latest" })).baseFeePerGas ?? 0n : 0n;
538
+ const gasLimitConfig = useCustomGas && chainDetail?.gasLimit != null ? Number(chainDetail.gasLimit) : void 0;
539
+ const chainGasLimitRouter = chainDetail?.gasLimit != null && Number.isFinite(Number(chainDetail.gasLimit)) && Number(chainDetail.gasLimit) > 0 ? Number(chainDetail.gasLimit) : void 0;
540
+ const gasFeeMultiplier = useCustomGas && chainDetail?.gasMultiplier != null ? Number(chainDetail.gasMultiplier) : void 0;
541
+ const executor = getAddress(executorAddress);
542
+ const baseNonce = await publicClient.getTransactionCount({ address: executor, blockTag: "pending" });
543
+ const legs = [];
544
+ for (let i = 0; i < steps.length; i++) {
545
+ const step = steps[i];
546
+ const currentNonce = baseNonce + i;
547
+ let estimatedGas;
548
+ if (args.estimateGasForStep) {
549
+ estimatedGas = await args.estimateGasForStep({ step, index: i, publicClient, executor });
550
+ } else {
551
+ try {
552
+ estimatedGas = await publicClient.estimateGas({
553
+ to: step.to,
554
+ data: step.data,
555
+ value: step.value,
556
+ account: executor
557
+ });
558
+ } catch {
559
+ estimatedGas = step.fallbackGas ?? 100000n;
560
+ }
507
561
  }
508
- const baseFeeGwei = parseFloat(formatUnits(baseFeePerGas, 9));
509
- let priorityFeeGwei;
510
- try {
511
- const priorityWei = await publicClient.estimateMaxPriorityFeePerGas();
512
- priorityFeeGwei = parseFloat(formatUnits(priorityWei, 9));
513
- } catch {
562
+ let gasLimitI;
563
+ if (args.resolveGasLimit) {
564
+ gasLimitI = await args.resolveGasLimit({ step, index: i, estimatedGas, publicClient });
565
+ } else if (step.routerSwap) {
566
+ gasLimitI = routerSwapGasLimitFromEstimate(estimatedGas, chainGasLimitRouter);
567
+ } else {
568
+ gasLimitI = useCustomGas ? gasLimitFromEstimateAndChainConfig(estimatedGas, gasLimitConfig) : estimatedGas;
514
569
  }
515
- const gasPriceGwei = await getGasPriceGwei();
516
- return {
517
- isEip1559: true,
518
- baseFeeGwei,
519
- priorityFeeGwei,
520
- gasPriceGwei
521
- };
522
- } catch {
523
- try {
524
- const gasPriceWei = await publicClient.getGasPrice();
525
- const gasPriceGwei = parseFloat(formatUnits(gasPriceWei, 9));
526
- return { isEip1559: false, gasPriceGwei };
527
- } catch {
528
- return { isEip1559: false };
570
+ let proposalTxParams;
571
+ let feeSnapshot;
572
+ let serialized;
573
+ if (legacy) {
574
+ let gasPriceWei = await publicClient.getGasPrice();
575
+ if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
576
+ gasPriceWei = gasPriceWei * BigInt(100 + gasFeeMultiplier) / 100n;
577
+ }
578
+ if (useCustomGas && chainDetail?.gasPrice != null && chainDetail.gasPrice > 0) {
579
+ const configured = parseGwei(gweiToDecimalString(Number(chainDetail.gasPrice)));
580
+ if (configured > gasPriceWei) gasPriceWei = configured;
581
+ }
582
+ serialized = serializeTransaction({
583
+ type: "legacy",
584
+ to: step.to,
585
+ data: step.data,
586
+ value: step.value,
587
+ gas: gasLimitI,
588
+ gasPrice: gasPriceWei,
589
+ nonce: currentNonce,
590
+ chainId
591
+ });
592
+ proposalTxParams = {
593
+ nonce: currentNonce,
594
+ gasLimit: gasLimitI.toString(),
595
+ txType: "legacy",
596
+ gasPrice: gasPriceWei.toString()
597
+ };
598
+ feeSnapshot = proposalTxParamsToFeeSnapshot(proposalTxParams);
599
+ } else {
600
+ const fetchedBase = feeParams.baseFeeGwei ?? 0;
601
+ const fetchedPriority = feeParams.priorityFeeGwei ?? 0;
602
+ const configuredBase = useCustomGas && chainDetail?.baseFee != null ? Number(chainDetail.baseFee) : 0;
603
+ const configuredPriority = useCustomGas && chainDetail?.priorityFee != null ? Number(chainDetail.priorityFee) : 0;
604
+ const effectiveBaseFeeGwei = Math.max(fetchedBase, configuredBase);
605
+ const effectivePriorityFeeGwei = Math.max(fetchedPriority, configuredPriority);
606
+ const baseFeeMultiplierPct = useCustomGas && chainDetail?.baseFeeMultiplier != null ? Math.max(100, Number(chainDetail.baseFeeMultiplier)) : 100;
607
+ const baseComponentGwei = effectiveBaseFeeGwei * baseFeeMultiplierPct / 100;
608
+ const maxFeePerGasGwei = baseComponentGwei + effectivePriorityFeeGwei;
609
+ let maxPriorityFeePerGas = effectivePriorityFeeGwei > 0 ? parseGwei(gweiToDecimalString(effectivePriorityFeeGwei)) : parseGwei("1");
610
+ let maxFeePerGas = parseGwei(gweiToDecimalString(maxFeePerGasGwei));
611
+ if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
612
+ maxPriorityFeePerGas = maxPriorityFeePerGas * BigInt(100 + gasFeeMultiplier) / 100n;
613
+ maxFeePerGas = maxFeePerGas * BigInt(100 + gasFeeMultiplier) / 100n;
614
+ }
615
+ ({ maxFeePerGas, maxPriorityFeePerGas } = alignEip1559FeesWithLatestBase(
616
+ maxFeePerGas,
617
+ maxPriorityFeePerGas,
618
+ latestBaseFeeWei
619
+ ));
620
+ serialized = serializeTransaction({
621
+ type: "eip1559",
622
+ to: step.to,
623
+ data: step.data,
624
+ value: step.value,
625
+ gas: gasLimitI,
626
+ maxFeePerGas,
627
+ maxPriorityFeePerGas,
628
+ nonce: currentNonce,
629
+ chainId
630
+ });
631
+ proposalTxParams = {
632
+ nonce: currentNonce,
633
+ gasLimit: gasLimitI.toString(),
634
+ txType: "eip1559",
635
+ maxFeePerGas: maxFeePerGas.toString(),
636
+ maxPriorityFeePerGas: maxPriorityFeePerGas.toString()
637
+ };
638
+ feeSnapshot = i === 0 ? proposalTxParamsToFeeSnapshot(proposalTxParams) : {};
639
+ }
640
+ const h = keccak256(serialized);
641
+ const msgHash = h.startsWith("0x") ? h.slice(2) : h;
642
+ const batchMetaExtra = args.buildBatchMeta({ step, index: i, gasLimit: gasLimitI });
643
+ legs.push({
644
+ msgHash,
645
+ msgRaw: i === 0 && args.firstMsgRawNo0x != null ? args.firstMsgRawNo0x : serialized,
646
+ destinationAddress: step.to,
647
+ signatureText: typeof batchMetaExtra.signatureText === "string" ? batchMetaExtra.signatureText : JSON.stringify(batchMetaExtra.signatureText ?? {}),
648
+ audit: batchMetaExtra,
649
+ feeSnapshot: i === 0 ? feeSnapshot : {},
650
+ proposalTxParams,
651
+ valueWei: i === 0 ? step.value : void 0
652
+ });
653
+ if (i === 0 && args.firstMsgRawNo0x != null) {
654
+ legs[0].msgRaw = args.firstMsgRawNo0x;
529
655
  }
530
656
  }
531
- }
532
- function finalizeEip1559Fees(maxFeePerGas, maxPriorityFeePerGas, floor, baseWei) {
533
- let maxP = maxPriorityFeePerGas;
534
- let maxF = maxFeePerGas;
535
- if (baseWei > 0n && maxF < baseWei + maxP) {
536
- maxF = baseWei + maxP + parseGwei("0.001");
537
- }
538
- if (maxF < maxP) {
539
- maxF = baseWei > 0n ? baseWei + maxP + parseGwei("0.001") : maxP * 2n;
657
+ const extraJSON = {};
658
+ if (useCustomGas && customGasChainDetails && Object.keys(customGasChainDetails).length > 0) {
659
+ extraJSON.customGasChainDetails = customGasChainDetails;
660
+ }
661
+ const result = finalizeMultisign({
662
+ keyGen,
663
+ purposeText,
664
+ purposeSuffix: args.purposeSuffix,
665
+ destinationChainID: String(chainId),
666
+ destinationAddress: args.destinationAddress ?? steps[0].to,
667
+ legs,
668
+ extraJSON: Object.keys(extraJSON).length > 0 ? extraJSON : void 0
669
+ });
670
+ const pv = args.payableValueWei;
671
+ if (pv != null && pv > 0n) {
672
+ result.bodyForSign.value = pv.toString();
540
673
  }
541
- return { maxFeePerGas: maxF, maxPriorityFeePerGas: maxP };
542
- }
543
- function alignEip1559FeesWithLatestBase(maxFeePerGas, maxPriorityFeePerGas, latestBlockBaseFeeWei) {
544
- return finalizeEip1559Fees(maxFeePerGas, maxPriorityFeePerGas, null, latestBlockBaseFeeWei);
545
- }
546
- function gweiToDecimalString(n) {
547
- if (!Number.isFinite(n)) return "0";
548
- if (n === 0) return "0";
549
- const s = String(n);
550
- if (s.indexOf("e") !== -1 || s.indexOf("E") !== -1) return n.toFixed(9).replace(/\.?0+$/, "") || "0";
551
- return s;
674
+ return result;
552
675
  }
553
676
 
554
677
  // src/protocols/evm/uniswap-v4/swapMultisign.ts
@@ -737,6 +860,35 @@ async function uniswapCreateSwap(args) {
737
860
  }
738
861
  return parsed;
739
862
  }
863
+ async function estimateUniswapRouterSwapGas(args) {
864
+ const fromTradeApi = parseOptionalGasLimitString(args.swapRecord.gasLimit) ?? parseOptionalGasLimitString(args.swapRecord.gas);
865
+ if (fromTradeApi != null && fromTradeApi > 0n) {
866
+ return { baseGasUnits: fromTradeApi, source: "tradeApi" };
867
+ }
868
+ try {
869
+ const est = await args.publicClient.estimateGas({
870
+ to: args.to,
871
+ data: args.data,
872
+ value: args.value,
873
+ account: args.executor
874
+ });
875
+ return { baseGasUnits: est, source: "rpcEstimate" };
876
+ } catch (e) {
877
+ const estimateGasError = e instanceof Error ? e.message : String(e);
878
+ const minRouterGas = 500000n;
879
+ if (args.useCustomGas) {
880
+ const cfg = args.chainDetail?.gasLimit != null ? parseOptionalGasLimitString(String(args.chainDetail.gasLimit)) : null;
881
+ if (cfg != null && cfg >= minRouterGas) {
882
+ return { baseGasUnits: cfg, source: "estimateFailedFallback", estimateGasError };
883
+ }
884
+ }
885
+ return {
886
+ baseGasUnits: DEFAULT_UNIVERSAL_ROUTER_GAS_FALLBACK,
887
+ source: "estimateFailedFallback",
888
+ estimateGasError
889
+ };
890
+ }
891
+ }
740
892
  var permit2ApproveRouterAbi = [
741
893
  {
742
894
  name: "approve",
@@ -837,10 +989,6 @@ function permit2SpenderAndApproveWeiFromSwapCalldata(dataHex, tokenIn, quoteInpu
837
989
  }
838
990
  }
839
991
  async function buildEvmMultisignBodyUniswapV4NativeInOnly(args, quoteInputWei) {
840
- const ph = (args.keyGen.pubkeyhex ?? "").trim();
841
- if (!ph) throw new Error("keyGen pubKey (pubkeyhex) is required");
842
- const keyList = args.keyGen.keylist ?? [];
843
- const clientId = firstClientIdFromKeyGen(args.keyGen);
844
992
  const toRouter = getAddress(
845
993
  (args.swap.to ?? "").trim().startsWith("0x") ? args.swap.to.trim() : `0x${args.swap.to.trim()}`
846
994
  );
@@ -858,214 +1006,90 @@ async function buildEvmMultisignBodyUniswapV4NativeInOnly(args, quoteInputWei) {
858
1006
  "Native (ETH) in swap: could not determine payable value (no `swap.value`, no `execute` amount in calldata, and quote input is zero). Refresh the quote and request /swap again."
859
1007
  );
860
1008
  }
861
- const ch = defineChain({
862
- id: args.chainId,
863
- name: "Destination",
864
- nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" },
865
- rpcUrls: { default: { http: [args.rpcUrl] } }
866
- });
867
- const publicClient = createPublicClient({ chain: ch, transport: http(args.rpcUrl) });
868
- const feeParams = await fetchChainFeeParams(args.rpcUrl, args.chainId);
869
- const legacy = Boolean(args.chainDetail?.legacy) || !feeParams.isEip1559;
870
- const latestBaseFeeWeiNativeIn = !legacy ? (await publicClient.getBlock({ blockTag: "latest" })).baseFeePerGas ?? 0n : 0n;
871
- const useCustomGas = args.useCustomGas;
872
- const chainGasLimitRouter = args.chainDetail?.gasLimit != null && Number.isFinite(Number(args.chainDetail.gasLimit)) && Number(args.chainDetail.gasLimit) > 0 ? Number(args.chainDetail.gasLimit) : void 0;
873
- const gasFeeMultiplier = useCustomGas && args.chainDetail?.gasMultiplier != null ? Number(args.chainDetail.gasMultiplier) : void 0;
874
- const nonce = await publicClient.getTransactionCount({ address: args.executorAddress, blockTag: "pending" });
875
- const proposalTxParamsBatch = [];
876
- const messageHashes = [];
877
- const messageRawBatch = [];
878
1009
  const swapRecord = args.swap;
879
- const fromTradeApi = parseOptionalGasLimitString(swapRecord.gasLimit) ?? parseOptionalGasLimitString(swapRecord.gas);
880
1010
  let gasBuildSource = "rpcEstimate";
881
1011
  let estimateGasError;
882
- let baseGasUnits1;
883
- if (fromTradeApi != null && fromTradeApi > 0n) {
884
- baseGasUnits1 = fromTradeApi;
885
- gasBuildSource = "tradeApi";
886
- } else {
887
- try {
888
- baseGasUnits1 = await publicClient.estimateGas({
1012
+ let swapBaseGasUnits = DEFAULT_UNIVERSAL_ROUTER_GAS_FALLBACK;
1013
+ const dataNo0x = dataHex.startsWith("0x") ? dataHex.slice(2) : dataHex;
1014
+ const purposeSuffix = "Uniswap V4: 1-tx batch \u2014 native gas token in (no ERC-20 approve) \u2014 single payable swap (Trade /swap).";
1015
+ return buildEvmMultisignBatch({
1016
+ context: {
1017
+ chainCategory: "evm",
1018
+ keyGen: args.keyGen,
1019
+ purposeText: args.purposeText,
1020
+ chainId: args.chainId,
1021
+ rpcUrl: args.rpcUrl,
1022
+ executorAddress: args.executorAddress,
1023
+ chainDetail: args.chainDetail,
1024
+ useCustomGas: args.useCustomGas,
1025
+ customGasChainDetails: args.customGasChainDetails
1026
+ },
1027
+ steps: [
1028
+ {
889
1029
  to: toRouter,
890
1030
  data: dataHex,
891
1031
  value: valueWei,
892
- account: args.executorAddress
893
- });
894
- gasBuildSource = "rpcEstimate";
895
- } catch (e) {
896
- estimateGasError = e instanceof Error ? e.message : String(e);
897
- const minRouterGas = 500000n;
898
- if (useCustomGas) {
899
- const cfg = args.chainDetail?.gasLimit != null ? parseOptionalGasLimitString(String(args.chainDetail.gasLimit)) : null;
900
- if (cfg != null && cfg >= minRouterGas) {
901
- baseGasUnits1 = cfg;
902
- } else {
903
- baseGasUnits1 = DEFAULT_UNIVERSAL_ROUTER_GAS_FALLBACK;
904
- }
905
- } else {
906
- baseGasUnits1 = DEFAULT_UNIVERSAL_ROUTER_GAS_FALLBACK;
907
- }
908
- gasBuildSource = "estimateFailedFallback";
909
- }
910
- }
911
- const gasLimit1 = routerSwapGasLimitFromEstimate(baseGasUnits1, chainGasLimitRouter);
912
- const currentNonce0 = nonce;
913
- let firstTxFeePayload = {};
914
- if (legacy) {
915
- let gasPriceWei1 = await publicClient.getGasPrice();
916
- if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
917
- gasPriceWei1 = gasPriceWei1 * BigInt(100 + gasFeeMultiplier) / 100n;
918
- }
919
- if (useCustomGas && args.chainDetail?.gasPrice != null && args.chainDetail.gasPrice > 0) {
920
- const configured = parseGwei(gweiToDecimalString(Number(args.chainDetail.gasPrice)));
921
- if (configured > gasPriceWei1) gasPriceWei1 = configured;
922
- }
923
- firstTxFeePayload = { txNonce: nonce, txGasLimit: gasLimit1.toString(), txGasPrice: gasPriceWei1.toString() };
924
- const ser0 = serializeTransaction({
925
- type: "legacy",
926
- to: toRouter,
927
- data: dataHex,
928
- value: valueWei,
929
- gas: gasLimit1,
930
- gasPrice: gasPriceWei1,
931
- nonce: currentNonce0,
932
- chainId: args.chainId
933
- });
934
- const h0 = keccak256(ser0);
935
- messageHashes.push(h0.startsWith("0x") ? h0.slice(2) : h0);
936
- messageRawBatch.push(ser0);
937
- proposalTxParamsBatch.push({
938
- nonce: currentNonce0,
939
- gasLimit: gasLimit1.toString(),
940
- txType: "legacy",
941
- gasPrice: gasPriceWei1.toString()
942
- });
943
- } else {
944
- const fetchedBase1 = feeParams.baseFeeGwei ?? 0;
945
- const fetchedPriority1 = feeParams.priorityFeeGwei ?? 0;
946
- const configuredBase1 = useCustomGas && args.chainDetail?.baseFee != null ? Number(args.chainDetail.baseFee) : 0;
947
- const configuredPriority1 = useCustomGas && args.chainDetail?.priorityFee != null ? Number(args.chainDetail.priorityFee) : 0;
948
- const effectiveBaseFeeGwei1 = Math.max(fetchedBase1, configuredBase1);
949
- const effectivePriorityFeeGwei1 = Math.max(fetchedPriority1, configuredPriority1);
950
- const baseFeeMultiplierPct1 = useCustomGas && args.chainDetail?.baseFeeMultiplier != null ? Math.max(100, Number(args.chainDetail.baseFeeMultiplier)) : 100;
951
- const baseComponentGwei1 = effectiveBaseFeeGwei1 * baseFeeMultiplierPct1 / 100;
952
- const maxFeePerGasGwei1 = baseComponentGwei1 + effectivePriorityFeeGwei1;
953
- let maxPriorityFeePerGas1 = effectivePriorityFeeGwei1 > 0 ? parseGwei(gweiToDecimalString(effectivePriorityFeeGwei1)) : parseGwei("1");
954
- let maxFeePerGas1 = parseGwei(gweiToDecimalString(maxFeePerGasGwei1));
955
- if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
956
- maxPriorityFeePerGas1 = maxPriorityFeePerGas1 * BigInt(100 + gasFeeMultiplier) / 100n;
957
- maxFeePerGas1 = maxFeePerGas1 * BigInt(100 + gasFeeMultiplier) / 100n;
958
- }
959
- ({ maxFeePerGas: maxFeePerGas1, maxPriorityFeePerGas: maxPriorityFeePerGas1 } = alignEip1559FeesWithLatestBase(maxFeePerGas1, maxPriorityFeePerGas1, latestBaseFeeWeiNativeIn));
960
- firstTxFeePayload = {
961
- txNonce: nonce,
962
- txGasLimit: gasLimit1.toString(),
963
- txMaxFeePerGas: maxFeePerGas1.toString(),
964
- txMaxPriorityFeePerGas: maxPriorityFeePerGas1.toString()
965
- };
966
- const ser0 = serializeTransaction({
967
- type: "eip1559",
968
- to: toRouter,
969
- data: dataHex,
970
- value: valueWei,
971
- gas: gasLimit1,
972
- maxFeePerGas: maxFeePerGas1,
973
- maxPriorityFeePerGas: maxPriorityFeePerGas1,
974
- nonce: currentNonce0,
975
- chainId: args.chainId
976
- });
977
- const h0 = keccak256(ser0);
978
- messageHashes.push(h0.startsWith("0x") ? h0.slice(2) : h0);
979
- messageRawBatch.push(ser0);
980
- proposalTxParamsBatch.push({
981
- nonce: currentNonce0,
982
- gasLimit: gasLimit1.toString(),
983
- txType: "eip1559",
984
- maxFeePerGas: maxFeePerGas1.toString(),
985
- maxPriorityFeePerGas: maxPriorityFeePerGas1.toString()
986
- });
987
- }
988
- const dataNo0x = dataHex.startsWith("0x") ? dataHex.slice(2) : dataHex;
989
- const audit = {
990
- skipPermit2Batch: true,
991
- inputKind: "native_eth",
992
- noErc20Approve: true,
993
- quoteInputWei: quoteInputWei.toString(),
994
- swapValueWei: valueWei.toString(),
995
- uniswapCreateSwap: {
996
- requestId: args.createSwapResponse.requestId,
997
- gasFee: args.createSwapResponse.gasFee,
998
- gasBuildSwap: {
999
- useCustomGas,
1000
- source: gasBuildSource,
1001
- baseGasUnits: baseGasUnits1.toString(),
1002
- ...estimateGasError != null && estimateGasError !== "" ? { estimateGasError } : {}
1003
- },
1004
- swap: {
1005
- to: args.createSwapResponse.swap.to,
1006
- value: args.createSwapResponse.swap.value,
1007
- dataNibbles: (() => {
1008
- const t = (args.createSwapResponse.swap.data ?? "").toString().trim();
1009
- return t.startsWith("0x") ? t.length - 2 : t.length;
1010
- })()
1032
+ routerSwap: true,
1033
+ fallbackGas: DEFAULT_UNIVERSAL_ROUTER_GAS_FALLBACK
1011
1034
  }
1035
+ ],
1036
+ purposeSuffix,
1037
+ firstMsgRawNo0x: dataNo0x,
1038
+ destinationAddress: toRouter,
1039
+ estimateGasForStep: async ({ publicClient, executor }) => {
1040
+ const r = await estimateUniswapRouterSwapGas({
1041
+ publicClient,
1042
+ executor,
1043
+ to: toRouter,
1044
+ data: dataHex,
1045
+ value: valueWei,
1046
+ swapRecord,
1047
+ useCustomGas: args.useCustomGas,
1048
+ chainDetail: args.chainDetail
1049
+ });
1050
+ gasBuildSource = r.source;
1051
+ estimateGasError = r.estimateGasError;
1052
+ swapBaseGasUnits = r.baseGasUnits;
1053
+ return r.baseGasUnits;
1012
1054
  },
1013
- fullQuoteFromPermitSnapshot: args.fullQuoteSnapshot,
1014
- originalPurpose: args.purposeText
1015
- };
1016
- const batchMeta = [
1017
- {
1018
- destinationAddress: toRouter,
1055
+ buildBatchMeta: () => ({
1019
1056
  signatureText: JSON.stringify({
1020
1057
  kind: "UniswapV4",
1021
1058
  name: "UniversalRouter (payable, native in)",
1022
1059
  note: "Single tx from Trade POST /swap; no ERC-20 approve. Calldata in messageHashes[0] / messageRawBatch[0]."
1023
1060
  }),
1024
1061
  evm: { type: "uniswap_v4_swap_tx", version: 1, chainId: String(args.chainId) },
1025
- uniswapV4: audit
1026
- }
1027
- ];
1028
- const extraPayload = { batchMeta };
1029
- if (useCustomGas) {
1030
- const snap = args.customGasChainDetails;
1031
- if (snap && typeof snap === "object" && !Array.isArray(snap) && Object.keys(snap).length > 0) {
1032
- extraPayload.customGasChainDetails = snap;
1033
- }
1034
- }
1035
- const extraJSON = JSON.stringify(extraPayload);
1036
- const firstSigText = batchMeta[0].signatureText;
1037
- const bodyForSign = {
1038
- keyList,
1039
- pubKey: ph,
1040
- msgHash: messageHashes[0],
1041
- msgRaw: dataNo0x,
1042
- messageHashes,
1043
- messageRawBatch,
1044
- destinationChainID: String(args.chainId),
1045
- destinationAddress: toRouter,
1046
- extraJSON,
1047
- signatureText: firstSigText,
1048
- purpose: (() => {
1049
- const t = args.purposeText.trim();
1050
- const batchDesc = "Uniswap V4: 1-tx batch \u2014 native gas token in (no ERC-20 approve) \u2014 single payable swap (Trade /swap).";
1051
- return (t ? `${t}
1052
-
1053
- ` : "") + batchDesc;
1054
- })(),
1055
- ...firstTxFeePayload,
1056
- proposalTxParams: proposalTxParamsBatch
1057
- };
1058
- if (valueWei > 0n) {
1059
- bodyForSign.value = valueWei.toString();
1060
- }
1061
- if (clientId) bodyForSign.clientId = clientId;
1062
- return { bodyForSign, messageToSign: JSON.stringify(bodyForSign) };
1062
+ uniswapV4: {
1063
+ skipPermit2Batch: true,
1064
+ inputKind: "native_eth",
1065
+ noErc20Approve: true,
1066
+ quoteInputWei: quoteInputWei.toString(),
1067
+ swapValueWei: valueWei.toString(),
1068
+ uniswapCreateSwap: {
1069
+ requestId: args.createSwapResponse.requestId,
1070
+ gasFee: args.createSwapResponse.gasFee,
1071
+ gasBuildSwap: {
1072
+ useCustomGas: args.useCustomGas,
1073
+ source: gasBuildSource,
1074
+ baseGasUnits: swapBaseGasUnits.toString(),
1075
+ ...estimateGasError != null && estimateGasError !== "" ? { estimateGasError } : {}
1076
+ },
1077
+ swap: {
1078
+ to: args.createSwapResponse.swap.to,
1079
+ value: args.createSwapResponse.swap.value,
1080
+ dataNibbles: (() => {
1081
+ const t = (args.createSwapResponse.swap.data ?? "").toString().trim();
1082
+ return t.startsWith("0x") ? t.length - 2 : t.length;
1083
+ })()
1084
+ }
1085
+ },
1086
+ fullQuoteFromPermitSnapshot: args.fullQuoteSnapshot,
1087
+ originalPurpose: args.purposeText
1088
+ }
1089
+ })
1090
+ });
1063
1091
  }
1064
1092
  async function buildEvmMultisignBodyUniswapV4SkipPermit2Batch(args) {
1065
- const ph = (args.keyGen.pubkeyhex ?? "").trim();
1066
- if (!ph) throw new Error("keyGen pubKey (pubkeyhex) is required");
1067
- const keyList = args.keyGen.keylist ?? [];
1068
- const clientId = firstClientIdFromKeyGen(args.keyGen);
1069
1093
  const tokenIn = getAddress(args.tokenIn);
1070
1094
  const parsedInOut = parseUniswapQuoteClassicInOut(args.fullQuoteSnapshot);
1071
1095
  const quoteInputWei = parsedInOut?.inputWei;
@@ -1120,298 +1144,33 @@ async function buildEvmMultisignBodyUniswapV4SkipPermit2Batch(args) {
1120
1144
  return 0n;
1121
1145
  }
1122
1146
  })();
1123
- const ch = defineChain({
1124
- id: args.chainId,
1125
- name: "Destination",
1126
- nativeCurrency: { decimals: 18, name: "Ether", symbol: "ETH" },
1127
- rpcUrls: { default: { http: [args.rpcUrl] } }
1128
- });
1129
- const publicClient = createPublicClient({ chain: ch, transport: http(args.rpcUrl) });
1130
- const feeParams = await fetchChainFeeParams(args.rpcUrl, args.chainId);
1131
- const legacy = Boolean(args.chainDetail?.legacy) || !feeParams.isEip1559;
1132
- const latestBaseFeeWeiSkipBatch = !legacy ? (await publicClient.getBlock({ blockTag: "latest" })).baseFeePerGas ?? 0n : 0n;
1133
- const useCustomGas = args.useCustomGas;
1134
- const gasLimitConfig = useCustomGas && args.chainDetail?.gasLimit != null ? Number(args.chainDetail.gasLimit) : void 0;
1135
- const chainGasLimitRouter = args.chainDetail?.gasLimit != null && Number.isFinite(Number(args.chainDetail.gasLimit)) && Number(args.chainDetail.gasLimit) > 0 ? Number(args.chainDetail.gasLimit) : void 0;
1136
- const gasFeeMultiplier = useCustomGas && args.chainDetail?.gasMultiplier != null ? Number(args.chainDetail.gasMultiplier) : void 0;
1137
- const nonce = await publicClient.getTransactionCount({ address: args.executorAddress, blockTag: "pending" });
1138
- const proposalTxParamsBatch = [];
1139
- const messageHashes = [];
1140
- const messageRawBatch = [];
1141
- const approveMsgRawNo0x = approveData.startsWith("0x") ? approveData.slice(2) : approveData;
1142
- let firstTxFeePayload = {};
1143
- const approveGas = await publicClient.estimateGas({
1144
- to: tokenIn,
1145
- data: approveData,
1146
- value: 0n,
1147
- account: args.executorAddress
1148
- });
1149
- const gasLimit0 = useCustomGas ? gasLimitFromEstimateAndChainConfig(approveGas, gasLimitConfig) : approveGas;
1150
- const currentNonce0 = nonce;
1151
- if (legacy) {
1152
- let gasPriceWei = await publicClient.getGasPrice();
1153
- if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
1154
- gasPriceWei = gasPriceWei * BigInt(100 + gasFeeMultiplier) / 100n;
1155
- }
1156
- if (useCustomGas && args.chainDetail?.gasPrice != null && args.chainDetail.gasPrice > 0) {
1157
- const configured = parseGwei(gweiToDecimalString(Number(args.chainDetail.gasPrice)));
1158
- if (configured > gasPriceWei) gasPriceWei = configured;
1159
- }
1160
- firstTxFeePayload = { txNonce: nonce, txGasLimit: gasLimit0.toString(), txGasPrice: gasPriceWei.toString() };
1161
- const ser0 = serializeTransaction({
1162
- type: "legacy",
1163
- to: tokenIn,
1164
- data: approveData,
1165
- value: 0n,
1166
- gas: gasLimit0,
1167
- gasPrice: gasPriceWei,
1168
- nonce: currentNonce0,
1169
- chainId: args.chainId
1170
- });
1171
- const h0 = keccak256(ser0);
1172
- messageHashes.push(h0.startsWith("0x") ? h0.slice(2) : h0);
1173
- messageRawBatch.push(ser0);
1174
- proposalTxParamsBatch.push({
1175
- nonce: currentNonce0,
1176
- gasLimit: gasLimit0.toString(),
1177
- txType: "legacy",
1178
- gasPrice: gasPriceWei.toString()
1179
- });
1180
- } else {
1181
- const fetchedBase = feeParams.baseFeeGwei ?? 0;
1182
- const fetchedPriority = feeParams.priorityFeeGwei ?? 0;
1183
- const configuredBase = useCustomGas && args.chainDetail?.baseFee != null ? Number(args.chainDetail.baseFee) : 0;
1184
- const configuredPriority = useCustomGas && args.chainDetail?.priorityFee != null ? Number(args.chainDetail.priorityFee) : 0;
1185
- const effectiveBaseFeeGwei = Math.max(fetchedBase, configuredBase);
1186
- const effectivePriorityFeeGwei = Math.max(fetchedPriority, configuredPriority);
1187
- const baseFeeMultiplierPct = useCustomGas && args.chainDetail?.baseFeeMultiplier != null ? Math.max(100, Number(args.chainDetail.baseFeeMultiplier)) : 100;
1188
- const baseComponentGwei = effectiveBaseFeeGwei * baseFeeMultiplierPct / 100;
1189
- const maxFeePerGasGwei = baseComponentGwei + effectivePriorityFeeGwei;
1190
- let maxPriorityFeePerGas = effectivePriorityFeeGwei > 0 ? parseGwei(gweiToDecimalString(effectivePriorityFeeGwei)) : parseGwei("1");
1191
- let maxFeePerGas = parseGwei(gweiToDecimalString(maxFeePerGasGwei));
1192
- if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
1193
- maxPriorityFeePerGas = maxPriorityFeePerGas * BigInt(100 + gasFeeMultiplier) / 100n;
1194
- maxFeePerGas = maxFeePerGas * BigInt(100 + gasFeeMultiplier) / 100n;
1195
- }
1196
- ({ maxFeePerGas, maxPriorityFeePerGas } = alignEip1559FeesWithLatestBase(
1197
- maxFeePerGas,
1198
- maxPriorityFeePerGas,
1199
- latestBaseFeeWeiSkipBatch
1200
- ));
1201
- firstTxFeePayload = {
1202
- txNonce: nonce,
1203
- txGasLimit: gasLimit0.toString(),
1204
- txMaxFeePerGas: maxFeePerGas.toString(),
1205
- txMaxPriorityFeePerGas: maxPriorityFeePerGas.toString()
1206
- };
1207
- const ser0 = serializeTransaction({
1208
- type: "eip1559",
1209
- to: tokenIn,
1210
- data: approveData,
1211
- value: 0n,
1212
- gas: gasLimit0,
1213
- maxFeePerGas,
1214
- maxPriorityFeePerGas,
1215
- nonce: currentNonce0,
1216
- chainId: args.chainId
1217
- });
1218
- const h0 = keccak256(ser0);
1219
- messageHashes.push(h0.startsWith("0x") ? h0.slice(2) : h0);
1220
- messageRawBatch.push(ser0);
1221
- proposalTxParamsBatch.push({
1222
- nonce: currentNonce0,
1223
- gasLimit: gasLimit0.toString(),
1224
- txType: "eip1559",
1225
- maxFeePerGas: maxFeePerGas.toString(),
1226
- maxPriorityFeePerGas: maxPriorityFeePerGas.toString()
1227
- });
1228
- }
1147
+ const swapRecord = args.swap;
1148
+ const swapMsgIndex = usePermit2Triple ? 2 : 1;
1149
+ let gasBuildSource = "rpcEstimate";
1150
+ let estimateGasError;
1151
+ let swapBaseGasUnits = DEFAULT_UNIVERSAL_ROUTER_GAS_FALLBACK;
1152
+ const steps = [
1153
+ { to: tokenIn, data: approveData, value: 0n, fallbackGas: 100000n }
1154
+ ];
1229
1155
  if (usePermit2Triple) {
1230
1156
  const permit2ApproveData = encodeFunctionData({
1231
1157
  abi: permit2ApproveRouterAbi,
1232
1158
  functionName: "approve",
1233
1159
  args: [tokenIn, permit2Spender, approveAmountWei, Number(expiration48)]
1234
1160
  });
1235
- const approveP2Gas = await publicClient.estimateGas({
1236
- to: PERMIT2_ADDRESS,
1237
- data: permit2ApproveData,
1238
- value: 0n,
1239
- account: args.executorAddress
1240
- });
1241
- const gasLimitP2 = useCustomGas ? gasLimitFromEstimateAndChainConfig(approveP2Gas, gasLimitConfig) : approveP2Gas;
1242
- const currentNonceP2 = nonce + 1;
1243
- if (legacy) {
1244
- let gasPriceWeiP2 = await publicClient.getGasPrice();
1245
- if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
1246
- gasPriceWeiP2 = gasPriceWeiP2 * BigInt(100 + gasFeeMultiplier) / 100n;
1247
- }
1248
- if (useCustomGas && args.chainDetail?.gasPrice != null && args.chainDetail.gasPrice > 0) {
1249
- const configured = parseGwei(gweiToDecimalString(Number(args.chainDetail.gasPrice)));
1250
- if (configured > gasPriceWeiP2) gasPriceWeiP2 = configured;
1251
- }
1252
- const serP2 = serializeTransaction({
1253
- type: "legacy",
1254
- to: PERMIT2_ADDRESS,
1255
- data: permit2ApproveData,
1256
- value: 0n,
1257
- gas: gasLimitP2,
1258
- gasPrice: gasPriceWeiP2,
1259
- nonce: currentNonceP2,
1260
- chainId: args.chainId
1261
- });
1262
- const hP2 = keccak256(serP2);
1263
- messageHashes.push(hP2.startsWith("0x") ? hP2.slice(2) : hP2);
1264
- messageRawBatch.push(serP2);
1265
- proposalTxParamsBatch.push({
1266
- nonce: currentNonceP2,
1267
- gasLimit: gasLimitP2.toString(),
1268
- txType: "legacy",
1269
- gasPrice: gasPriceWeiP2.toString()
1270
- });
1271
- } else {
1272
- const fetchedBaseP2 = feeParams.baseFeeGwei ?? 0;
1273
- const fetchedPriorityP2 = feeParams.priorityFeeGwei ?? 0;
1274
- const configuredBaseP2 = useCustomGas && args.chainDetail?.baseFee != null ? Number(args.chainDetail.baseFee) : 0;
1275
- const configuredPriorityP2 = useCustomGas && args.chainDetail?.priorityFee != null ? Number(args.chainDetail.priorityFee) : 0;
1276
- const effectiveBaseFeeGweiP2 = Math.max(fetchedBaseP2, configuredBaseP2);
1277
- const effectivePriorityFeeGweiP2 = Math.max(fetchedPriorityP2, configuredPriorityP2);
1278
- const baseFeeMultiplierPctP2 = useCustomGas && args.chainDetail?.baseFeeMultiplier != null ? Math.max(100, Number(args.chainDetail.baseFeeMultiplier)) : 100;
1279
- const baseComponentGweiP2 = effectiveBaseFeeGweiP2 * baseFeeMultiplierPctP2 / 100;
1280
- const maxFeePerGasGweiP2 = baseComponentGweiP2 + effectivePriorityFeeGweiP2;
1281
- let maxPriorityFeePerGasP2 = effectivePriorityFeeGweiP2 > 0 ? parseGwei(gweiToDecimalString(effectivePriorityFeeGweiP2)) : parseGwei("1");
1282
- let maxFeePerGasP2 = parseGwei(gweiToDecimalString(maxFeePerGasGweiP2));
1283
- if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
1284
- maxPriorityFeePerGasP2 = maxPriorityFeePerGasP2 * BigInt(100 + gasFeeMultiplier) / 100n;
1285
- maxFeePerGasP2 = maxFeePerGasP2 * BigInt(100 + gasFeeMultiplier) / 100n;
1286
- }
1287
- ({ maxFeePerGas: maxFeePerGasP2, maxPriorityFeePerGas: maxPriorityFeePerGasP2 } = alignEip1559FeesWithLatestBase(maxFeePerGasP2, maxPriorityFeePerGasP2, latestBaseFeeWeiSkipBatch));
1288
- const serP2 = serializeTransaction({
1289
- type: "eip1559",
1290
- to: PERMIT2_ADDRESS,
1291
- data: permit2ApproveData,
1292
- value: 0n,
1293
- gas: gasLimitP2,
1294
- maxFeePerGas: maxFeePerGasP2,
1295
- maxPriorityFeePerGas: maxPriorityFeePerGasP2,
1296
- nonce: currentNonceP2,
1297
- chainId: args.chainId
1298
- });
1299
- const hP2 = keccak256(serP2);
1300
- messageHashes.push(hP2.startsWith("0x") ? hP2.slice(2) : hP2);
1301
- messageRawBatch.push(serP2);
1302
- proposalTxParamsBatch.push({
1303
- nonce: currentNonceP2,
1304
- gasLimit: gasLimitP2.toString(),
1305
- txType: "eip1559",
1306
- maxFeePerGas: maxFeePerGasP2.toString(),
1307
- maxPriorityFeePerGas: maxPriorityFeePerGasP2.toString()
1308
- });
1309
- }
1310
- }
1311
- const swapRecord = args.swap;
1312
- const fromTradeApi = parseOptionalGasLimitString(swapRecord.gasLimit) ?? parseOptionalGasLimitString(swapRecord.gas);
1313
- let gasBuildSource = "rpcEstimate";
1314
- let estimateGasError;
1315
- let baseGasUnits1;
1316
- if (fromTradeApi != null && fromTradeApi > 0n) {
1317
- baseGasUnits1 = fromTradeApi;
1318
- gasBuildSource = "tradeApi";
1319
- } else {
1320
- try {
1321
- baseGasUnits1 = await publicClient.estimateGas({
1322
- to: toRouter,
1323
- data: dataHex,
1324
- value: valueWei,
1325
- account: args.executorAddress
1326
- });
1327
- gasBuildSource = "rpcEstimate";
1328
- } catch (e) {
1329
- estimateGasError = e instanceof Error ? e.message : String(e);
1330
- const minRouterGas = 500000n;
1331
- if (useCustomGas) {
1332
- const cfg = args.chainDetail?.gasLimit != null ? parseOptionalGasLimitString(String(args.chainDetail.gasLimit)) : null;
1333
- if (cfg != null && cfg >= minRouterGas) {
1334
- baseGasUnits1 = cfg;
1335
- } else {
1336
- baseGasUnits1 = DEFAULT_UNIVERSAL_ROUTER_GAS_FALLBACK;
1337
- }
1338
- } else {
1339
- baseGasUnits1 = DEFAULT_UNIVERSAL_ROUTER_GAS_FALLBACK;
1340
- }
1341
- gasBuildSource = "estimateFailedFallback";
1342
- }
1343
- }
1344
- const gasLimit1 = routerSwapGasLimitFromEstimate(baseGasUnits1, chainGasLimitRouter);
1345
- const currentNonce1 = nonce + (usePermit2Triple ? 2 : 1);
1346
- if (legacy) {
1347
- let gasPriceWei1 = await publicClient.getGasPrice();
1348
- if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
1349
- gasPriceWei1 = gasPriceWei1 * BigInt(100 + gasFeeMultiplier) / 100n;
1350
- }
1351
- if (useCustomGas && args.chainDetail?.gasPrice != null && args.chainDetail.gasPrice > 0) {
1352
- const configured = parseGwei(gweiToDecimalString(Number(args.chainDetail.gasPrice)));
1353
- if (configured > gasPriceWei1) gasPriceWei1 = configured;
1354
- }
1355
- const ser1 = serializeTransaction({
1356
- type: "legacy",
1357
- to: toRouter,
1358
- data: dataHex,
1359
- value: valueWei,
1360
- gas: gasLimit1,
1361
- gasPrice: gasPriceWei1,
1362
- nonce: currentNonce1,
1363
- chainId: args.chainId
1364
- });
1365
- const h1 = keccak256(ser1);
1366
- messageHashes.push(h1.startsWith("0x") ? h1.slice(2) : h1);
1367
- messageRawBatch.push(ser1);
1368
- proposalTxParamsBatch.push({
1369
- nonce: currentNonce1,
1370
- gasLimit: gasLimit1.toString(),
1371
- txType: "legacy",
1372
- gasPrice: gasPriceWei1.toString()
1373
- });
1374
- } else {
1375
- const fetchedBase1 = feeParams.baseFeeGwei ?? 0;
1376
- const fetchedPriority1 = feeParams.priorityFeeGwei ?? 0;
1377
- const configuredBase1 = useCustomGas && args.chainDetail?.baseFee != null ? Number(args.chainDetail.baseFee) : 0;
1378
- const configuredPriority1 = useCustomGas && args.chainDetail?.priorityFee != null ? Number(args.chainDetail.priorityFee) : 0;
1379
- const effectiveBaseFeeGwei1 = Math.max(fetchedBase1, configuredBase1);
1380
- const effectivePriorityFeeGwei1 = Math.max(fetchedPriority1, configuredPriority1);
1381
- const baseFeeMultiplierPct1 = useCustomGas && args.chainDetail?.baseFeeMultiplier != null ? Math.max(100, Number(args.chainDetail.baseFeeMultiplier)) : 100;
1382
- const baseComponentGwei1 = effectiveBaseFeeGwei1 * baseFeeMultiplierPct1 / 100;
1383
- const maxFeePerGasGwei1 = baseComponentGwei1 + effectivePriorityFeeGwei1;
1384
- let maxPriorityFeePerGas1 = effectivePriorityFeeGwei1 > 0 ? parseGwei(gweiToDecimalString(effectivePriorityFeeGwei1)) : parseGwei("1");
1385
- let maxFeePerGas1 = parseGwei(gweiToDecimalString(maxFeePerGasGwei1));
1386
- if (useCustomGas && gasFeeMultiplier != null && gasFeeMultiplier > 0) {
1387
- maxPriorityFeePerGas1 = maxPriorityFeePerGas1 * BigInt(100 + gasFeeMultiplier) / 100n;
1388
- maxFeePerGas1 = maxFeePerGas1 * BigInt(100 + gasFeeMultiplier) / 100n;
1389
- }
1390
- ({ maxFeePerGas: maxFeePerGas1, maxPriorityFeePerGas: maxPriorityFeePerGas1 } = alignEip1559FeesWithLatestBase(maxFeePerGas1, maxPriorityFeePerGas1, latestBaseFeeWeiSkipBatch));
1391
- const ser1 = serializeTransaction({
1392
- type: "eip1559",
1393
- to: toRouter,
1394
- data: dataHex,
1395
- value: valueWei,
1396
- gas: gasLimit1,
1397
- maxFeePerGas: maxFeePerGas1,
1398
- maxPriorityFeePerGas: maxPriorityFeePerGas1,
1399
- nonce: currentNonce1,
1400
- chainId: args.chainId
1401
- });
1402
- const h1 = keccak256(ser1);
1403
- messageHashes.push(h1.startsWith("0x") ? h1.slice(2) : h1);
1404
- messageRawBatch.push(ser1);
1405
- proposalTxParamsBatch.push({
1406
- nonce: currentNonce1,
1407
- gasLimit: gasLimit1.toString(),
1408
- txType: "eip1559",
1409
- maxFeePerGas: maxFeePerGas1.toString(),
1410
- maxPriorityFeePerGas: maxPriorityFeePerGas1.toString()
1411
- });
1412
- }
1413
- const swapMsgIndex = usePermit2Triple ? 2 : 1;
1414
- const audit = {
1161
+ steps.push({ to: PERMIT2_ADDRESS, data: permit2ApproveData, value: 0n, fallbackGas: 100000n });
1162
+ }
1163
+ steps.push({
1164
+ to: toRouter,
1165
+ data: dataHex,
1166
+ value: valueWei,
1167
+ routerSwap: true,
1168
+ fallbackGas: DEFAULT_UNIVERSAL_ROUTER_GAS_FALLBACK
1169
+ });
1170
+ const swapStepIndex = steps.length - 1;
1171
+ const approveMsgRawNo0x = approveData.startsWith("0x") ? approveData.slice(2) : approveData;
1172
+ const purposeSuffix = usePermit2Triple ? "Uniswap V4: 3-tx batch (classic allowance) \u2014 (1) ERC-20 approve allowance hub, (2) hub approve(Universal Router), (3) swap (Trade /swap)." : "Uniswap V4: 2-tx batch (classic allowance, dispatcher) \u2014 (1) ERC-20 approve swap.to (dispatcher pulls tokens), (2) swap (Trade /swap).";
1173
+ const buildSwapAudit = () => ({
1415
1174
  skipPermit2Batch: true,
1416
1175
  approvalPath: usePermit2Triple ? "permit2_triple" : "dispatcher",
1417
1176
  approveAmount: {
@@ -1425,9 +1184,9 @@ async function buildEvmMultisignBodyUniswapV4SkipPermit2Batch(args) {
1425
1184
  requestId: args.createSwapResponse.requestId,
1426
1185
  gasFee: args.createSwapResponse.gasFee,
1427
1186
  gasBuildSwap: {
1428
- useCustomGas,
1187
+ useCustomGas: args.useCustomGas,
1429
1188
  source: gasBuildSource,
1430
- baseGasUnits: baseGasUnits1.toString(),
1189
+ baseGasUnits: swapBaseGasUnits.toString(),
1431
1190
  ...estimateGasError != null && estimateGasError !== "" ? { estimateGasError } : {}
1432
1191
  },
1433
1192
  swap: {
@@ -1469,92 +1228,98 @@ async function buildEvmMultisignBodyUniswapV4SkipPermit2Batch(args) {
1469
1228
  note: "Dispatcher pulls via ERC20.transferFrom(user, universalRouter, amount); allowance must be on swap.to (see cast trace TRANSFER_FROM_FAILED when only the hub is approved)."
1470
1229
  }
1471
1230
  }
1472
- };
1473
- const batchMeta = [
1474
- {
1475
- destinationAddress: tokenIn,
1476
- signatureText: JSON.stringify({
1477
- kind: "UniswapV4",
1478
- name: "ERC20.approve",
1479
- to: usePermit2Triple ? "allowance hub" : "dispatcher(swap.to)",
1480
- function: "approve(address spender, uint256 amount)",
1481
- spender: erc20ApproveSpender,
1482
- amountWei: approveAmountWei.toString()
1483
- }),
1484
- evm: {
1485
- type: usePermit2Triple ? "uniswap_v4_skip_permit2_approve" : "uniswap_v4_skip_permit2_dispatcher_approve",
1486
- version: 1,
1487
- chainId: String(args.chainId),
1488
- ...usePermit2Triple ? { permit2: PERMIT2_ADDRESS } : { dispatcher: toRouter }
1231
+ });
1232
+ return buildEvmMultisignBatch({
1233
+ context: {
1234
+ chainCategory: "evm",
1235
+ keyGen: args.keyGen,
1236
+ purposeText: args.purposeText,
1237
+ chainId: args.chainId,
1238
+ rpcUrl: args.rpcUrl,
1239
+ executorAddress: args.executorAddress,
1240
+ chainDetail: args.chainDetail,
1241
+ useCustomGas: args.useCustomGas,
1242
+ customGasChainDetails: args.customGasChainDetails
1243
+ },
1244
+ steps,
1245
+ purposeSuffix,
1246
+ firstMsgRawNo0x: approveMsgRawNo0x,
1247
+ destinationAddress: tokenIn,
1248
+ payableValueWei: valueWei > 0n ? valueWei : void 0,
1249
+ estimateGasForStep: async ({ step, index, publicClient, executor }) => {
1250
+ if (index !== swapStepIndex) {
1251
+ return publicClient.estimateGas({
1252
+ to: step.to,
1253
+ data: step.data,
1254
+ value: step.value,
1255
+ account: executor
1256
+ });
1489
1257
  }
1490
- }
1491
- ];
1492
- if (usePermit2Triple) {
1493
- batchMeta.push({
1494
- destinationAddress: PERMIT2_ADDRESS,
1495
- signatureText: JSON.stringify({
1496
- kind: "UniswapV4",
1497
- name: "AllowanceHub.approve",
1498
- function: "approve(address token, address spender, uint160 amount, uint48 expiration)",
1499
- token: tokenIn,
1500
- spender: permit2Spender,
1501
- amountWei: approveAmountWei.toString(),
1502
- expiration: expiration48.toString()
1503
- }),
1504
- evm: {
1505
- type: "uniswap_v4_skip_permit2_permit2_approve",
1506
- version: 1,
1507
- chainId: String(args.chainId),
1508
- permit2: PERMIT2_ADDRESS,
1509
- permit2Spender
1258
+ const r = await estimateUniswapRouterSwapGas({
1259
+ publicClient,
1260
+ executor,
1261
+ to: toRouter,
1262
+ data: dataHex,
1263
+ value: valueWei,
1264
+ swapRecord,
1265
+ useCustomGas: args.useCustomGas,
1266
+ chainDetail: args.chainDetail
1267
+ });
1268
+ gasBuildSource = r.source;
1269
+ estimateGasError = r.estimateGasError;
1270
+ swapBaseGasUnits = r.baseGasUnits;
1271
+ return r.baseGasUnits;
1272
+ },
1273
+ buildBatchMeta: ({ index }) => {
1274
+ if (index === 0) {
1275
+ return {
1276
+ signatureText: JSON.stringify({
1277
+ kind: "UniswapV4",
1278
+ name: "ERC20.approve",
1279
+ to: usePermit2Triple ? "allowance hub" : "dispatcher(swap.to)",
1280
+ function: "approve(address spender, uint256 amount)",
1281
+ spender: erc20ApproveSpender,
1282
+ amountWei: approveAmountWei.toString()
1283
+ }),
1284
+ evm: {
1285
+ type: usePermit2Triple ? "uniswap_v4_skip_permit2_approve" : "uniswap_v4_skip_permit2_dispatcher_approve",
1286
+ version: 1,
1287
+ chainId: String(args.chainId),
1288
+ ...usePermit2Triple ? { permit2: PERMIT2_ADDRESS } : { dispatcher: toRouter }
1289
+ }
1290
+ };
1510
1291
  }
1511
- });
1512
- }
1513
- batchMeta.push({
1514
- destinationAddress: toRouter,
1515
- signatureText: JSON.stringify({
1516
- kind: "UniswapV4",
1517
- name: "UniversalRouter.execute",
1518
- note: `Calldata from Trade API POST /swap; signed tx hash in messageHashes[${swapMsgIndex}].`
1519
- }),
1520
- evm: { type: "uniswap_v4_swap_tx", version: 1, chainId: String(args.chainId) },
1521
- uniswapV4: audit
1522
- });
1523
- const extraPayload = { batchMeta };
1524
- if (useCustomGas) {
1525
- const snap = args.customGasChainDetails;
1526
- if (snap && typeof snap === "object" && !Array.isArray(snap) && Object.keys(snap).length > 0) {
1527
- extraPayload.customGasChainDetails = snap;
1292
+ if (usePermit2Triple && index === 1) {
1293
+ return {
1294
+ signatureText: JSON.stringify({
1295
+ kind: "UniswapV4",
1296
+ name: "AllowanceHub.approve",
1297
+ function: "approve(address token, address spender, uint160 amount, uint48 expiration)",
1298
+ token: tokenIn,
1299
+ spender: permit2Spender,
1300
+ amountWei: approveAmountWei.toString(),
1301
+ expiration: expiration48.toString()
1302
+ }),
1303
+ evm: {
1304
+ type: "uniswap_v4_skip_permit2_permit2_approve",
1305
+ version: 1,
1306
+ chainId: String(args.chainId),
1307
+ permit2: PERMIT2_ADDRESS,
1308
+ permit2Spender
1309
+ }
1310
+ };
1311
+ }
1312
+ return {
1313
+ signatureText: JSON.stringify({
1314
+ kind: "UniswapV4",
1315
+ name: "UniversalRouter.execute",
1316
+ note: `Calldata from Trade API POST /swap; signed tx hash in messageHashes[${swapMsgIndex}].`
1317
+ }),
1318
+ evm: { type: "uniswap_v4_swap_tx", version: 1, chainId: String(args.chainId) },
1319
+ uniswapV4: buildSwapAudit()
1320
+ };
1528
1321
  }
1529
- }
1530
- const extraJSON = JSON.stringify(extraPayload);
1531
- const firstSigText = batchMeta[0].signatureText;
1532
- const bodyForSign = {
1533
- keyList,
1534
- pubKey: ph,
1535
- msgHash: messageHashes[0],
1536
- msgRaw: approveMsgRawNo0x,
1537
- messageHashes,
1538
- messageRawBatch,
1539
- destinationChainID: String(args.chainId),
1540
- destinationAddress: tokenIn,
1541
- extraJSON,
1542
- signatureText: firstSigText,
1543
- purpose: (() => {
1544
- const t = args.purposeText.trim();
1545
- const batchDesc = usePermit2Triple ? "Uniswap V4: 3-tx batch (classic allowance) \u2014 (1) ERC-20 approve allowance hub, (2) hub approve(Universal Router), (3) swap (Trade /swap)." : "Uniswap V4: 2-tx batch (classic allowance, dispatcher) \u2014 (1) ERC-20 approve swap.to (dispatcher pulls tokens), (2) swap (Trade /swap).";
1546
- return (t ? `${t}
1547
-
1548
- ` : "") + batchDesc;
1549
- })(),
1550
- ...firstTxFeePayload,
1551
- proposalTxParams: proposalTxParamsBatch
1552
- };
1553
- if (valueWei > 0n) {
1554
- bodyForSign.value = valueWei.toString();
1555
- }
1556
- if (clientId) bodyForSign.clientId = clientId;
1557
- return { bodyForSign, messageToSign: JSON.stringify(bodyForSign) };
1322
+ });
1558
1323
  }
1559
1324
 
1560
1325
  // src/protocols/evm/uniswap-v4/support.ts