@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
JavaScript
// 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(