UNPKG

@safe-global/relay-kit

Version:

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

1,336 lines (1,318 loc) 55.5 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, toHex as toHex5 } 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; } 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; } 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; } 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: 1n, verificationGasLimit: 1n, preVerificationGas: 1n, maxFeePerGas: 1n, maxPriorityFeePerGas: 1n, paymasterAndData, signature: "0x" }; } return { sender: safeAddress, nonce: nonce.toString(), ...unpackInitCode(initCode), callData, callGasLimit: 1n, verificationGasLimit: 1n, preVerificationGas: 1n, 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) ? { paymaster: userOpV07.paymaster !== "0x" ? userOpV07.paymaster : null, paymasterData: userOpV07.paymasterData !== "0x" ? userOpV07.paymasterData : null, paymasterVerificationGasLimit: userOpV07.paymasterVerificationGasLimit ? toHex3(userOpV07.paymasterVerificationGasLimit) : null, paymasterPostOpGasLimit: userOpV07.paymasterPostOpGasLimit ? toHex3(userOpV07.paymasterPostOpGasLimit) : null } : {} }; return userOperationWithHexValues; } // src/packs/safe-4337/utils/getRelayKitVersion.ts var getRelayKitVersion = () => "4.0.4"; // 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 var PimlicoFeeEstimator = class { async preEstimateUserOperationGas({ bundlerUrl, userOperation, entryPoint, paymasterOptions }) { const bundlerClient = createBundlerClient(bundlerUrl); const feeData = await this.#getUserOperationGasPrices(bundlerClient); const chainId = await this.#getChainId(bundlerClient); 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, chainId, context] }); } return { ...feeData, ...paymasterStubData }; } async postEstimateUserOperationGas({ userOperation, entryPoint, paymasterOptions }) { 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 this.#getChainId(paymasterClient); const erc20PaymasterData = await paymasterClient.request({ method: "pm_getPaymasterData" /* GET_PAYMASTER_DATA */, params: [ userOperationToHexValues(userOperation, entryPoint), entryPoint, 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 }; } async #getChainId(client) { const chainId = await client.request({ method: "eth_chainId" }); return chainId; } }; // src/packs/safe-4337/Safe4337Pack.ts var MAX_ERC20_AMOUNT_TO_APPROVE = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn; var EQ_OR_GT_1_4_1 = ">=1.4.1"; var Safe4337Pack = class _Safe4337Pack extends RelayKitBasePack { #BUNDLER_URL; #ENTRYPOINT_ADDRESS; #SAFE_4337_MODULE_ADDRESS = "0x"; #SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS = "0x"; #bundlerClient; #chainId; #paymasterOptions; #onchainIdentifier = ""; /** * Creates an instance of the Safe4337Pack. * * @param {Safe4337Options} options - The initialization parameters. */ constructor({ protocolKit, bundlerClient, bundlerUrl, chainId, paymasterOptions, entryPointAddress, safe4337ModuleAddress, safeWebAuthnSharedSignerAddress, onchainAnalytics }) { super(protocolKit); this.#BUNDLER_URL = bundlerUrl; this.#bundlerClient = bundlerClient; this.#chainId = chainId; this.#paymasterOptions = paymasterOptions; this.#ENTRYPOINT_ADDRESS = entryPointAddress; this.#SAFE_4337_MODULE_ADDRESS = safe4337ModuleAddress; this.#SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS = safeWebAuthnSharedSignerAddress || "0x"; if (onchainAnalytics?.project) { const { project, platform } = onchainAnalytics; this.#onchainIdentifier = generateOnChainIdentifier({ project, platform, tool: "relay-kit", toolVersion: getRelayKitVersion() }); } } /** * Initializes a Safe4337Pack class. * This method creates the protocolKit instance based on the input parameters. * When the Safe address is provided, it will use the existing Safe. * When the Safe address is not provided, it will use the predictedSafe feature with the provided owners and threshold. * It will use the correct contract addresses for the fallbackHandler and the module and will add the data to enable the 4337 module. * * @param {Safe4337InitOptions} initOptions - The initialization parameters. * @return {Promise<Safe4337Pack>} The Promise object that will be resolved into an instance of Safe4337Pack. */ static async init(initOptions) { const { provider, signer, options, bundlerUrl, customContracts, paymasterOptions, onchainAnalytics } = initOptions; let protocolKit; const bundlerClient = createBundlerClient(bundlerUrl); const chainId = await bundlerClient.request({ method: "eth_chainId" /* CHAIN_ID */ }); let safeModulesSetupAddress = customContracts?.safeModulesSetupAddress; const network = parseInt(chainId, 16).toString(); const safeModulesVersion = initOptions.safeModulesVersion || DEFAULT_SAFE_MODULES_VERSION; if (!safeModulesSetupAddress) { const safeModuleSetupDeployment = getSafeModuleSetupDeployment({ released: true, version: safeModulesVersion, network }); safeModulesSetupAddress = safeModuleSetupDeployment?.networkAddresses[network]; } let safe4337ModuleAddress = customContracts?.safe4337ModuleAddress; if (!safe4337ModuleAddress) { const safe4337ModuleDeployment = getSafe4337ModuleDeployment({ released: true, version: safeModulesVersion, network }); safe4337ModuleAddress = safe4337ModuleDeployment?.networkAddresses[network]; } if (!safeModulesSetupAddress || !safe4337ModuleAddress) { throw new Error( `Safe4337Module and/or SafeModuleSetup not available for chain ${network} and modules version ${safeModulesVersion}` ); } let safeWebAuthnSharedSignerAddress = customContracts?.safeWebAuthnSharedSignerAddress; if ("safeAddress" in options) { protocolKit = await Safe.init({ provider, signer, safeAddress: options.safeAddress }); const safeVersion = protocolKit.getContractVersion(); const isSafeVersion4337Compatible = semverSatisfies(safeVersion, EQ_OR_GT_1_4_1); if (!isSafeVersion4337Compatible) { throw new Error( `Incompatibility detected: The current Safe Account version (${safeVersion}) is not supported. EIP-4337 requires the Safe to use at least v1.4.1.` ); } const safeModules = await protocolKit.getModules(); const is4337ModulePresent = safeModules.some((module) => module === safe4337ModuleAddress); if (!is4337ModulePresent) { throw new Error( `Incompatibility detected: The EIP-4337 module is not enabled in the provided Safe Account. Enable this module (address: ${safe4337ModuleAddress}) to add compatibility.` ); } const safeFallbackhandler = await protocolKit.getFallbackHandler(); const is4337FallbackhandlerPresent = safeFallbackhandler === safe4337ModuleAddress; if (!is4337FallbackhandlerPresent) { throw new Error( `Incompatibility detected: The EIP-4337 fallbackhandler is not attached to the Safe Account. Attach this fallbackhandler (address: ${safe4337ModuleAddress}) to ensure compatibility.` ); } } else { if (!options.owners || !options.threshold) { throw new Error("Owners and threshold are required to deploy a new Safe"); } const safeVersion = options.safeVersion || DEFAULT_SAFE_VERSION; const enable4337ModuleTransaction = { to: safeModulesSetupAddress, value: "0", data: encodeFunctionData3({ abi: ABI, functionName: "enableModules", args: [[safe4337ModuleAddress]] }), operation: OperationType3.DelegateCall // DelegateCall required for enabling the 4337 module }; const setupTransactions = [enable4337ModuleTransaction]; const isApproveTransactionRequired = !!paymasterOptions && !paymasterOptions.isSponsored && !!paymasterOptions.paymasterTokenAddress; if (isApproveTransactionRequired) { const { paymasterAddress, amountToApprove = MAX_ERC20_AMOUNT_TO_APPROVE } = paymasterOptions; const approveToPaymasterTransaction = { to: paymasterOptions.paymasterTokenAddress, data: encodeFunctionData3({ abi: ABI, functionName: "approve", args: [paymasterAddress, amountToApprove] }), value: "0", operation: OperationType3.Call // Call for approve }; setupTransactions.push(approveToPaymasterTransaction); } const safeProvider = await SafeProvider.init({ provider, signer, safeVersion }); const isPasskeySigner = await safeProvider.isPasskeySigner(); if (isPasskeySigner) { if (!safeWebAuthnSharedSignerAddress) { const safeWebAuthnSharedSignerDeployment = getSafeWebAuthnShareSignerDeployment({ released: true, version: "0.2.1", network }); safeWebAuthnSharedSignerAddress = safeWebAuthnSharedSignerDeployment?.networkAddresses[network]; } if (!safeWebAuthnSharedSignerAddress) { throw new Error(`safeWebAuthnSharedSignerAddress not available for chain ${network}`); } const passkeySigner = await safeProvider.getExternalSigner(); const checkSummedOwners = options.owners.map((owner) => getAddress2(owner)); const checkSummedSignerAddress = getAddress2(safeWebAuthnSharedSignerAddress); if (!checkSummedOwners.includes(checkSummedSignerAddress)) { options.owners.push(checkSummedSignerAddress); } const sharedSignerTransaction = { to: safeWebAuthnSharedSignerAddress, value: "0", data: passkeySigner.encodeConfigure(), operation: OperationType3.DelegateCall // DelegateCall required into the SafeWebAuthnSharedSigner instance in order for it to set its configuration. }; setupTransactions.push(sharedSignerTransaction); } let deploymentTo; let deploymentData; const isBatch = setupTransactions.length > 1; if (isBatch) { const multiSendContract = await getMultiSendContract({ safeProvider, safeVersion, deploymentType: options.deploymentType || void 0 }); const batchData = encodeFunctionData3({ abi: ABI, functionName: "multiSend", args: [encodeMultiSendData2(setupTransactions)] }); deploymentTo = multiSendContract.getAddress(); deploymentData = batchData; } else { deploymentTo = enable4337ModuleTransaction.to; deploymentData = enable4337ModuleTransaction.data; } protocolKit = await Safe.init({ provider, signer, predictedSafe: { safeDeploymentConfig: { safeVersion, saltNonce: options.saltNonce || void 0, deploymentType: options.deploymentType || void 0 }, safeAccountConfig: { owners: options.owners, threshold: options.threshold, to: deploymentTo, data: deploymentData, fallbackHandler: safe4337ModuleAddress, paymentToken: zeroAddress, payment: 0, paymentReceiver: zeroAddress } }, onchainAnalytics }); } let selectedEntryPoint; if (customContracts?.entryPointAddress) { const requiredSafeModulesVersion = entryPointToSafeModules(customContracts?.entryPointAddress); if (!semverSatisfies(safeModulesVersion, requiredSafeModulesVersion)) throw new Error( `The selected entrypoint ${customContracts?.entryPointAddress} is not compatible with version ${safeModulesVersion} of Safe modules` ); selectedEntryPoint = customContracts?.entryPointAddress; } else { const supportedEntryPoints = await bundlerClient.request({ method: "eth_supportedEntryPoints" /* SUPPORTED_ENTRY_POINTS */ }); if (!supportedEntryPoints.length) { throw new Error("No entrypoint provided or available through the bundler"); } selectedEntryPoint = supportedEntryPoints.find((entryPoint) => { const requiredSafeModulesVersion = entryPointToSafeModules(entryPoint); return semverSatisfies(safeModulesVersion, requiredSafeModulesVersion); }); if (!selectedEntryPoint) { throw new Error( `Incompatibility detected: None of the entrypoints provided by the bundler is compatible with the Safe modules version ${safeModulesVersion}` ); } } return new _Safe4337Pack({ chainId: BigInt(chainId), protocolKit, bundlerClient, paymasterOptions, bundlerUrl, entryPointAddress: selectedEntryPoint, safe4337ModuleAddress, safeWebAuthnSharedSignerAddress, onchainAnalytics }); } /** * Estimates gas for the SafeOperation. * * @param {EstimateFeeProps} props - The parameters for the gas estimation. * @param {BaseSafeOperation} props.safeOperation - The SafeOperation to estimate the gas. * @param {IFeeEstimator} props.feeEstimator - The function to estimate the gas. * @return {Promise<BaseSafeOperation>} The Promise object that will be resolved into the gas estimation. */ async getEstimateFee({ safeOperation, feeEstimator = new PimlicoFeeEstimator() }) { const threshold = await this.protocolKit.getThreshold(); const preEstimationData = await feeEstimator?.preEstimateUserOperationGas?.({ bundlerUrl: this.#BUNDLER_URL, entryPoint: this.#ENTRYPOINT_ADDRESS, userOperation: safeOperation.getUserOperation(), paymasterOptions: this.#paymasterOptions }); if (preEstimationData) { safeOperation.addEstimations(preEstimationData); } const estimateUserOperationGas = await this.#bundlerClient.request({ method: "eth_estimateUserOperationGas" /* ESTIMATE_USER_OPERATION_GAS */, params: [ { ...userOperationToHexValues(safeOperation.getUserOperation(), this.#ENTRYPOINT_ADDRESS), signature: getDummySignature(this.#SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS, threshold) }, this.#ENTRYPOINT_ADDRESS ] }); if (estimateUserOperationGas) { safeOperation.addEstimations(estimateUserOperationGas); } const postEstimationData = await feeEstimator?.postEstimateUserOperationGas?.({ bundlerUrl: this.#BUNDLER_URL, entryPoint: this.#ENTRYPOINT_ADDRESS, userOperation: { ...safeOperation.getUserOperation(), signature: getDummySignature(this.#SAFE_WEBAUTHN_SHARED_SIGNER_ADDRESS, threshold) }, paymasterOptions: this.#paymasterOptions }); if (postEstimationData) { safeOperation.addEstimations(postEstimationData); } return safeOperation; } /** * Creates a relayed transaction based on the provided parameters. * * @param {MetaTransactionData[]} transactions - The transactions to batch in a SafeOperation. * @param options - Optional configuration options for the transaction creation. * @return {Promise<BaseSafeOperation>} The Promise object will resolve a SafeOperation. */ async createTransaction({ transactions, options = {} }) { const { amountToApprove, validUntil, validAfter, feeEstimator, customNonce } = options; const userOperation = await createUserOperation(this.protocolKit, transactions, { entryPoint: this.#ENTRYPOINT_ADDRESS, paymasterOptions: this.#paymasterOptions, amountToApprove, customNonce }); if (this.#onchainIdentifier) { userOperation.callData += this.#onchainIdentifier; } const safeOperation = SafeOperationFactory_default.createSafeOperation(userOperation, { chainId: this.#chainId, moduleAddress: this.#SAFE_4337_MODULE_ADDRESS, entryPoint: this.#ENTRYPOINT_ADDRESS, validUntil, validAfter }); return await this.getEstimateFee({ safeOperation, feeEstimator }); } /** * Converts a SafeOperationResponse to an SafeOperation. * * @param {SafeOperationResponse} safeOperationResponse - The SafeOperationResponse to convert to SafeOperation * @returns {BaseSafeOperation} - The SafeOperation object */ #toSafeOperation(safeOperationResponse) { const { validUntil, validAfter, userOperation } = safeOperationResponse; const paymaster = userOperation?.paymaster || "0x"; const paymasterData = userOperation?.paymasterData || "0x"; const safeOperation = SafeOperationFactory_default.createSafeOperation( { sender: userOperation?.sender || "0x", nonce: userOperation?.nonce || "0", initCode: userOperation?.initCode || "", callData: userOperation?.callData || "", callGasLimit: BigInt(userOperation?.callGasLimit || 0n), verificationGasLimit: BigInt(userOperation?.verificationGasLimit || 0), preVerificationGas: BigInt(userOperation?.preVerificationGas || 0), maxFeePerGas: BigInt(userOperation?.maxFeePerGas || 0), maxPriorityFeePerGas: BigInt(userOperation?.maxPriorityFeePerGas || 0), paymasterAndData: concat2([paymaster, paymasterData]), signature: safeOperationResponse.preparedSignature || "0x" }, { chainId: this.#chainId, moduleAddress: this.#SAFE_4337_MODULE_ADDRESS, entryPoint: userOperation?.entryPoint || this.#ENTRYPOINT_ADDRESS, validAfter: this.#timestamp(validAfter), validUntil: this.#timestamp(validUntil) } ); if (safeOperationResponse.confirmations) { safeOperationResponse.confirmations.forEach((confirmation) => { safeOperation.addSignature(new EthSafeSignature2(confirmation.owner, confirmation.signature)); }); } return safeOperation; } /** * * @param date An ISO string date * @returns The timestamp in seconds to send to the bundler */ #timestamp(date) { return date ? new Date(date).getTime() / 1e3 : void 0; } /** * Signs a safe operation. * * @param {BaseSafeOperation | SafeOperationResponse} safeOperation - The SafeOperation to sign. It can be: * - A response from the API (Tx Service) * - An instance of SafeOperation * @param {SigningMethod} signingMethod - The signing method to use. * @return {Promise<BaseSafeOperation>} The Promise object will resolve to the signed SafeOperation. */ async signSafeOperation(safeOperation, signingMethod = SigningMethod.ETH_SIGN_TYPED_DATA_V4) { let safeOp; if (safeOperation instanceof BaseSafeOperation_default) { safeOp = safeOperation; } else { safeOp = this.#toSafeOperation(safeOperation); } const safeProvider = this.protocolKit.getSafeProvider(); const signerAddress = await safeProvider.getSignerAddress(); const isPasskeySigner = await safeProvider.isPasskeySigner(); if (!signerAddress) { throw new Error("There is no signer address available to sign the SafeOperation"); } const isOwner = await this.protocolKit.isOwner(