diff --git a/packages/api-kit/src/SafeApiKit.ts b/packages/api-kit/src/SafeApiKit.ts index 0623228c8..01ac92f1d 100644 --- a/packages/api-kit/src/SafeApiKit.ts +++ b/packages/api-kit/src/SafeApiKit.ts @@ -831,6 +831,10 @@ class SafeApiKit { throw new Error('Signature must not be empty') } + // We are receiving the timestamp in seconds (block timestamp), but the API expects it in milliseconds + const getISOString = (date: number | undefined) => + !date ? null : new Date(date * 1000).toISOString() + return sendRequest({ url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/safe-operations/`, method: HttpMethod.Post, @@ -847,8 +851,8 @@ class SafeApiKit { ? null : userOperation.paymasterAndData, entryPoint, - validAfter: !options?.validAfter ? null : options?.validAfter, - validUntil: !options?.validUntil ? null : options?.validUntil, + validAfter: getISOString(options?.validAfter), + validUntil: getISOString(options?.validUntil), signature: userOperation.signature, moduleAddress } diff --git a/packages/api-kit/tests/endpoint/index.test.ts b/packages/api-kit/tests/endpoint/index.test.ts index 68f96c9a3..18dfd2fe1 100644 --- a/packages/api-kit/tests/endpoint/index.test.ts +++ b/packages/api-kit/tests/endpoint/index.test.ts @@ -683,7 +683,13 @@ describe('Endpoint tests', () => { } const entryPoint = '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789' - const options = { validAfter: 123, validUntil: 234 } + + const ethersProvider = protocolKit.getSafeProvider().getExternalProvider() + const timestamp = (await ethersProvider.getBlock('latest'))?.timestamp || 0 + + const validAfter = timestamp - 60_000 + const validUntil = timestamp + 60_000 + const options = { validAfter, validUntil } await chai .expect( @@ -711,7 +717,8 @@ describe('Endpoint tests', () => { maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas.toString(), paymasterAndData: userOperation.paymasterAndData, entryPoint, - ...options, + validAfter: new Date(validAfter * 1000).toISOString(), + validUntil: new Date(validUntil * 1000).toISOString(), signature: userOperation.signature, moduleAddress } diff --git a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.test.ts b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.test.ts index e24a5c761..390e41bcc 100644 --- a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.test.ts +++ b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.test.ts @@ -367,7 +367,7 @@ describe('Safe4337Pack', () => { maxFeePerGas: 100000n, maxPriorityFeePerGas: 200000n, verificationGasLimit: 150000n, - preVerificationGas: 100000n + preVerificationGas: 105000n }) }) @@ -395,7 +395,7 @@ describe('Safe4337Pack', () => { maxFeePerGas: 100000n, maxPriorityFeePerGas: 200000n, verificationGasLimit: 150000n, - preVerificationGas: 100000n + preVerificationGas: 105000n }) }) @@ -454,7 +454,7 @@ describe('Safe4337Pack', () => { maxFeePerGas: 100000n, maxPriorityFeePerGas: 200000n, verificationGasLimit: 150000n, - preVerificationGas: 100000n + preVerificationGas: 105000n }) }) @@ -529,7 +529,7 @@ describe('Safe4337Pack', () => { maxFeePerGas: 100000n, maxPriorityFeePerGas: 200000n, verificationGasLimit: 150000n, - preVerificationGas: 100000n + preVerificationGas: 105000n }) }) }) @@ -557,7 +557,7 @@ describe('Safe4337Pack', () => { fixtures.OWNER_1.toLowerCase(), new protocolKit.EthSafeSignature( fixtures.OWNER_1, - '0x63de7fdf99bcf20a1981ae74c3960604139d8bf025da894abc11604b30f438e82ceb0d37e73bf16b0b8b896f8be82a49750433733c0414fe4a3b8182a3875e1f1c', + '0x8ce4849928aef19e8f5cc199e069a451568dcbaca194a86dc953ae24acac3cbb02a458343127b2a52e1af3b99622b2fc8f1bd9957f84828c33940532a94ea3261c', false ) ) diff --git a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts index 106830bdb..d86926b64 100644 --- a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts +++ b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts @@ -478,15 +478,17 @@ export class Safe4337Pack extends RelayKitBasePack<{ preVerificationGas: BigInt(userOperation?.preVerificationGas || 0), maxFeePerGas: BigInt(userOperation?.maxFeePerGas || 0), maxPriorityFeePerGas: BigInt(userOperation?.maxPriorityFeePerGas || 0), - paymasterAndData: userOperation?.paymasterData || '0x', - signature: userOperation?.signature || '0x' + paymasterAndData: ethers.hexlify( + ethers.concat([userOperation?.paymaster || '0x', userOperation?.paymasterData || '0x']) + ), + signature: safeOperationResponse.preparedSignature || '0x' }, { chainId: this.#chainId, moduleAddress: this.#SAFE_4337_MODULE_ADDRESS, entryPoint: userOperation?.entryPoint || this.#ENTRYPOINT_ADDRESS, - validAfter: validAfter ? new Date(validAfter).getTime() : undefined, - validUntil: validUntil ? new Date(validUntil).getTime() : undefined + validAfter: this.#timestamp(validAfter), + validUntil: this.#timestamp(validUntil) } ) @@ -499,6 +501,15 @@ export class Safe4337Pack extends RelayKitBasePack<{ return safeOperation } + /** + * + * @param date An ISO string date + * @returns The timestamp in seconds to send to the bundler + */ + #timestamp(date: string | null) { + return date ? new Date(date).getTime() / 1000 : undefined + } + /** * Signs a safe operation. * @@ -546,7 +557,7 @@ export class Safe4337Pack extends RelayKitBasePack<{ this.#SAFE_4337_MODULE_ADDRESS ) } else { - const safeOpHash = await safeOp.getHash() + const safeOpHash = safeOp.getHash() signature = await this.protocolKit.signHash(safeOpHash) } diff --git a/packages/relay-kit/src/packs/safe-4337/estimators/PimlicoFeeEstimator.ts b/packages/relay-kit/src/packs/safe-4337/estimators/PimlicoFeeEstimator.ts index f00932f43..0830929ca 100644 --- a/packages/relay-kit/src/packs/safe-4337/estimators/PimlicoFeeEstimator.ts +++ b/packages/relay-kit/src/packs/safe-4337/estimators/PimlicoFeeEstimator.ts @@ -22,7 +22,8 @@ export class PimlicoFeeEstimator implements IFeeEstimator { return { callGasLimit: userOperation.callGasLimit + userOperation.callGasLimit / 2n, verificationGasLimit: - userOperation.verificationGasLimit + userOperation.verificationGasLimit / 2n + userOperation.verificationGasLimit + userOperation.verificationGasLimit / 2n, + preVerificationGas: userOperation.preVerificationGas + userOperation.preVerificationGas / 20n } } diff --git a/packages/safe-kit/src/extensions/safe-operations/SafeOperationClient.ts b/packages/safe-kit/src/extensions/safe-operations/SafeOperationClient.ts index 3be2a69a6..e93e87a16 100644 --- a/packages/safe-kit/src/extensions/safe-operations/SafeOperationClient.ts +++ b/packages/safe-kit/src/extensions/safe-operations/SafeOperationClient.ts @@ -45,19 +45,7 @@ export class SafeOperationClient { safeOperation = await this.safe4337Pack.signSafeOperation(safeOperation) if (isMultisigSafe) { - const userOperation = safeOperation.toUserOperation() - userOperation.signature = safeOperation.encodedSignatures() // Without validity dates - - await this.apiKit.addSafeOperation({ - entryPoint: safeOperation.data.entryPoint, - moduleAddress: safeOperation.moduleAddress, - safeAddress: safeOperation.data.safe, - userOperation, - options: { - validAfter: safeOperation.data.validAfter, - validUntil: safeOperation.data.validUntil - } - }) + await this.apiKit.addSafeOperation(safeOperation) const safeOperationHash = safeOperation.getHash() diff --git a/playground/relay-kit/api-kit-interoperability.ts b/playground/relay-kit/api-kit-interoperability.ts index 66c2041de..2745fdb0e 100644 --- a/playground/relay-kit/api-kit-interoperability.ts +++ b/playground/relay-kit/api-kit-interoperability.ts @@ -10,7 +10,7 @@ const SAFE_ADDRESS = '' // Safe 2/N const CHAIN_NAME = 'sepolia' // Constants -const BUNDLER_URL = `https://api.pimlico.io/v1/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` +const BUNDLER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` const PAYMASTER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` const RPC_URL = 'https://sepolia.gateway.tenderly.co' const PAYMASTER_ADDRESS = '0x0000000000325602a77416A16136FDafd04b299f' // SEPOLIA diff --git a/playground/relay-kit/usdc-transfer-4337-counterfactual.ts b/playground/relay-kit/usdc-transfer-4337-counterfactual.ts index 7b3e91d88..50ac4795f 100644 --- a/playground/relay-kit/usdc-transfer-4337-counterfactual.ts +++ b/playground/relay-kit/usdc-transfer-4337-counterfactual.ts @@ -19,7 +19,7 @@ const CHAIN_NAME = 'sepolia' // const CHAIN_NAME = 'gnosis' // Bundler URL -const BUNDLER_URL = `https://api.pimlico.io/v1/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO +const BUNDLER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO // USDC CONTRACT ADDRESS IN SEPOLIA // faucet: https://faucet.circle.com/ diff --git a/playground/relay-kit/usdc-transfer-4337-erc20-counterfactual.ts b/playground/relay-kit/usdc-transfer-4337-erc20-counterfactual.ts index f80a23db9..dbfad9543 100644 --- a/playground/relay-kit/usdc-transfer-4337-erc20-counterfactual.ts +++ b/playground/relay-kit/usdc-transfer-4337-erc20-counterfactual.ts @@ -18,7 +18,7 @@ const RPC_URL = 'https://sepolia.gateway.tenderly.co' // SEPOLIA // const RPC_URL = 'https://rpc.gnosischain.com/' // GNOSIS // Bundler URL -const BUNDLER_URL = `https://api.pimlico.io/v1/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO +const BUNDLER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO // PAYMASTER ADDRESS const paymasterAddress = '0x0000000000325602a77416A16136FDafd04b299f' // SEPOLIA diff --git a/playground/relay-kit/usdc-transfer-4337-erc20.ts b/playground/relay-kit/usdc-transfer-4337-erc20.ts index 3dd524274..770057245 100644 --- a/playground/relay-kit/usdc-transfer-4337-erc20.ts +++ b/playground/relay-kit/usdc-transfer-4337-erc20.ts @@ -18,7 +18,7 @@ const RPC_URL = 'https://sepolia.gateway.tenderly.co' // SEPOLIA // const RPC_URL = 'https://rpc.gnosischain.com/' // GNOSIS // Bundler URL -const BUNDLER_URL = `https://api.pimlico.io/v1/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO +const BUNDLER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO // PAYMASTER ADDRESS const paymasterAddress = '0x0000000000325602a77416A16136FDafd04b299f' // SEPOLIA diff --git a/playground/relay-kit/usdc-transfer-4337-sponsored-counterfactual.ts b/playground/relay-kit/usdc-transfer-4337-sponsored-counterfactual.ts index 2e89c7b5a..7697558bc 100644 --- a/playground/relay-kit/usdc-transfer-4337-sponsored-counterfactual.ts +++ b/playground/relay-kit/usdc-transfer-4337-sponsored-counterfactual.ts @@ -21,7 +21,7 @@ const RPC_URL = 'https://sepolia.gateway.tenderly.co' // SEPOLIA // const RPC_URL = 'https://rpc.gnosischain.com/' // GNOSIS // Bundler URL -const BUNDLER_URL = `https://api.pimlico.io/v1/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO +const BUNDLER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO // Paymaster URL const PAYMASTER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO diff --git a/playground/relay-kit/usdc-transfer-4337-sponsored.ts b/playground/relay-kit/usdc-transfer-4337-sponsored.ts index 7751c0c91..a3aebbb7e 100644 --- a/playground/relay-kit/usdc-transfer-4337-sponsored.ts +++ b/playground/relay-kit/usdc-transfer-4337-sponsored.ts @@ -21,7 +21,7 @@ const RPC_URL = 'https://sepolia.gateway.tenderly.co' // SEPOLIA // const RPC_URL = 'https://rpc.gnosischain.com/' // GNOSIS // Bundler URL -const BUNDLER_URL = `https://api.pimlico.io/v1/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO +const BUNDLER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO // Paymaster URL const PAYMASTER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO diff --git a/playground/relay-kit/usdc-transfer-4337.ts b/playground/relay-kit/usdc-transfer-4337.ts index b58105444..ded38476c 100644 --- a/playground/relay-kit/usdc-transfer-4337.ts +++ b/playground/relay-kit/usdc-transfer-4337.ts @@ -10,7 +10,7 @@ const PIMLICO_API_KEY = '' const SAFE_ADDRESS = '' // Bundler URL -const BUNDLER_URL = `https://api.pimlico.io/v1/sepolia/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO +const BUNDLER_URL = `https://api.pimlico.io/v2/sepolia/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO // RPC URL const RPC_URL = 'https://sepolia.gateway.tenderly.co' diff --git a/playground/safe-kit/send-safe-operation.ts b/playground/safe-kit/send-safe-operation.ts index 8e21ca5d9..45b6a6fe8 100644 --- a/playground/safe-kit/send-safe-operation.ts +++ b/playground/safe-kit/send-safe-operation.ts @@ -1,3 +1,4 @@ +import { ethers } from 'ethers' import { SafeClientResult, createSafeClient, safeOperations } from '@safe-global/safe-kit' import { generateTransferCallData } from '../utils' @@ -20,9 +21,9 @@ const usdcAmount = 10_000n // 0.01 USDC const paymasterAddress = '0x0000000000325602a77416A16136FDafd04b299f' // SEPOLIA // Paymaster URL -const PIMLICO_API_KEY = '30b296fa-8947-4775-b44a-b225336e2a66' +const PIMLICO_API_KEY = '' const PAYMASTER_URL = `https://api.pimlico.io/v2/sepolia/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO -const BUNDLER_URL = `https://api.pimlico.io/v1/sepolia/rpc?apikey=${PIMLICO_API_KEY}` +const BUNDLER_URL = `https://api.pimlico.io/v2/sepolia/rpc?apikey=${PIMLICO_API_KEY}` async function send(): Promise { const safeClient = await createSafeClient({ @@ -59,7 +60,16 @@ async function send(): Promise { } const transactions = [transferUSDC, transferUSDC] - const safeOperationResult = await safeClientWithSafeOperation.sendSafeOperation({ transactions }) + const ethersProvider = new ethers.JsonRpcProvider(RPC_URL) + const timestamp = (await ethersProvider.getBlock('latest'))?.timestamp || 0 + + const safeOperationResult = await safeClientWithSafeOperation.sendSafeOperation({ + transactions, + options: { + validAfter: timestamp - 60_000, + validUntil: timestamp + 60_000 + } + }) console.log('-Send result: ', safeOperationResult) @@ -73,7 +83,7 @@ async function confirm(safeClientResult: SafeClientResult, pk: string) { const safeClient = await createSafeClient({ provider: RPC_URL, - signer: OWNER_1_PRIVATE_KEY, + signer: pk, safeOptions: { owners: [OWNER_1_ADDRESS, OWNER_2_ADDRESS, OWNER_3_ADDRESS], threshold: THRESHOLD,