UNPKG

@tunghm/relay-kit

Version:

SDK for Safe Smart Accounts with support for ERC-4337 and Relay

1,391 lines (1,369 loc) 75.3 kB
// src/packs/gelato/GelatoRelayPack.ts import { GelatoRelay as GelatoNetworkRelay } from "@gelatonetwork/relay-sdk"; import { estimateTxBaseGas, estimateSafeTxGas, estimateSafeDeploymentGas, createERC20TokenTransferTransaction, isGasTokenCompatibleWithHandlePayment } from "@safe-global/protocol-kit"; // src/RelayKitBasePack.ts var RelayKitBasePack = class { /** * Creates a new RelayKitBasePack instance. * The packs implemented using our SDK should extend this class and therefore provide a Safe SDK instance * @param {Safe} protocolKit - The Safe SDK instance */ constructor(protocolKit) { this.protocolKit = protocolKit; } }; // src/constants.ts var GELATO_NATIVE_TOKEN_ADDRESS = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; var GELATO_FEE_COLLECTOR = "0x3AC05161b76a35c1c28dC99Aa01BEd7B24cEA3bf"; var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; var GELATO_GAS_EXECUTION_OVERHEAD = 15e4; var GELATO_TRANSFER_GAS_COST = 15e3; // src/packs/gelato/GelatoRelayPack.ts var GelatoRelayPack = class extends RelayKitBasePack { #gelatoRelay; #apiKey; constructor({ apiKey, protocolKit }) { super(protocolKit); this.#gelatoRelay = new GelatoNetworkRelay(); this.#apiKey = apiKey; } _getFeeToken(gasToken) { return !gasToken || gasToken === ZERO_ADDRESS ? GELATO_NATIVE_TOKEN_ADDRESS : gasToken; } getFeeCollector() { return GELATO_FEE_COLLECTOR; } async getEstimateFee(propsOrChainId, inputGasLimit, inputGasToken) { let chainId; let gasLimit; let gasToken; if (typeof propsOrChainId === "object") { ; ({ chainId, gasLimit, gasToken } = propsOrChainId); } else { chainId = propsOrChainId; gasLimit = inputGasLimit; gasToken = inputGasToken; } const feeToken = this._getFeeToken(gasToken); const estimation = await this.#gelatoRelay.getEstimatedFee( chainId, feeToken, BigInt(gasLimit), false ); return estimation.toString(); } async getTaskStatus(taskId) { return this.#gelatoRelay.getTaskStatus(taskId); } /** * Creates a payment transaction to Gelato * * @private * @async * @function * @param {string} gas - The gas amount for the payment. * @param {MetaTransactionOptions} options - Options for the meta transaction. * @returns {Promise<Transaction>} Promise object representing the created payment transaction. * */ async createPaymentToGelato(gas, options) { const chainId = await this.protocolKit.getChainId(); const gelatoAddress = this.getFeeCollector(); const gasToken = options.gasToken ?? ZERO_ADDRESS; const paymentToGelato = await this.getEstimateFee({ chainId, gasLimit: gas, gasToken }); const transferToGelato = createERC20TokenTransferTransaction( gasToken, gelatoAddress, paymentToGelato ); return transferToGelato; } /** * @deprecated Use createTransaction instead */ async createRelayedTransaction({ transactions, onlyCalls = false, options = {} }) { return this.createTransaction({ transactions, onlyCalls, options }); } /** * Creates a Safe transaction designed to be executed using the Gelato Relayer. * * @param {GelatoCreateTransactionProps} options - Options for Gelato. * @param {MetaTransactionData[]} [options.transactions] - The transactions batch. * @param {boolean} [options.onlyCalls=false] - If true, MultiSendCallOnly contract should be used. Remember to not use delegate calls in the batch. * @param {MetaTransactionOptions} [options.options={}] - Gas Options for the transaction batch. * @returns {Promise<SafeTransaction>} Returns a Promise that resolves with a SafeTransaction object. */ async createTransaction({ transactions, onlyCalls = false, options = {} }) { const { isSponsored = false } = options; if (isSponsored) { const nonce = await this.protocolKit.getNonce(); const sponsoredTransaction = await this.protocolKit.createTransaction({ transactions, onlyCalls, options: { nonce } }); return sponsoredTransaction; } const gasToken = options.gasToken ?? ZERO_ADDRESS; const isGasTokenCompatible = await isGasTokenCompatibleWithHandlePayment( gasToken, this.protocolKit ); if (!isGasTokenCompatible) { return this.createTransactionWithTransfer({ transactions, onlyCalls, options }); } return this.createTransactionWithHandlePayment({ transactions, onlyCalls, options }); } /** * Creates a Safe transaction designed to be executed using the Gelato Relayer and * uses the handlePayment function defined in the Safe contract to pay the fees * to the Gelato relayer. * * @async * @function createTransactionWithHandlePayment * @param {GelatoCreateTransactionProps} options - Options for Gelato. * @param {MetaTransactionData[]} [options.transactions] - The transactions batch. * @param {boolean} [options.onlyCalls=false] - If true, MultiSendCallOnly contract should be used. Remember to not use delegate calls in the batch. * @param {MetaTransactionOptions} [options.options={}] - Gas Options for the transaction batch. * @returns {Promise<SafeTransaction>} Returns a promise that resolves to the created SafeTransaction. * @private */ async createTransactionWithHandlePayment({ transactions, onlyCalls = false, options = {} }) { const { gasLimit } = options; const nonce = await this.protocolKit.getNonce(); const transactionToEstimateGas = await this.protocolKit.createTransaction({ transactions, onlyCalls, options: { nonce } }); const gasPrice = "1"; const safeTxGas = await estimateSafeTxGas(this.protocolKit, transactionToEstimateGas); const gasToken = options.gasToken ?? ZERO_ADDRESS; const refundReceiver = this.getFeeCollector(); const chainId = await this.protocolKit.getChainId(); if (gasLimit) { const paymentToGelato2 = await this.getEstimateFee({ chainId, gasLimit, gasToken }); const syncTransaction2 = await this.protocolKit.createTransaction({ transactions, onlyCalls, options: { baseGas: paymentToGelato2, gasPrice, safeTxGas, gasToken, refundReceiver, nonce } }); return syncTransaction2; } const baseGas = await estimateTxBaseGas(this.protocolKit, transactionToEstimateGas); const safeDeploymentGasCost = await estimateSafeDeploymentGas(this.protocolKit); const totalGas = Number(baseGas) + // baseGas Number(safeTxGas) + // safeTxGas Number(safeDeploymentGasCost) + // Safe deploymet gas cost if it is required GELATO_GAS_EXECUTION_OVERHEAD; const paymentToGelato = await this.getEstimateFee({ chainId, gasLimit: String(totalGas), gasToken }); const syncTransaction = await this.protocolKit.createTransaction({ transactions, onlyCalls, options: { baseGas: paymentToGelato, // payment to Gelato gasPrice, safeTxGas, gasToken, refundReceiver, nonce } }); return syncTransaction; } /** * Creates a Safe transaction designed to be executed using the Gelato Relayer and * uses a separate ERC20 transfer to pay the fees to the Gelato relayer. * * @async * @function createTransactionWithTransfer * @param {GelatoCreateTransactionProps} options - Options for Gelato. * @param {MetaTransactionData[]} [options.transactions] - The transactions batch. * @param {boolean} [options.onlyCalls=false] - If true, MultiSendCallOnly contract should be used. Remember to not use delegate calls in the batch. * @param {MetaTransactionOptions} [options.options={}] - Gas Options for the transaction batch. * @returns {Promise<SafeTransaction>} Returns a promise that resolves to the created SafeTransaction. * @private */ async createTransactionWithTransfer({ transactions, onlyCalls = false, options = {} }) { const { gasLimit } = options; const nonce = await this.protocolKit.getNonce(); const gasToken = options.gasToken ?? ZERO_ADDRESS; if (gasLimit) { const transferToGelato2 = await this.createPaymentToGelato(gasLimit, options); const syncTransaction2 = await this.protocolKit.createTransaction({ transactions: [...transactions, transferToGelato2], onlyCalls, options: { nonce, gasToken } }); return syncTransaction2; } const transactionToEstimateGas = await this.protocolKit.createTransaction({ transactions, onlyCalls, options: { nonce } }); const safeTxGas = await estimateSafeTxGas(this.protocolKit, transactionToEstimateGas); const baseGas = await estimateTxBaseGas(this.protocolKit, transactionToEstimateGas); const safeDeploymentGasCost = await estimateSafeDeploymentGas(this.protocolKit); const totalGas = Number(baseGas) + // baseGas Number(safeTxGas) + // safeTxGas without Gelato payment transfer Number(safeDeploymentGasCost) + // Safe deploymet gas cost if it is required GELATO_TRANSFER_GAS_COST + // Gelato payment transfer GELATO_GAS_EXECUTION_OVERHEAD; const transferToGelato = await this.createPaymentToGelato(String(totalGas), options); const syncTransaction = await this.protocolKit.createTransaction({ transactions: [...transactions, transferToGelato], onlyCalls, options: { nonce, gasToken } }); return syncTransaction; } async sendSponsorTransaction(target, encodedTransaction, chainId) { if (!this.#apiKey) { throw new Error("API key not defined"); } const request = { chainId, target, data: encodedTransaction }; const response = await this.#gelatoRelay.sponsoredCall(request, this.#apiKey); return response; } async sendSyncTransaction(target, encodedTransaction, chainId, options) { const { gasLimit, gasToken } = options; const feeToken = this._getFeeToken(gasToken); const request = { chainId, target, data: encodedTransaction, feeToken, isRelayContext: false }; const relayRequestOptions = { gasLimit: gasLimit ? BigInt(gasLimit) : void 0 }; const response = await this.#gelatoRelay.callWithSyncFee(request, relayRequestOptions); return response; } async relayTransaction({ target, encodedTransaction, chainId, options = {} }) { const response = options.isSponsored ? this.sendSponsorTransaction(target, encodedTransaction, chainId) : this.sendSyncTransaction(target, encodedTransaction, chainId, options); return response; } /** * @deprecated Use executeTransaction instead */ async executeRelayTransaction(safeTransaction, options) { return this.executeTransaction({ executable: safeTransaction, options }); } /** * Sends the Safe transaction to the Gelato Relayer for execution. * If the Safe is not deployed, it creates a batch of transactions including the Safe deployment transaction. * * @param {GelatoExecuteTransactionProps} props - Execution props * @param {SafeTransaction} props.executable - The Safe transaction to be executed. * @param {MetaTransactionOptions} props.options - Options for the transaction. * @returns {Promise<RelayResponse>} Returns a Promise that resolves with a RelayResponse object. */ async executeTransaction({ executable: safeTransaction, options }) { const isSafeDeployed = await this.protocolKit.isSafeDeployed(); const chainId = await this.protocolKit.getChainId(); const safeAddress = await this.protocolKit.getAddress(); const safeTransactionEncodedData = await this.protocolKit.getEncodedTransaction(safeTransaction); const gasToken = options?.gasToken || safeTransaction.data.gasToken; if (isSafeDeployed) { const relayTransaction2 = { target: safeAddress, encodedTransaction: safeTransactionEncodedData, chainId, options: { ...options, gasToken } }; return this.relayTransaction(relayTransaction2); } const safeDeploymentBatch = await this.protocolKit.wrapSafeTransactionIntoDeploymentBatch(safeTransaction); const relayTransaction = { target: safeDeploymentBatch.to, // multiSend Contract address encodedTransaction: safeDeploymentBatch.data, chainId, options: { ...options, gasToken } }; return this.relayTransaction(relayTransaction); } }; // src/packs/safe-4337/Safe4337Pack.ts import { getAddress as getAddress2, isAddress as isAddress2, toHex as toHex9 } from "viem"; import semverSatisfies from "semver/functions/satisfies.js"; import Safe, { EthSafeSignature as EthSafeSignature2, encodeMultiSendData as encodeMultiSendData2, getMultiSendContract, SafeProvider, generateOnChainIdentifier } from "@safe-global/protocol-kit"; import { OperationType as OperationType3, SigningMethod } from "@safe-global/types-kit"; import { getSafeModuleSetupDeployment, getSafe4337ModuleDeployment, getSafeWebAuthnShareSignerDeployment } from "@safe-global/safe-modules-deployments"; import { encodeFunctionData as encodeFunctionData3, zeroAddress, concat as concat2 } from "viem"; // src/packs/safe-4337/BaseSafeOperation.ts import { encodePacked, hashTypedData } from "viem"; import { buildSignatureBytes } from "@safe-global/protocol-kit"; var BaseSafeOperation = class { constructor(userOperation, options) { this.signatures = /* @__PURE__ */ new Map(); this.userOperation = userOperation; this.options = options; } /** * Helper to apply percentage increase to a bigint value. * @param value - The original value * @param percentage - The percentage to add (e.g., 50 means +50%) * @returns The adjusted value */ applyPercentageIncrease(value, percentage) { if (percentage <= 0) return value; return value + value * BigInt(Math.floor(percentage)) / 100n; } getSignature(signer) { return this.signatures.get(signer.toLowerCase()); } addSignature(signature) { this.signatures.set(signature.signer.toLowerCase(), signature); } encodedSignatures() { return buildSignatureBytes(Array.from(this.signatures.values())); } getUserOperation() { return { ...this.userOperation, signature: encodePacked( ["uint48", "uint48", "bytes"], [ this.options.validAfter || 0, this.options.validUntil || 0, this.encodedSignatures() ] ) }; } getHash() { return hashTypedData({ domain: { chainId: Number(this.options.chainId), verifyingContract: this.options.moduleAddress }, types: this.getEIP712Type(), primaryType: "SafeOp", message: this.getSafeOperation() }); } }; var BaseSafeOperation_default = BaseSafeOperation; // src/packs/safe-4337/constants.ts import { parseAbi } from "viem"; var DEFAULT_SAFE_VERSION = "1.4.1"; var DEFAULT_SAFE_MODULES_VERSION = "0.2.0"; var EIP712_SAFE_OPERATION_TYPE_V06 = { SafeOp: [ { type: "address", name: "safe" }, { type: "uint256", name: "nonce" }, { type: "bytes", name: "initCode" }, { type: "bytes", name: "callData" }, { type: "uint256", name: "callGasLimit" }, { type: "uint256", name: "verificationGasLimit" }, { type: "uint256", name: "preVerificationGas" }, { type: "uint256", name: "maxFeePerGas" }, { type: "uint256", name: "maxPriorityFeePerGas" }, { type: "bytes", name: "paymasterAndData" }, { type: "uint48", name: "validAfter" }, { type: "uint48", name: "validUntil" }, { type: "address", name: "entryPoint" } ] }; var EIP712_SAFE_OPERATION_TYPE_V07 = { SafeOp: [ { type: "address", name: "safe" }, { type: "uint256", name: "nonce" }, { type: "bytes", name: "initCode" }, { type: "bytes", name: "callData" }, { type: "uint128", name: "verificationGasLimit" }, { type: "uint128", name: "callGasLimit" }, { type: "uint256", name: "preVerificationGas" }, { type: "uint128", name: "maxPriorityFeePerGas" }, { type: "uint128", name: "maxFeePerGas" }, { type: "bytes", name: "paymasterAndData" }, { type: "uint48", name: "validAfter" }, { type: "uint48", name: "validUntil" }, { type: "address", name: "entryPoint" } ] }; var ABI = parseAbi([ "function enableModules(address[])", "function multiSend(bytes memory transactions) public payable", "function executeUserOp(address to, uint256 value, bytes data, uint8 operation)", "function approve(address _spender, uint256 _value)", "function configure((uint256 x, uint256 y, uint176 verifiers) signer)" ]); var ENTRYPOINT_ABI = [ { inputs: [ { name: "sender", type: "address" }, { name: "key", type: "uint192" } ], name: "getNonce", outputs: [{ name: "nonce", type: "uint256" }], stateMutability: "view", type: "function" } ]; var ENTRYPOINT_ADDRESS_V06 = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; var ENTRYPOINT_ADDRESS_V07 = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"; // src/packs/safe-4337/SafeOperationV06.ts var SafeOperationV06 = class extends BaseSafeOperation_default { constructor(userOperation, options) { super(userOperation, options); } addEstimations(estimations) { this.userOperation.maxFeePerGas = BigInt( estimations.maxFeePerGas || this.userOperation.maxFeePerGas ); this.userOperation.maxPriorityFeePerGas = BigInt( estimations.maxPriorityFeePerGas || this.userOperation.maxPriorityFeePerGas ); this.userOperation.verificationGasLimit = BigInt( estimations.verificationGasLimit || this.userOperation.verificationGasLimit ); this.userOperation.preVerificationGas = BigInt( estimations.preVerificationGas || this.userOperation.preVerificationGas ); this.userOperation.callGasLimit = BigInt( estimations.callGasLimit || this.userOperation.callGasLimit ); this.userOperation.paymasterAndData = estimations.paymasterAndData || this.userOperation.paymasterAndData; } adjustEstimations(adjustments) { if (adjustments.maxFeePerGas) { this.userOperation.maxFeePerGas = this.applyPercentageIncrease( this.userOperation.maxFeePerGas, adjustments.maxFeePerGas ); } if (adjustments.maxPriorityFeePerGas) { this.userOperation.maxPriorityFeePerGas = this.applyPercentageIncrease( this.userOperation.maxPriorityFeePerGas, adjustments.maxPriorityFeePerGas ); } if (adjustments.verificationGasLimit) { this.userOperation.verificationGasLimit = this.applyPercentageIncrease( this.userOperation.verificationGasLimit, adjustments.verificationGasLimit ); } if (adjustments.preVerificationGas) { this.userOperation.preVerificationGas = this.applyPercentageIncrease( this.userOperation.preVerificationGas, adjustments.preVerificationGas ); } if (adjustments.callGasLimit) { this.userOperation.callGasLimit = this.applyPercentageIncrease( this.userOperation.callGasLimit, adjustments.callGasLimit ); } } getSafeOperation() { return { safe: this.userOperation.sender, nonce: this.userOperation.nonce, initCode: this.userOperation.initCode, callData: this.userOperation.callData, callGasLimit: this.userOperation.callGasLimit, verificationGasLimit: this.userOperation.verificationGasLimit, preVerificationGas: this.userOperation.preVerificationGas, maxFeePerGas: this.userOperation.maxFeePerGas, maxPriorityFeePerGas: this.userOperation.maxPriorityFeePerGas, paymasterAndData: this.userOperation.paymasterAndData, validAfter: this.options.validAfter || 0, validUntil: this.options.validUntil || 0, entryPoint: this.options.entryPoint }; } getEIP712Type() { return EIP712_SAFE_OPERATION_TYPE_V06; } }; var SafeOperationV06_default = SafeOperationV06; // src/packs/safe-4337/SafeOperationV07.ts import { concat, isAddress, pad, toHex } from "viem"; var SafeOperationV07 = class extends BaseSafeOperation_default { constructor(userOperation, options) { super(userOperation, options); } addEstimations(estimations) { this.userOperation.maxFeePerGas = BigInt( estimations.maxFeePerGas || this.userOperation.maxFeePerGas ); this.userOperation.maxPriorityFeePerGas = BigInt( estimations.maxPriorityFeePerGas || this.userOperation.maxPriorityFeePerGas ); this.userOperation.verificationGasLimit = BigInt( estimations.verificationGasLimit || this.userOperation.verificationGasLimit ); this.userOperation.preVerificationGas = BigInt( estimations.preVerificationGas || this.userOperation.preVerificationGas ); this.userOperation.callGasLimit = BigInt( estimations.callGasLimit || this.userOperation.callGasLimit ); this.userOperation.paymasterPostOpGasLimit = estimations.paymasterPostOpGasLimit ? BigInt(estimations.paymasterPostOpGasLimit) : this.userOperation.paymasterPostOpGasLimit; this.userOperation.paymasterVerificationGasLimit = estimations.paymasterVerificationGasLimit ? BigInt(estimations.paymasterVerificationGasLimit) : this.userOperation.paymasterVerificationGasLimit; this.userOperation.paymaster = estimations.paymaster || this.userOperation.paymaster; this.userOperation.paymasterData = estimations.paymasterData || this.userOperation.paymasterData; } adjustEstimations(adjustments) { if (adjustments.maxFeePerGas) { this.userOperation.maxFeePerGas = this.applyPercentageIncrease( this.userOperation.maxFeePerGas, adjustments.maxFeePerGas ); } if (adjustments.maxPriorityFeePerGas) { this.userOperation.maxPriorityFeePerGas = this.applyPercentageIncrease( this.userOperation.maxPriorityFeePerGas, adjustments.maxPriorityFeePerGas ); } if (adjustments.verificationGasLimit) { this.userOperation.verificationGasLimit = this.applyPercentageIncrease( this.userOperation.verificationGasLimit, adjustments.verificationGasLimit ); } if (adjustments.preVerificationGas) { this.userOperation.preVerificationGas = this.applyPercentageIncrease( this.userOperation.preVerificationGas, adjustments.preVerificationGas ); } if (adjustments.callGasLimit) { this.userOperation.callGasLimit = this.applyPercentageIncrease( this.userOperation.callGasLimit, adjustments.callGasLimit ); } if (adjustments.paymasterVerificationGasLimit && this.userOperation.paymasterVerificationGasLimit) { this.userOperation.paymasterVerificationGasLimit = this.applyPercentageIncrease( this.userOperation.paymasterVerificationGasLimit, adjustments.paymasterVerificationGasLimit ); } if (adjustments.paymasterPostOpGasLimit && this.userOperation.paymasterPostOpGasLimit) { this.userOperation.paymasterPostOpGasLimit = this.applyPercentageIncrease( this.userOperation.paymasterPostOpGasLimit, adjustments.paymasterPostOpGasLimit ); } } getSafeOperation() { const initCode = this.userOperation.factory ? concat([ this.userOperation.factory, this.userOperation.factoryData || "0x" ]) : "0x"; const paymasterAndData = isAddress(this.userOperation.paymaster || "") ? concat([ this.userOperation.paymaster, pad(toHex(this.userOperation.paymasterVerificationGasLimit || 0n), { size: 16 }), pad(toHex(this.userOperation.paymasterPostOpGasLimit || 0n), { size: 16 }), this.userOperation.paymasterData || "0x" ]) : "0x"; return { safe: this.userOperation.sender, nonce: this.userOperation.nonce, initCode, callData: this.userOperation.callData, callGasLimit: this.userOperation.callGasLimit, verificationGasLimit: this.userOperation.verificationGasLimit, preVerificationGas: this.userOperation.preVerificationGas, maxFeePerGas: this.userOperation.maxFeePerGas, maxPriorityFeePerGas: this.userOperation.maxPriorityFeePerGas, paymasterAndData, validAfter: this.options.validAfter || 0, validUntil: this.options.validUntil || 0, entryPoint: this.options.entryPoint }; } getEIP712Type() { return EIP712_SAFE_OPERATION_TYPE_V07; } }; var SafeOperationV07_default = SafeOperationV07; // src/packs/safe-4337/utils/index.ts import { createPublicClient, encodeFunctionData as encodeFunctionData2, http, rpcSchema } from "viem"; import { OperationType as OperationType2 } from "@safe-global/types-kit"; import { encodeMultiSendData } from "@safe-global/protocol-kit"; // src/packs/safe-4337/utils/entrypoint.ts var EQ_0_2_0 = "0.2.0"; var EQ_OR_GT_0_3_0 = ">=0.3.0"; function sameString(str1, str2) { return str1.toLowerCase() === str2.toLowerCase(); } function entryPointToSafeModules(entryPoint) { const moduleVersionToEntryPoint = { [ENTRYPOINT_ADDRESS_V06]: EQ_0_2_0, [ENTRYPOINT_ADDRESS_V07]: EQ_OR_GT_0_3_0 }; return moduleVersionToEntryPoint[entryPoint]; } function isEntryPointV6(address) { return sameString(address, ENTRYPOINT_ADDRESS_V06); } function isEntryPointV7(address) { return sameString(address, ENTRYPOINT_ADDRESS_V07); } async function getSafeNonceFromEntrypoint(protocolKit, safeAddress, entryPointAddress) { const safeProvider = protocolKit.getSafeProvider(); const newNonce = await safeProvider.readContract({ address: entryPointAddress || "0x", abi: ENTRYPOINT_ABI, functionName: "getNonce", args: [safeAddress, 0n] }); return newNonce; } // src/packs/safe-4337/utils/signing.ts import { encodePacked as encodePacked2, toHex as toHex2 } from "viem"; import { EthSafeSignature, buildSignatureBytes as buildSignatureBytes2 } from "@safe-global/protocol-kit"; var DUMMY_CLIENT_DATA_FIELDS = [ `"origin":"https://safe.global"`, `"padding":"This pads the clientDataJSON so that we can leave room for additional implementation specific fields for a more accurate 'preVerificationGas' estimate."` ].join(","); var DUMMY_AUTHENTICATOR_DATA = new Uint8Array(37); DUMMY_AUTHENTICATOR_DATA.fill(254); DUMMY_AUTHENTICATOR_DATA[32] = 4; function getDummySignature(signer, threshold) { const signatures = []; for (let i = 0; i < threshold; i++) { const isContractSignature = true; const passkeySignature = getSignatureBytes({ authenticatorData: DUMMY_AUTHENTICATOR_DATA, clientDataFields: DUMMY_CLIENT_DATA_FIELDS, r: BigInt(`0x${"ec".repeat(32)}`), s: BigInt(`0x${"d5a".repeat(21)}f`) }); signatures.push(new EthSafeSignature(signer, passkeySignature, isContractSignature)); } return encodePacked2(["uint48", "uint48", "bytes"], [0, 0, buildSignatureBytes2(signatures)]); } function getSignatureBytes({ authenticatorData, clientDataFields, r, s }) { const encodeUint256 = (x) => x.toString(16).padStart(64, "0"); const byteSize = (data) => 32 * (Math.ceil(data.length / 32) + 1); const encodeBytes = (data) => `${encodeUint256(data.length)}${toHex2(data).slice(2)}`.padEnd(byteSize(data) * 2, "0"); const authenticatorDataOffset = 32 * 4; const clientDataFieldsOffset = authenticatorDataOffset + byteSize(authenticatorData); return "0x" + encodeUint256(authenticatorDataOffset) + encodeUint256(clientDataFieldsOffset) + encodeUint256(r) + encodeUint256(s) + encodeBytes(authenticatorData) + encodeBytes(new TextEncoder().encode(clientDataFields)); } // src/packs/safe-4337/utils/userOperations.ts import { encodeFunctionData, getAddress, hexToBytes, sliceHex, toHex as toHex3 } from "viem"; import { OperationType } from "@safe-global/types-kit"; function encodeExecuteUserOpCallData(transaction) { return encodeFunctionData({ abi: ABI, functionName: "executeUserOp", args: [ transaction.to, BigInt(transaction.value), transaction.data, transaction.operation || OperationType.Call ] }); } async function getCallData(protocolKit, transactions, paymasterOptions, amountToApprove) { if (amountToApprove) { const approveToPaymasterTransaction = { to: paymasterOptions.paymasterTokenAddress, data: encodeFunctionData({ abi: ABI, functionName: "approve", args: [paymasterOptions.paymasterAddress, amountToApprove] }), value: "0", operation: OperationType.Call // Call for approve }; transactions.push(approveToPaymasterTransaction); } const isBatch = transactions.length > 1; const multiSendAddress = protocolKit.getMultiSendAddress(); const callData = isBatch ? encodeExecuteUserOpCallData({ to: multiSendAddress, value: "0", data: encodeMultiSendCallData(transactions), operation: OperationType.DelegateCall }) : encodeExecuteUserOpCallData(transactions[0]); return callData; } function unpackInitCode(initCode) { const initCodeBytes = hexToBytes(initCode); return initCodeBytes.length > 0 ? { factory: getAddress(sliceHex(initCode, 0, 20)), factoryData: sliceHex(initCode, 20) } : {}; } async function createUserOperation(protocolKit, transactions, { amountToApprove, entryPoint, paymasterOptions, customNonce }) { const safeAddress = await protocolKit.getAddress(); const nonce = customNonce || await getSafeNonceFromEntrypoint(protocolKit, safeAddress, entryPoint); const isSafeDeployed = await protocolKit.isSafeDeployed(); const paymasterAndData = paymasterOptions && "paymasterAddress" in paymasterOptions ? paymasterOptions.paymasterAddress : "0x"; const callData = await getCallData( protocolKit, transactions, paymasterOptions, amountToApprove ); const initCode = isSafeDeployed ? "0x" : await protocolKit.getInitCode(); if (isEntryPointV6(entryPoint)) { return { sender: safeAddress, nonce: nonce.toString(), initCode, callData, callGasLimit: 0n, verificationGasLimit: 0n, preVerificationGas: 0n, maxFeePerGas: 1n, maxPriorityFeePerGas: 1n, paymasterAndData, signature: "0x" }; } return { sender: safeAddress, nonce: nonce.toString(), ...unpackInitCode(initCode), callData, callGasLimit: 0n, verificationGasLimit: 0n, preVerificationGas: 0n, maxFeePerGas: 1n, maxPriorityFeePerGas: 1n, paymaster: paymasterAndData, paymasterData: "0x", paymasterVerificationGasLimit: void 0, paymasterPostOpGasLimit: void 0, signature: "0x" }; } function userOperationToHexValues(userOperation, entryPointAddress) { const userOpV07 = userOperation; const userOperationWithHexValues = { ...userOperation, nonce: toHex3(BigInt(userOperation.nonce)), callGasLimit: toHex3(userOperation.callGasLimit), verificationGasLimit: toHex3(userOperation.verificationGasLimit), preVerificationGas: toHex3(userOperation.preVerificationGas), maxFeePerGas: toHex3(userOperation.maxFeePerGas), maxPriorityFeePerGas: toHex3(userOperation.maxPriorityFeePerGas), ...isEntryPointV7(entryPointAddress) ? { // Only include paymaster fields if paymaster is actually used (not empty "0x") ...userOpV07.paymaster && userOpV07.paymaster !== "0x" ? { paymaster: userOpV07.paymaster, paymasterData: userOpV07.paymasterData || "0x", paymasterVerificationGasLimit: userOpV07.paymasterVerificationGasLimit ? toHex3(userOpV07.paymasterVerificationGasLimit) : void 0, paymasterPostOpGasLimit: userOpV07.paymasterPostOpGasLimit ? toHex3(userOpV07.paymasterPostOpGasLimit) : void 0 } : {} } : {} }; if (isEntryPointV7(entryPointAddress) && (!userOpV07.paymaster || userOpV07.paymaster === "0x")) { delete userOperationWithHexValues.paymaster; delete userOperationWithHexValues.paymasterData; delete userOperationWithHexValues.paymasterVerificationGasLimit; delete userOperationWithHexValues.paymasterPostOpGasLimit; } return userOperationWithHexValues; } // src/packs/safe-4337/utils/getRelayKitVersion.ts var getRelayKitVersion = () => "4.4.1"; // src/packs/safe-4337/utils/encodeNonce.ts import { toHex as toHex4 } from "viem"; function encodeNonce(args) { const key = BigInt(toHex4(args.key, { size: 24 })); const sequence = BigInt(toHex4(args.sequence, { size: 8 })); return (key << BigInt(64)) + sequence; } // src/packs/safe-4337/utils/index.ts function createBundlerClient(bundlerUrl) { const provider = createPublicClient({ transport: http(bundlerUrl), rpcSchema: rpcSchema() }); return provider; } function encodeMultiSendCallData(transactions) { return encodeFunctionData2({ abi: ABI, functionName: "multiSend", args: [ encodeMultiSendData( transactions.map((tx) => ({ ...tx, operation: tx.operation ?? OperationType2.Call })) ) ] }); } // src/packs/safe-4337/SafeOperationFactory.ts var SafeOperationFactory = class { /** * Creates a new SafeOperation with proper validation * @param userOperation - The base user operation * @param options - Configuration options * @returns Validated SafeOperation instance */ static createSafeOperation(userOperation, options) { if (isEntryPointV6(options.entryPoint)) { return new SafeOperationV06_default(userOperation, options); } return new SafeOperationV07_default(userOperation, options); } }; var SafeOperationFactory_default = SafeOperationFactory; // src/packs/safe-4337/estimators/pimlico/PimlicoFeeEstimator.ts import { toHex as toHex5 } from "viem"; var PimlicoFeeEstimator = class { async preEstimateUserOperationGas({ bundlerUrl, userOperation, entryPoint, paymasterOptions, protocolKit }) { const bundlerClient = createBundlerClient(bundlerUrl); const feeData = await this.#getUserOperationGasPrices(bundlerClient); const chainId = await protocolKit.getChainId(); let paymasterStubData = {}; if (paymasterOptions) { const paymasterClient = createBundlerClient( paymasterOptions.paymasterUrl ); const context = "paymasterTokenAddress" in paymasterOptions ? { token: paymasterOptions.paymasterTokenAddress } : void 0; paymasterStubData = await paymasterClient.request({ method: "pm_getPaymasterStubData" /* GET_PAYMASTER_STUB_DATA */, params: [ userOperationToHexValues(userOperation, entryPoint), entryPoint, toHex5(chainId), context ] }); } return { ...feeData, ...paymasterStubData }; } async postEstimateUserOperationGas({ userOperation, entryPoint, paymasterOptions, protocolKit }) { if (!paymasterOptions) return {}; const paymasterClient = createBundlerClient( paymasterOptions.paymasterUrl ); if (paymasterOptions.isSponsored) { const params = [ userOperationToHexValues(userOperation, entryPoint), entryPoint ]; if (paymasterOptions.sponsorshipPolicyId) { params.push({ sponsorshipPolicyId: paymasterOptions.sponsorshipPolicyId }); } const sponsoredData = await paymasterClient.request({ method: "pm_sponsorUserOperation" /* SPONSOR_USER_OPERATION */, params }); return sponsoredData; } const chainId = await protocolKit.getChainId(); const erc20PaymasterData = await paymasterClient.request({ method: "pm_getPaymasterData" /* GET_PAYMASTER_DATA */, params: [ userOperationToHexValues(userOperation, entryPoint), entryPoint, toHex5(chainId), { token: paymasterOptions.paymasterTokenAddress } ] }); return erc20PaymasterData; } async #getUserOperationGasPrices(client) { const feeData = await client.request({ method: "pimlico_getUserOperationGasPrice" /* GET_USER_OPERATION_GAS_PRICE */ }); const { fast: { maxFeePerGas, maxPriorityFeePerGas } } = feeData; return { maxFeePerGas, maxPriorityFeePerGas }; } }; // src/packs/safe-4337/estimators/generic/GenericFeeEstimator.ts import { createPublicClient as createPublicClient2, http as http2, toHex as toHex6 } from "viem"; var GenericFeeEstimator = class { constructor(rpcUrl, overrides = {}) { this.defaultVerificationGasLimitOverhead = overrides.defaultVerificationGasLimitOverhead ?? 35000n; this.overrides = overrides; this.rpcUrl = rpcUrl; } async preEstimateUserOperationGas({ userOperation, entryPoint, paymasterOptions, protocolKit }) { let feeDataRes = {}; let paymasterStubDataRes = {}; if (paymasterOptions) { const chainId = await protocolKit.getChainId(); const paymasterClient = createBundlerClient(paymasterOptions.paymasterUrl); const context = "paymasterTokenAddress" in paymasterOptions ? { token: paymasterOptions.paymasterTokenAddress } : paymasterOptions.paymasterContext ?? {}; const [feeData, paymasterStubData] = await Promise.all([ this.#getUserOperationGasPrices(this.rpcUrl), paymasterClient.request({ method: "pm_getPaymasterStubData" /* GET_PAYMASTER_STUB_DATA */, params: [ userOperationToHexValues(userOperation, entryPoint), entryPoint, toHex6(chainId), context ] }) ]); feeDataRes = feeData; paymasterStubDataRes = paymasterStubData; } else { const feeData = await this.#getUserOperationGasPrices(this.rpcUrl); feeDataRes = feeData; } feeDataRes.callGasLimit = this.overrides.callGasLimit ?? feeDataRes.callGasLimit; feeDataRes.verificationGasLimit = this.overrides.verificationGasLimit ?? feeDataRes.verificationGasLimit; feeDataRes.preVerificationGas = this.overrides.preVerificationGas ?? feeDataRes.preVerificationGas; feeDataRes.maxFeePerGas = this.overrides.maxFeePerGas ?? feeDataRes.maxFeePerGas; feeDataRes.maxPriorityFeePerGas = this.overrides.maxPriorityFeePerGas ?? feeDataRes.maxPriorityFeePerGas; const result = { ...feeDataRes, ...paymasterStubDataRes }; if (result.verificationGasLimit != null) { const threshold = await protocolKit.getThreshold(); result.verificationGasLimit = (BigInt(result.verificationGasLimit) + BigInt(threshold) * this.defaultVerificationGasLimitOverhead).toString(); } return result; } async postEstimateUserOperationGas({ userOperation, entryPoint, paymasterOptions, protocolKit }) { if (protocolKit == null) { throw new Error("Can't use GenericFeeEstimator if protocolKit is null."); } if (!paymasterOptions) return {}; const paymasterClient = createBundlerClient(paymasterOptions.paymasterUrl); const chainId = await protocolKit.getChainId(); if (paymasterOptions.isSponsored) { const params = [ userOperationToHexValues(userOperation, entryPoint), entryPoint, toHex6(chainId) ]; if (paymasterOptions.paymasterContext) { params.push(paymasterOptions.paymasterContext); } const sponsoredData = await paymasterClient.request({ method: "pm_getPaymasterData" /* GET_PAYMASTER_DATA */, params }); return sponsoredData; } const erc20PaymasterData = await paymasterClient.request({ method: "pm_getPaymasterData" /* GET_PAYMASTER_DATA */, params: [ userOperationToHexValues(userOperation, entryPoint), entryPoint, toHex6(chainId), { token: paymasterOptions.paymasterTokenAddress } ] }); if ("verificationGasLimit" in erc20PaymasterData && erc20PaymasterData.verificationGasLimit != null) { const threshold = await protocolKit.getThreshold(); erc20PaymasterData.verificationGasLimit = (BigInt(erc20PaymasterData.verificationGasLimit) + BigInt(threshold) * this.defaultVerificationGasLimitOverhead).toString(); } return erc20PaymasterData; } async #getUserOperationGasPrices(rpcUrl) { const client = createPublicClient2({ transport: http2(rpcUrl) }); const [block, maxPriorityFeePerGas] = await Promise.all([ client.getBlock({ blockTag: "latest" }), client.estimateMaxPriorityFeePerGas() ]); const baseFeePerGas = block.baseFeePerGas; if (!baseFeePerGas) { throw new Error("Base fee not available - probably not an EIP-1559 block."); } const maxFeePerGas = baseFeePerGas + maxPriorityFeePerGas; return { maxFeePerGas: BigInt( Math.ceil(Number(maxFeePerGas) * (this.overrides.maxFeePerGasMultiplier ?? 1.5)) ), maxPriorityFeePerGas: BigInt( Math.ceil( Number(maxPriorityFeePerGas) * (this.overrides.maxPriorityFeePerGasMultiplier ?? 1.5) ) ) }; } }; // src/packs/safe-4337/estimators/biconomy/BiconomyFeeEstimator.ts import { toHex as toHex7 } from "viem"; var BiconomyFeeEstimator = class { /** * Setup the userOperation before calling eth_estimateUserOperationGas * * This method: * 1. Fetches gas prices from biconomy_getGasFeeValues * 2. If paymaster enabled, fetches stub data from pm_getPaymasterStubData * 3. Returns combined estimation data * * @param props - Estimation parameters * @returns Gas estimation data including fees and paymaster stub data */ async preEstimateUserOperationGas({ bundlerUrl, userOperation, entryPoint, paymasterOptions, protocolKit }) { const bundlerClient = createBundlerClient(bundlerUrl); const feeData = await this.#getGasFeeValues(bundlerClient); const chainId = await protocolKit.getChainId(); let paymasterStubData = {}; if (paymasterOptions) { const paymasterClient = createBundlerClient(paymasterOptions.paymasterUrl); const context = "paymasterTokenAddress" in paymasterOptions ? { token: paymasterOptions.paymasterTokenAddress } : void 0; paymasterStubData = await paymasterClient.request({ method: "pm_getPaymasterStubData" /* GET_PAYMASTER_STUB_DATA */, // Standard method params: [ userOperationToHexValues(userOperation, entryPoint), entryPoint, toHex7(chainId), context ] }); } return { ...feeData, ...paymasterStubData }; } /** * Adjust the userOperation values after calling eth_estimateUserOperationGas * * This method: * 1. If sponsored mode: calls pm_getPaymasterData with sponsorship context * 2. If ERC-20 mode: calls pm_getPaymasterData with token context * 3. Returns paymaster data to be merged into userOperation * * Note: Unlike Pimlico which uses custom pm_sponsorUserOperation for sponsored mode, * Biconomy uses the standard pm_getPaymasterData for both modes. * * @param props - Estimation parameters * @returns Paymaster data including paymasterAndData/paymaster fields */ async postEstimateUserOperationGas({ userOperation, entryPoint, paymasterOptions, protocolKit }) { if (!paymasterOptions) return {}; const paymasterClient = createBundlerClient(paymasterOptions.paymasterUrl); const chainId = await protocolKit.getChainId(); const context = paymasterOptions.isSponsored ? paymasterOptions.sponsorshipPolicyId ? { sponsorshipPolicyId: paymasterOptions.sponsorshipPolicyId } : {} : { token: paymasterOptions.paymasterTokenAddress }; const paymasterData = await paymasterClient.request({ method: "pm_getPaymasterData" /* GET_PAYMASTER_DATA */, // Standard method params: [ userOperationToHexValues(userOperation, entryPoint), entryPoint, toHex7(chainId), context ] }); return paymasterData; } /** * Fetch gas fee values from Biconomy bundler * * Biconomy returns 3 tiers (slow/standard/fast) like Pimlico. * We use the 'fast' tier by default for optimal transaction speed. * * @param client - Bundler client with Biconomy custom RPC schema * @returns Gas fee data (maxFeePerGas, maxPriorityFeePerGas) from 'fast' tier * @private */ async #getGasFeeValues(client) { const feeData = await client.request({ method: "biconomy_getGasFeeValues" /* GET_GAS_FEE_VALUES */ }); const { fast: { maxFeePerGas, maxPriorityFeePerGas } } = feeData; return { maxFeePerGas, maxPriorityFeePerGas }; } }; // src/packs/safe-4337/estimators/gelato/GelatoFeeEstimator.ts import { createPublicClient as createPublicClient3, http as http3, toHex as toHex8 } from "viem"; // src/packs/safe-4337/estimators/gelato/types.ts var GELATO_URL_PATTERNS = ["api.gelato.digital"]; function isUsing1Balance(bundlerUrl) { return bundlerUrl.includes("sponsorApiKey="); } // src/packs/safe-4337/estimators/gelato/GelatoFeeEstimator.ts var DEFAULT_GAS_MULTIPLIERS = { maxFeePerGasMultiplier: 1.5, maxPriorityFeePerGasMultiplier: 1.5 }; var DEFAULT_INITIAL_GAS_LIMITS = { verificationGasLimit: 500000n, callGasLimit: 100000n, preVerificationGas: 50000n }; var GelatoFeeEstimator = class { #gasMultipliers; constructor(gasMultipliers) { this.#gasMultipliers = { ...DEFAULT_GAS_MULTIPLIERS, ...gasMultipliers }; } /** * Setup the userOperation before calling eth_estimateUserOperationGas * * This method: * 1. Checks if using 1Balance sponsorship (gas fields = 0) * 2. If not sponsored, calculates gas from standard RPC * 3. If external paymaster provided, fetches stub data * * @param props - Estimation parameters * @returns Gas estimation data including fees and paymaster stub data */ async preEstimateUserOperationGas({ bundlerUrl, userOperation, entryPoint, paymasterOptions, protocolKit }) { if (isUsing1Balance(bundlerUrl)) { return { maxFeePerGas: "0x0", maxPriorityFeePerGas: "0x0" }; } const providerUrl = protocolKit.getSafeProvider().getExternalProvider().transport?.url; if (!providerUrl) { throw new Error("Cannot get provider URL from protocolKit for gas estimation"); } const feeData = await this.#getGasFees(providerUrl); const chainId = await protocolKit.getChainId(); let paymasterStubData = {}; if (paymasterOptions) { const paymasterClient = createBundlerClient(paymasterOptions.paymasterUrl); const context = "paymasterTokenAddress" in paymasterOptions ? { token: paymasterOptions.paymasterTokenAddress } : void 0; paymasterStubData = await paymasterClient.request({ method: "pm_getPaymasterStubData" /* GET_PAYMASTER_STUB_DATA */, params: [ userOperationToHexValues(userOperation, entryPoint), entryPoint, toHex8(chainId), context ] }); } return { ...feeData, ...DEFAULT_INITIAL_GAS_LIMITS, ...paymasterStubData }; } /** * Adjust the userOperation values after calling eth_estimateUserOperationGas * * This method: * 1. If using 1Balance: no post-estimation needed (return empty) * 2. If external paymaster: calls pm_getPaymasterData for final data * * @param props - Estimation parameters * @returns Paymaster data to merge into userOperation */ async postEstimateUserOperationGas({ bundlerUrl, userOperation, entryPoint, paymasterOptions, protocolKit }) { if (isUsing1Balance(bundlerUrl)) { return {}; } if (!paymasterOptions) { return {}; } const paymasterClient = createBundlerClient(paymasterOptions.paymasterUrl); const chainId = await protocolKit.getChainId(); const context = paymasterOptions.isSponsored ? paymasterOptions.sponsorshipPolicyId ? { sponsorshipPolicyId: paymasterOptions.sponsorshipPolicyId } : paymasterOptions.paymasterContext || {} : { token: paymasterOptions.paymasterTokenAddress }; const paymasterData = await paymasterClient.request({ method: "pm_getPaymasterData" /* GET_PAYMASTER_DATA */, params: [ userOperationToHexValues(userOperation, entryPoint), entryPoint, toHex8(chainId), context ] }); return paymasterData; } /** * Get gas fees using chain RPC provider (not bundler) * * Gelato doesn't have a custom gas price RPC like Pimlico/Biconomy. * Instead, we use the chain provider (not bundler) to: * - Get latest block baseFeePerGas * - Estimate maxPriorityFeePerGas * - Calculate: maxFeePerGas = baseFee + priorityFee * - Apply multipliers for safety margin * * Note: Bundler URL only supports 4337-specific RPC methods (eth_sendUserOperation, * eth_estimateUserOperationGas, etc.), NOT standard Ethereum RPC methods like * eth_getBlockByNumber or eth_maxPriorityFeePerGas. * * @param rpcUrl - Chain RPC URL (from protocolKit provider) * @returns Gas fee data (maxFeePerGas, maxPriorityFeePerGas) * @private */ async #getGasFees(rpcUrl) { const client = createPublicClient3({ transport: http3(rpcUrl) }); const [block, maxPriorityFeePerGas] = await Promise.all([ client.getBlock({ blockTag: "latest" }), client.estimateMaxPriorityFeePerGas() ]); const baseFeePerGas = block.baseFeePerGas; if (!baseFeePerGas) { throw new Error("Base fee not available - probably not an EIP-1559 block."); } const rawMaxFee = baseFeePerGas + maxPriorityFee