From 10e69075e8a10b6b0304cd431240d818067825e0 Mon Sep 17 00:00:00 2001 From: Tim Schwarz <4171783+tmjssz@users.noreply.github.com> Date: Fri, 19 Apr 2024 10:41:37 +0200 Subject: [PATCH 01/24] Implement functions to call 4337 api endpoints --- packages/api-kit/src/SafeApiKit.ts | 108 ++++++++++++++++++ .../src/types/safeTransactionServiceTypes.ts | 53 +++++++++ packages/api-kit/src/utils/constants.ts | 1 + packages/api-kit/src/utils/index.ts | 3 + 4 files changed, 165 insertions(+) create mode 100644 packages/api-kit/src/utils/constants.ts create mode 100644 packages/api-kit/src/utils/index.ts diff --git a/packages/api-kit/src/SafeApiKit.ts b/packages/api-kit/src/SafeApiKit.ts index f18a370df..2f4a099a0 100644 --- a/packages/api-kit/src/SafeApiKit.ts +++ b/packages/api-kit/src/SafeApiKit.ts @@ -1,10 +1,12 @@ import { AddMessageProps, AddSafeDelegateProps, + AddSafeOperationProps, AllTransactionsListResponse, AllTransactionsOptions, DeleteSafeDelegateProps, GetSafeDelegateProps, + GetSafeOperationListResponse, SafeSingletonResponse, GetSafeMessageListProps, ModulesResponse, @@ -20,6 +22,7 @@ import { SafeMultisigTransactionEstimate, SafeMultisigTransactionEstimateResponse, SafeMultisigTransactionListResponse, + SafeOperationResponse, SafeServiceInfoResponse, SignatureResponse, TokenInfoListResponse, @@ -34,6 +37,7 @@ import { SafeMultisigTransactionResponse } from '@safe-global/safe-core-sdk-types' import { TRANSACTION_SERVICE_URLS } from './utils/config' +import { isEmptyHexData } from './utils' export interface SafeApiKitConfig { /** chainId - The chainId */ @@ -715,6 +719,110 @@ class SafeApiKit { } }) } + + /** + * Get the SafeOperations that were sent from a particular address. + * @param safeAddress - The Safe address to retrieve SafeOperations for + * @throws "Safe address must not be empty" + * @throws "Invalid Ethereum address {safeAddress}" + * @returns The SafeOperations sent from the given Safe's address + */ + async getSafeOperationsByAddress(safeAddress: string): Promise { + if (!safeAddress) { + throw new Error('Safe address must not be empty') + } + const { address } = this.#getEip3770Address(safeAddress) + + return sendRequest({ + url: `${this.#txServiceBaseUrl}/v1/safes/${address}/safe-operations/`, + method: HttpMethod.Get + }) + } + + /** + * Get a SafeOperation by its hash. + * @param safeOperationHash The SafeOperation hash + * @throws "SafeOperation hash must not be empty" + * @throws "Not found." + * @returns The SafeOperation + */ + async getSafeOperation(safeOperationHash: string): Promise { + if (!safeOperationHash) { + throw new Error('SafeOperation hash must not be empty') + } + + return sendRequest({ + url: `${this.#txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/`, + method: HttpMethod.Get + }) + } + + /** + * Create a new 4337 SafeOperation for a Safe. + * @param addSafeOperationProps - The configuration of the SafeOperation + * @throws "Safe address must not be empty" + * @throws "Invalid Safe address {safeAddress}" + * @throws "Module address must not be empty" + * @throws "Invalid module address {moduleAddress}" + * @throws "SafeOperation is not signed by the given signer {signerAddress}" + */ + async addSafeOperation({ + moduleAddress: moduleAddressProp, + safeAddress: safeAddressProp, + safeOperation, + signer + }: AddSafeOperationProps): Promise { + let safeAddress: string, moduleAddress: string + + if (!safeAddressProp) { + throw new Error('Safe address must not be empty') + } + try { + safeAddress = this.#getEip3770Address(safeAddressProp).address + } catch (err) { + throw new Error(`Invalid Safe address ${safeAddressProp}`) + } + + if (!moduleAddressProp) { + throw new Error('Module address must not be empty') + } + + try { + moduleAddress = this.#getEip3770Address(moduleAddressProp).address + } catch (err) { + throw new Error(`Invalid module address ${moduleAddressProp}`) + } + + const signerAddress = await signer.getAddress() + const signature = safeOperation.getSignature(signerAddress) + + if (!signature) { + throw new Error(`SafeOperation is not signed by the given signer ${signerAddress}`) + } + + const { data } = safeOperation + + return sendRequest({ + url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/safe-operations/`, + method: HttpMethod.Post, + body: { + nonce: Number(data.nonce), + initCode: isEmptyHexData(data.initCode) ? null : data.initCode, + callData: data.callData, + callDataGasLimit: Number(data.callGasLimit), + verificationGasLimit: Number(data.verificationGasLimit), + preVerificationGas: Number(data.preVerificationGas), + maxFeePerGas: Number(data.maxFeePerGas), + maxPriorityFeePerGas: Number(data.maxPriorityFeePerGas), + paymasterAndData: isEmptyHexData(data.paymasterAndData) ? null : data.paymasterAndData, + entryPoint: data.entryPoint, + validAfter: !data.validAfter ? null : data.validAfter, + validUntil: !data.validUntil ? null : data.validUntil, + signature: signature.data, + moduleAddress + } + }) + } } export default SafeApiKit diff --git a/packages/api-kit/src/types/safeTransactionServiceTypes.ts b/packages/api-kit/src/types/safeTransactionServiceTypes.ts index 92ee2b70b..80fcb80ab 100644 --- a/packages/api-kit/src/types/safeTransactionServiceTypes.ts +++ b/packages/api-kit/src/types/safeTransactionServiceTypes.ts @@ -3,6 +3,7 @@ import { SafeMultisigTransactionResponse, SafeTransactionData } from '@safe-global/safe-core-sdk-types' +import SafeOperation from '@safe-global/relay-kit/src/packs/safe-4337/SafeOperation' export type SafeServiceInfoResponse = { readonly name: string @@ -287,3 +288,55 @@ export type EIP712TypedData = { types: TypedDataField message: Record } + +export type SafeOperationConfirmation = { + readonly created: string + readonly modified: string + readonly owner: string + readonly signature: string + readonly signatureType: string +} + +export type SafeOperationResponse = { + readonly created: string + readonly modified: string + readonly ethereumTxHash: string + readonly sender: string + readonly userOperationHash: string + readonly safeOperationHash: string + readonly nonce: number + readonly initCode: null | string + readonly callData: null | string + readonly callDataGasLimit: number + readonly verificationGasLimit: number + readonly preVerificationGas: number + readonly maxFeePerGas: number + readonly maxPriorityFeePerGas: number + readonly paymaster: null | string + readonly paymasterData: null | string + readonly signature: string + readonly entryPoint: string + readonly validAfter: string + readonly validUntil: string + readonly moduleAddress: string + readonly confirmations?: Array + readonly preparedSignature?: string +} + +export type GetSafeOperationListResponse = { + readonly count: number + readonly next?: string + readonly previous?: string + readonly results: Array +} + +export type AddSafeOperationProps = { + /** Address of the Safe4337Module contract */ + moduleAddress: string + /** Address of the Safe to add a SafeOperation for */ + safeAddress: string + /** Signed SafeOperation object to add */ + safeOperation: SafeOperation + /** Signer instance */ + signer: Signer +} diff --git a/packages/api-kit/src/utils/constants.ts b/packages/api-kit/src/utils/constants.ts new file mode 100644 index 000000000..c403b44cb --- /dev/null +++ b/packages/api-kit/src/utils/constants.ts @@ -0,0 +1 @@ +export const EMPTY_DATA = '0x' diff --git a/packages/api-kit/src/utils/index.ts b/packages/api-kit/src/utils/index.ts new file mode 100644 index 000000000..9c583cf8a --- /dev/null +++ b/packages/api-kit/src/utils/index.ts @@ -0,0 +1,3 @@ +import { EMPTY_DATA } from './constants' + +export const isEmptyHexData = (input: string) => !input || input === EMPTY_DATA From aed236bd91bcbeab8b079bb390e404e0569ae7ea Mon Sep 17 00:00:00 2001 From: Tim Schwarz <4171783+tmjssz@users.noreply.github.com> Date: Fri, 19 Apr 2024 10:49:07 +0200 Subject: [PATCH 02/24] e2e tests for addSafeOperation function --- packages/api-kit/.env.example | 3 +- .../tests/e2e/addSafeOperation.test.ts | 170 ++++++++++++++++++ .../api-kit/tests/utils/setupServiceClient.ts | 5 +- 3 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 packages/api-kit/tests/e2e/addSafeOperation.test.ts diff --git a/packages/api-kit/.env.example b/packages/api-kit/.env.example index c519ed0be..e7285b3f9 100644 --- a/packages/api-kit/.env.example +++ b/packages/api-kit/.env.example @@ -1,2 +1,3 @@ MNEMONIC= -PK= \ No newline at end of file +PK= +PIMLICO_API_KEY= \ No newline at end of file diff --git a/packages/api-kit/tests/e2e/addSafeOperation.test.ts b/packages/api-kit/tests/e2e/addSafeOperation.test.ts new file mode 100644 index 000000000..19826e2bb --- /dev/null +++ b/packages/api-kit/tests/e2e/addSafeOperation.test.ts @@ -0,0 +1,170 @@ +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import dotenv from 'dotenv' +import { Wallet, ethers } from 'ethers' +import { EthAdapter } from '@safe-global/safe-core-sdk-types' +import SafeApiKit from '@safe-global/api-kit' +import { Safe4337Pack } from '@safe-global/relay-kit' +import { generateTransferCallData } from '@safe-global/relay-kit/src/packs/safe-4337/testing-utils/helpers' +import { getSafe4337ModuleDeployment } from '@safe-global/safe-modules-deployments' +import { EthersAdapter } from 'packages/protocol-kit/dist/src' +import { getServiceClient } from '../utils/setupServiceClient' + +dotenv.config() + +const { PIMLICO_API_KEY } = process.env + +chai.use(chaiAsPromised) + +const SIGNER_PK = '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676' +const SAFE_ADDRESS = '0x60C4Ab82D06Fd7dFE9517e17736C2Dcc77443EF0' // 1/1 Safe (v1.4.1) with signer above as owner + 4337 module enabled +const RPC_URL = 'https://rpc.ankr.com/eth_sepolia' +const PAYMASTER_TOKEN_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' +const PAYMASTER_ADDRESS = '0x0000000000325602a77416A16136FDafd04b299f' +const BUNDLER_URL = `https://api.pimlico.io/v1/sepolia/rpc?apikey=${PIMLICO_API_KEY}` + +let safeApiKit: SafeApiKit +let ethAdapter: EthAdapter +let safe4337Pack: Safe4337Pack +let signer: Wallet +let moduleAddress: string + +describe('addSafeOperation', () => { + const transferUSDC = { + to: PAYMASTER_TOKEN_ADDRESS, + data: generateTransferCallData(SAFE_ADDRESS, 100_000n), + value: '0', + operation: 0 + } + + before(async () => { + ;({ safeApiKit, ethAdapter, signer } = await getServiceClient(SIGNER_PK, undefined, RPC_URL)) + + const ethersAdapter = new EthersAdapter({ + ethers, + signerOrProvider: signer + }) + + safe4337Pack = await Safe4337Pack.init({ + options: { safeAddress: SAFE_ADDRESS }, + ethersAdapter, + rpcUrl: RPC_URL, + bundlerUrl: BUNDLER_URL, + paymasterOptions: { + paymasterTokenAddress: PAYMASTER_TOKEN_ADDRESS, + paymasterAddress: PAYMASTER_ADDRESS + } + }) + + const chainId = (await ethAdapter.getChainId()).toString() + + moduleAddress = getSafe4337ModuleDeployment({ + released: true, + version: '0.2.0', + network: chainId + })?.networkAddresses[chainId] as string + }) + + describe('should fail', () => { + it('if safeAddress is empty', async () => { + const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] }) + const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) + + await chai + .expect( + safeApiKit.addSafeOperation({ + moduleAddress, + safeAddress: '', + safeOperation: signedSafeOperation, + signer + }) + ) + .to.be.rejectedWith('Safe address must not be empty') + }) + + it('if safeAddress is invalid', async () => { + const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] }) + const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) + + await chai + .expect( + safeApiKit.addSafeOperation({ + moduleAddress, + safeAddress: '0x123', + safeOperation: signedSafeOperation, + signer + }) + ) + .to.be.rejectedWith('Invalid Safe address 0x123') + }) + + it('if moduleAddress is empty', async () => { + const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] }) + const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) + + await chai + .expect( + safeApiKit.addSafeOperation({ + moduleAddress: '', + safeAddress: SAFE_ADDRESS, + safeOperation: signedSafeOperation, + signer + }) + ) + .to.be.rejectedWith('Module address must not be empty') + }) + + it('if moduleAddress is invalid', async () => { + const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] }) + const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) + + await chai + .expect( + safeApiKit.addSafeOperation({ + moduleAddress: '0x234', + safeAddress: SAFE_ADDRESS, + safeOperation: signedSafeOperation, + signer + }) + ) + .to.be.rejectedWith('Invalid module address 0x234') + }) + + it('if the SafeOperation is not signed', async () => { + const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] }) + + await chai + .expect( + safeApiKit.addSafeOperation({ + moduleAddress, + safeAddress: SAFE_ADDRESS, + safeOperation, + signer + }) + ) + .to.be.rejectedWith( + 'SafeOperation is not signed by the given signer 0x56e2C102c664De6DfD7315d12c0178b61D16F171' + ) + }) + }) + + it('should add a new SafeOperation', async () => { + const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] }) + const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) + + const safeOperationsBefore = await safeApiKit.getSafeOperationsByAddress(SAFE_ADDRESS) + const initialNumSafeOperations = safeOperationsBefore.results.length + + await chai.expect( + safeApiKit.addSafeOperation({ + moduleAddress, + safeAddress: SAFE_ADDRESS, + safeOperation: signedSafeOperation, + signer + }) + ).to.be.fulfilled + + const safeOperationsAfter = await safeApiKit.getSafeOperationsByAddress(SAFE_ADDRESS) + chai.expect(safeOperationsAfter.results.length).to.equal(initialNumSafeOperations + 1) + }) +}) diff --git a/packages/api-kit/tests/utils/setupServiceClient.ts b/packages/api-kit/tests/utils/setupServiceClient.ts index a69e5c1bf..1eee628ef 100644 --- a/packages/api-kit/tests/utils/setupServiceClient.ts +++ b/packages/api-kit/tests/utils/setupServiceClient.ts @@ -12,9 +12,10 @@ interface ServiceClientConfig { export async function getServiceClient( signerPk: string, - txServiceUrl?: string + txServiceUrl?: string, + rpcUrl?: string ): Promise { - const provider = getDefaultProvider(config.JSON_RPC) + const provider = getDefaultProvider(rpcUrl || config.JSON_RPC) const signer = new Wallet(signerPk, provider) const ethAdapter = await getEthAdapter(signer) const safeApiKit = new SafeApiKit({ chainId: config.CHAIN_ID, txServiceUrl }) From f4ad53d708bacbd3d956dae9f3b14e653c7f532f Mon Sep 17 00:00:00 2001 From: Tim Schwarz <4171783+tmjssz@users.noreply.github.com> Date: Fri, 19 Apr 2024 10:49:40 +0200 Subject: [PATCH 03/24] e2e tests for getSafeOperation function --- .../tests/e2e/getSafeOperation.test.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 packages/api-kit/tests/e2e/getSafeOperation.test.ts diff --git a/packages/api-kit/tests/e2e/getSafeOperation.test.ts b/packages/api-kit/tests/e2e/getSafeOperation.test.ts new file mode 100644 index 000000000..184e8883d --- /dev/null +++ b/packages/api-kit/tests/e2e/getSafeOperation.test.ts @@ -0,0 +1,38 @@ +import SafeApiKit from '@safe-global/api-kit/index' +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { getServiceClient } from '../utils/setupServiceClient' + +chai.use(chaiAsPromised) + +const SAFE_ADDRESS = '0x60C4Ab82D06Fd7dFE9517e17736C2Dcc77443EF0' // v1.4.1 +const SIGNER_PK = '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676' + +let safeApiKit: SafeApiKit + +describe('getSafeOperation', () => { + before(async () => { + ;({ safeApiKit } = await getServiceClient(SIGNER_PK)) + }) + + describe('should fail', () => { + it('should fail if safeOperationHash is empty', async () => { + await chai + .expect(safeApiKit.getSafeOperation('')) + .to.be.rejectedWith('SafeOperation hash must not be empty') + }) + + it('should fail if safeOperationHash is invalid', async () => { + await chai.expect(safeApiKit.getSafeOperation('0x123')).to.be.rejectedWith('Not found.') + }) + }) + + it('should get the SafeOperation', async () => { + const safeOperations = await safeApiKit.getSafeOperationsByAddress(SAFE_ADDRESS) + const safeOperationHash = safeOperations.results[0].safeOperationHash + + const safeOperation = await safeApiKit.getSafeOperation(safeOperationHash) + + chai.expect(safeOperation).to.deep.eq(safeOperations.results[0]) + }) +}) From 9c49f60f1c828db712518be7798074571f5eebe8 Mon Sep 17 00:00:00 2001 From: Tim Schwarz <4171783+tmjssz@users.noreply.github.com> Date: Fri, 19 Apr 2024 10:49:56 +0200 Subject: [PATCH 04/24] e2e tests for getSafeOperationsByAddress function --- .../e2e/getSafeOperationsByAddress.test.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts diff --git a/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts b/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts new file mode 100644 index 000000000..a659eedd9 --- /dev/null +++ b/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts @@ -0,0 +1,64 @@ +import SafeApiKit from '@safe-global/api-kit/index' +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { getServiceClient } from '../utils/setupServiceClient' + +chai.use(chaiAsPromised) + +const SAFE_ADDRESS = '0x60C4Ab82D06Fd7dFE9517e17736C2Dcc77443EF0' // v1.4.1 +const SIGNER_PK = '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676' + +let safeApiKit: SafeApiKit + +describe('getSafeOperationsByAddress', () => { + before(async () => { + ;({ safeApiKit } = await getServiceClient(SIGNER_PK)) + }) + + describe('should fail', () => { + it('should fail if safeAddress is empty', async () => { + await chai + .expect(safeApiKit.getSafeOperationsByAddress('')) + .to.be.rejectedWith('Safe address must not be empty') + }) + + it('should fail if safeAddress is invalid', async () => { + await chai + .expect(safeApiKit.getSafeOperationsByAddress('0x123')) + .to.be.rejectedWith('Invalid Ethereum address 0x123') + }) + }) + + it('should get the SafeOperation list', async () => { + const safeOperations = await safeApiKit.getSafeOperationsByAddress(SAFE_ADDRESS) + + chai.expect(safeOperations).to.have.property('count').greaterThan(1) + chai.expect(safeOperations).to.have.property('results').to.be.an('array') + + safeOperations.results.every((safeOperation) => { + chai.expect(safeOperation).to.have.property('created') + chai.expect(safeOperation).to.have.property('modified') + chai.expect(safeOperation).to.have.property('ethereumTxHash') + chai.expect(safeOperation).to.have.property('sender').to.eq(SAFE_ADDRESS) + chai.expect(safeOperation).to.have.property('userOperationHash') + chai.expect(safeOperation).to.have.property('safeOperationHash') + chai.expect(safeOperation).to.have.property('nonce') + chai.expect(safeOperation).to.have.property('initCode') + chai.expect(safeOperation).to.have.property('callData') + chai.expect(safeOperation).to.have.property('callDataGasLimit') + chai.expect(safeOperation).to.have.property('verificationGasLimit') + chai.expect(safeOperation).to.have.property('preVerificationGas') + chai.expect(safeOperation).to.have.property('maxFeePerGas') + chai.expect(safeOperation).to.have.property('maxPriorityFeePerGas') + chai.expect(safeOperation).to.have.property('paymaster') + chai.expect(safeOperation).to.have.property('paymasterData') + chai.expect(safeOperation).to.have.property('signature') + chai.expect(safeOperation).to.have.property('entryPoint') + chai.expect(safeOperation).to.have.property('validAfter') + chai.expect(safeOperation).to.have.property('validUntil') + chai.expect(safeOperation).to.have.property('moduleAddress') + chai.expect(safeOperation).to.have.property('confirmations').to.be.an('array') + chai.expect(safeOperation).to.have.property('preparedSignature') + }) + }) +}) From 5153baa3810bae3875ad3307b8f87176c80c4fd4 Mon Sep 17 00:00:00 2001 From: Tim Schwarz <4171783+tmjssz@users.noreply.github.com> Date: Fri, 19 Apr 2024 12:08:42 +0200 Subject: [PATCH 05/24] Endpoint tests for 4337 api functions --- packages/api-kit/tests/endpoint/index.test.ts | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/packages/api-kit/tests/endpoint/index.test.ts b/packages/api-kit/tests/endpoint/index.test.ts index 002fb3420..071b56323 100644 --- a/packages/api-kit/tests/endpoint/index.test.ts +++ b/packages/api-kit/tests/endpoint/index.test.ts @@ -14,6 +14,7 @@ import sinon from 'sinon' import sinonChai from 'sinon-chai' import config from '../utils/config' import { getServiceClient } from '../utils/setupServiceClient' +import SafeOperation from '@safe-global/relay-kit/packs/safe-4337/SafeOperation' chai.use(chaiAsPromised) chai.use(sinonChai) @@ -645,6 +646,77 @@ describe('Endpoint tests', () => { } }) }) + + it('getSafeOperationsByAddress', async () => { + await chai + .expect(safeApiKit.getSafeOperationsByAddress(safeAddress)) + .to.be.eventually.deep.equals({ data: { success: true } }) + chai.expect(fetchData).to.have.been.calledWith({ + url: `${txServiceBaseUrl}/v1/safes/${safeAddress}/safe-operations/`, + method: 'get' + }) + }) + + it('getSafeOperation', async () => { + const safeOperationHash = 'safe-operation-hash' + + await chai + .expect(safeApiKit.getSafeOperation(safeOperationHash)) + .to.be.eventually.deep.equals({ data: { success: true } }) + chai.expect(fetchData).to.have.been.calledWith({ + url: `${txServiceBaseUrl}/v1/safe-operations/${safeOperationHash}/`, + method: 'get' + }) + }) + + it('addSafeOperation', async () => { + const signature = '0xsignature' + const moduleAddress = '0xa581c4A4DB7175302464fF3C06380BC3270b4037' + + const safeOperation = { + data: { + nonce: 42, + initCode: '0x123initcode', + callData: '0xcallData123', + callGasLimit: 123, + verificationGasLimit: 234, + preVerificationGas: 345, + maxFeePerGas: 456, + maxPriorityFeePerGas: 567, + paymasterAndData: '0xpaymasterAndData123', + entryPoint: '0xentryPoint', + validAfter: 'validAfter', + validUntil: 'validUntil' + }, + getSignature: () => ({ data: signature }) + } as unknown as SafeOperation + + await chai + .expect( + safeApiKit.addSafeOperation({ + moduleAddress, + safeAddress, + safeOperation, + signer + }) + ) + .to.be.eventually.deep.equals({ data: { success: true } }) + + // We need to get the object without the "callGasLimit" prop because the api endpoint expects it as "callDataGasLimit" + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + const { callGasLimit, ...safeOperationWithoutCallGasLimit } = safeOperation.data + + chai.expect(fetchData).to.have.been.calledWith({ + url: `${txServiceBaseUrl}/v1/safes/${safeAddress}/safe-operations/`, + method: 'post', + body: { + ...safeOperationWithoutCallGasLimit, + callDataGasLimit: safeOperation.data.callGasLimit, + signature, + moduleAddress + } + }) + }) }) describe('Custom endpoint', () => { From ba4937e417133a1031fabdd5e74657d567fa75f3 Mon Sep 17 00:00:00 2001 From: Daniel <25051234+dasanra@users.noreply.github.com> Date: Fri, 26 Apr 2024 12:49:35 +0200 Subject: [PATCH 06/24] set staging backend for SafeOperations tests --- packages/api-kit/tests/e2e/addSafeOperation.test.ts | 7 ++++++- packages/api-kit/tests/e2e/getSafeOperation.test.ts | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/api-kit/tests/e2e/addSafeOperation.test.ts b/packages/api-kit/tests/e2e/addSafeOperation.test.ts index 19826e2bb..db04bbab0 100644 --- a/packages/api-kit/tests/e2e/addSafeOperation.test.ts +++ b/packages/api-kit/tests/e2e/addSafeOperation.test.ts @@ -22,6 +22,7 @@ const RPC_URL = 'https://rpc.ankr.com/eth_sepolia' const PAYMASTER_TOKEN_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' const PAYMASTER_ADDRESS = '0x0000000000325602a77416A16136FDafd04b299f' const BUNDLER_URL = `https://api.pimlico.io/v1/sepolia/rpc?apikey=${PIMLICO_API_KEY}` +const TX_SERVICE_URL = 'https://safe-transaction-sepolia.staging.5afe.dev/api' let safeApiKit: SafeApiKit let ethAdapter: EthAdapter @@ -38,7 +39,11 @@ describe('addSafeOperation', () => { } before(async () => { - ;({ safeApiKit, ethAdapter, signer } = await getServiceClient(SIGNER_PK, undefined, RPC_URL)) + ;({ safeApiKit, ethAdapter, signer } = await getServiceClient( + SIGNER_PK, + TX_SERVICE_URL, + RPC_URL + )) const ethersAdapter = new EthersAdapter({ ethers, diff --git a/packages/api-kit/tests/e2e/getSafeOperation.test.ts b/packages/api-kit/tests/e2e/getSafeOperation.test.ts index 184e8883d..020c47962 100644 --- a/packages/api-kit/tests/e2e/getSafeOperation.test.ts +++ b/packages/api-kit/tests/e2e/getSafeOperation.test.ts @@ -7,12 +7,13 @@ chai.use(chaiAsPromised) const SAFE_ADDRESS = '0x60C4Ab82D06Fd7dFE9517e17736C2Dcc77443EF0' // v1.4.1 const SIGNER_PK = '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676' +const TX_SERVICE_URL = 'https://safe-transaction-sepolia.staging.5afe.dev/api' let safeApiKit: SafeApiKit describe('getSafeOperation', () => { before(async () => { - ;({ safeApiKit } = await getServiceClient(SIGNER_PK)) + ;({ safeApiKit } = await getServiceClient(SIGNER_PK, TX_SERVICE_URL)) }) describe('should fail', () => { From a9e0a66323fc6d65e2dc7f89539757ed94bbc019 Mon Sep 17 00:00:00 2001 From: Daniel <25051234+dasanra@users.noreply.github.com> Date: Fri, 26 Apr 2024 16:09:19 +0200 Subject: [PATCH 07/24] remove unnecessary property from test setup --- packages/api-kit/tests/e2e/addSafeOperation.test.ts | 12 ++++-------- packages/api-kit/tests/e2e/getSafeOperation.test.ts | 2 +- .../tests/e2e/getSafeOperationsByAddress.test.ts | 5 +++-- packages/api-kit/tests/utils/setupServiceClient.ts | 5 ++--- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/api-kit/tests/e2e/addSafeOperation.test.ts b/packages/api-kit/tests/e2e/addSafeOperation.test.ts index db04bbab0..e1c82da75 100644 --- a/packages/api-kit/tests/e2e/addSafeOperation.test.ts +++ b/packages/api-kit/tests/e2e/addSafeOperation.test.ts @@ -7,8 +7,9 @@ import SafeApiKit from '@safe-global/api-kit' import { Safe4337Pack } from '@safe-global/relay-kit' import { generateTransferCallData } from '@safe-global/relay-kit/src/packs/safe-4337/testing-utils/helpers' import { getSafe4337ModuleDeployment } from '@safe-global/safe-modules-deployments' -import { EthersAdapter } from 'packages/protocol-kit/dist/src' +import { EthersAdapter } from 'packages/protocol-kit' import { getServiceClient } from '../utils/setupServiceClient' +import config from '../utils/config' dotenv.config() @@ -18,7 +19,6 @@ chai.use(chaiAsPromised) const SIGNER_PK = '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676' const SAFE_ADDRESS = '0x60C4Ab82D06Fd7dFE9517e17736C2Dcc77443EF0' // 1/1 Safe (v1.4.1) with signer above as owner + 4337 module enabled -const RPC_URL = 'https://rpc.ankr.com/eth_sepolia' const PAYMASTER_TOKEN_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' const PAYMASTER_ADDRESS = '0x0000000000325602a77416A16136FDafd04b299f' const BUNDLER_URL = `https://api.pimlico.io/v1/sepolia/rpc?apikey=${PIMLICO_API_KEY}` @@ -39,11 +39,7 @@ describe('addSafeOperation', () => { } before(async () => { - ;({ safeApiKit, ethAdapter, signer } = await getServiceClient( - SIGNER_PK, - TX_SERVICE_URL, - RPC_URL - )) + ;({ safeApiKit, ethAdapter, signer } = await getServiceClient(SIGNER_PK, TX_SERVICE_URL)) const ethersAdapter = new EthersAdapter({ ethers, @@ -53,7 +49,7 @@ describe('addSafeOperation', () => { safe4337Pack = await Safe4337Pack.init({ options: { safeAddress: SAFE_ADDRESS }, ethersAdapter, - rpcUrl: RPC_URL, + rpcUrl: config.JSON_RPC, bundlerUrl: BUNDLER_URL, paymasterOptions: { paymasterTokenAddress: PAYMASTER_TOKEN_ADDRESS, diff --git a/packages/api-kit/tests/e2e/getSafeOperation.test.ts b/packages/api-kit/tests/e2e/getSafeOperation.test.ts index 020c47962..529759824 100644 --- a/packages/api-kit/tests/e2e/getSafeOperation.test.ts +++ b/packages/api-kit/tests/e2e/getSafeOperation.test.ts @@ -1,4 +1,4 @@ -import SafeApiKit from '@safe-global/api-kit/index' +import SafeApiKit from '@safe-global/api-kit' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import { getServiceClient } from '../utils/setupServiceClient' diff --git a/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts b/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts index a659eedd9..4dbf2f5df 100644 --- a/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts +++ b/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts @@ -1,4 +1,4 @@ -import SafeApiKit from '@safe-global/api-kit/index' +import SafeApiKit from '@safe-global/api-kit' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import { getServiceClient } from '../utils/setupServiceClient' @@ -7,12 +7,13 @@ chai.use(chaiAsPromised) const SAFE_ADDRESS = '0x60C4Ab82D06Fd7dFE9517e17736C2Dcc77443EF0' // v1.4.1 const SIGNER_PK = '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676' +const TX_SERVICE_URL = 'https://safe-transaction-sepolia.staging.5afe.dev/api' let safeApiKit: SafeApiKit describe('getSafeOperationsByAddress', () => { before(async () => { - ;({ safeApiKit } = await getServiceClient(SIGNER_PK)) + ;({ safeApiKit } = await getServiceClient(SIGNER_PK, TX_SERVICE_URL)) }) describe('should fail', () => { diff --git a/packages/api-kit/tests/utils/setupServiceClient.ts b/packages/api-kit/tests/utils/setupServiceClient.ts index 1eee628ef..a69e5c1bf 100644 --- a/packages/api-kit/tests/utils/setupServiceClient.ts +++ b/packages/api-kit/tests/utils/setupServiceClient.ts @@ -12,10 +12,9 @@ interface ServiceClientConfig { export async function getServiceClient( signerPk: string, - txServiceUrl?: string, - rpcUrl?: string + txServiceUrl?: string ): Promise { - const provider = getDefaultProvider(rpcUrl || config.JSON_RPC) + const provider = getDefaultProvider(config.JSON_RPC) const signer = new Wallet(signerPk, provider) const ethAdapter = await getEthAdapter(signer) const safeApiKit = new SafeApiKit({ chainId: config.CHAIN_ID, txServiceUrl }) From 4640747ca1b65eb5908912223c8132282e3e48a4 Mon Sep 17 00:00:00 2001 From: Daniel <25051234+dasanra@users.noreply.github.com> Date: Fri, 26 Apr 2024 16:27:35 +0200 Subject: [PATCH 08/24] Use string to avoid precision errors in gas parameters --- packages/api-kit/src/SafeApiKit.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/api-kit/src/SafeApiKit.ts b/packages/api-kit/src/SafeApiKit.ts index 2f4a099a0..c24b20f76 100644 --- a/packages/api-kit/src/SafeApiKit.ts +++ b/packages/api-kit/src/SafeApiKit.ts @@ -809,11 +809,11 @@ class SafeApiKit { nonce: Number(data.nonce), initCode: isEmptyHexData(data.initCode) ? null : data.initCode, callData: data.callData, - callDataGasLimit: Number(data.callGasLimit), - verificationGasLimit: Number(data.verificationGasLimit), - preVerificationGas: Number(data.preVerificationGas), - maxFeePerGas: Number(data.maxFeePerGas), - maxPriorityFeePerGas: Number(data.maxPriorityFeePerGas), + callDataGasLimit: data.callGasLimit.toString(), + verificationGasLimit: data.verificationGasLimit.toString(), + preVerificationGas: data.preVerificationGas.toString(), + maxFeePerGas: data.maxFeePerGas.toString(), + maxPriorityFeePerGas: data.maxPriorityFeePerGas.toString(), paymasterAndData: isEmptyHexData(data.paymasterAndData) ? null : data.paymasterAndData, entryPoint: data.entryPoint, validAfter: !data.validAfter ? null : data.validAfter, From f0846b4b3463f06532a33c060266100374cf15f6 Mon Sep 17 00:00:00 2001 From: Daniel <25051234+dasanra@users.noreply.github.com> Date: Fri, 26 Apr 2024 16:45:32 +0200 Subject: [PATCH 09/24] fix endpoint test --- packages/api-kit/tests/endpoint/index.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/api-kit/tests/endpoint/index.test.ts b/packages/api-kit/tests/endpoint/index.test.ts index 071b56323..5ac189233 100644 --- a/packages/api-kit/tests/endpoint/index.test.ts +++ b/packages/api-kit/tests/endpoint/index.test.ts @@ -678,11 +678,11 @@ describe('Endpoint tests', () => { nonce: 42, initCode: '0x123initcode', callData: '0xcallData123', - callGasLimit: 123, - verificationGasLimit: 234, - preVerificationGas: 345, - maxFeePerGas: 456, - maxPriorityFeePerGas: 567, + callGasLimit: '123', + verificationGasLimit: '234', + preVerificationGas: '345', + maxFeePerGas: '456', + maxPriorityFeePerGas: '567', paymasterAndData: '0xpaymasterAndData123', entryPoint: '0xentryPoint', validAfter: 'validAfter', From 295b68b099170216f0fafe690f0411711dc046db Mon Sep 17 00:00:00 2001 From: Tim Schwarz <4171783+tmjssz@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:18:37 +0200 Subject: [PATCH 10/24] Move shared types to `safe-core-sdk-types` --- .../src/types/safeTransactionServiceTypes.ts | 2 +- packages/api-kit/tests/endpoint/index.test.ts | 3 +- .../src/packs/safe-4337/Safe4337Pack.ts | 11 +++-- .../src/packs/safe-4337/SafeOperation.ts | 12 +++-- .../estimators/PimlicoFeeEstimator.ts | 2 +- .../relay-kit/src/packs/safe-4337/types.ts | 45 +++-------------- .../relay-kit/src/packs/safe-4337/utils.ts | 2 +- packages/safe-core-sdk-types/src/types.ts | 48 +++++++++++++++++++ 8 files changed, 73 insertions(+), 52 deletions(-) diff --git a/packages/api-kit/src/types/safeTransactionServiceTypes.ts b/packages/api-kit/src/types/safeTransactionServiceTypes.ts index 80fcb80ab..348d19b4e 100644 --- a/packages/api-kit/src/types/safeTransactionServiceTypes.ts +++ b/packages/api-kit/src/types/safeTransactionServiceTypes.ts @@ -1,9 +1,9 @@ import { Signer, TypedDataDomain, TypedDataField } from 'ethers' import { SafeMultisigTransactionResponse, + SafeOperation, SafeTransactionData } from '@safe-global/safe-core-sdk-types' -import SafeOperation from '@safe-global/relay-kit/src/packs/safe-4337/SafeOperation' export type SafeServiceInfoResponse = { readonly name: string diff --git a/packages/api-kit/tests/endpoint/index.test.ts b/packages/api-kit/tests/endpoint/index.test.ts index 5ac189233..39eaa99d3 100644 --- a/packages/api-kit/tests/endpoint/index.test.ts +++ b/packages/api-kit/tests/endpoint/index.test.ts @@ -7,14 +7,13 @@ import SafeApiKit, { } from '@safe-global/api-kit/index' import * as httpRequests from '@safe-global/api-kit/utils/httpRequests' import Safe from '@safe-global/protocol-kit' -import { EthAdapter } from '@safe-global/safe-core-sdk-types' +import { EthAdapter, SafeOperation } from '@safe-global/safe-core-sdk-types' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import sinon from 'sinon' import sinonChai from 'sinon-chai' import config from '../utils/config' import { getServiceClient } from '../utils/setupServiceClient' -import SafeOperation from '@safe-global/relay-kit/packs/safe-4337/SafeOperation' chai.use(chaiAsPromised) chai.use(sinonChai) diff --git a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts index d5b5d10b6..65d20f6a3 100644 --- a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts +++ b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts @@ -8,12 +8,17 @@ import Safe, { getMultiSendContract } from '@safe-global/protocol-kit' import { RelayKitBasePack } from '@safe-global/relay-kit/RelayKitBasePack' -import { MetaTransactionData, OperationType, SafeSignature } from '@safe-global/safe-core-sdk-types' +import { + MetaTransactionData, + OperationType, + SafeSignature, + UserOperation, + SafeUserOperation +} from '@safe-global/safe-core-sdk-types' import { getAddModulesLibDeployment, getSafe4337ModuleDeployment } from '@safe-global/safe-modules-deployments' - import SafeOperation from './SafeOperation' import { EstimateFeeProps, @@ -21,8 +26,6 @@ import { Safe4337ExecutableProps, Safe4337InitOptions, Safe4337Options, - SafeUserOperation, - UserOperation, UserOperationReceipt, UserOperationWithPayload, PaymasterOptions diff --git a/packages/relay-kit/src/packs/safe-4337/SafeOperation.ts b/packages/relay-kit/src/packs/safe-4337/SafeOperation.ts index e27e6d974..b7dc1a9b0 100644 --- a/packages/relay-kit/src/packs/safe-4337/SafeOperation.ts +++ b/packages/relay-kit/src/packs/safe-4337/SafeOperation.ts @@ -1,12 +1,16 @@ import { ethers } from 'ethers' -import { SafeSignature } from '@safe-global/safe-core-sdk-types' +import { + EstimateGasData, + SafeOperation as SafeOperationType, + SafeSignature, + SafeUserOperation, + UserOperation +} from '@safe-global/safe-core-sdk-types' import { buildSignatureBytes } from '@safe-global/protocol-kit' -import { EstimateGasData, SafeUserOperation, UserOperation } from './types' - type SafeOperationOptions = { entryPoint: string; validAfter?: number; validUntil?: number } -class SafeOperation { +class SafeOperation implements SafeOperationType { data: SafeUserOperation signatures: Map = new Map() 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 a61eca986..f00932f43 100644 --- a/packages/relay-kit/src/packs/safe-4337/estimators/PimlicoFeeEstimator.ts +++ b/packages/relay-kit/src/packs/safe-4337/estimators/PimlicoFeeEstimator.ts @@ -1,7 +1,7 @@ import { ethers } from 'ethers' +import { EstimateGasData } from '@safe-global/safe-core-sdk-types' import { EstimateFeeFunctionProps, - EstimateGasData, EstimateSponsoredFeeFunctionProps, EstimateSponsoredGasData, IFeeEstimator diff --git a/packages/relay-kit/src/packs/safe-4337/types.ts b/packages/relay-kit/src/packs/safe-4337/types.ts index ddb701b37..c338a11fc 100644 --- a/packages/relay-kit/src/packs/safe-4337/types.ts +++ b/packages/relay-kit/src/packs/safe-4337/types.ts @@ -1,5 +1,10 @@ import Safe, { EthersAdapter } from '@safe-global/protocol-kit' -import { MetaTransactionData, SafeVersion } from '@safe-global/safe-core-sdk-types' +import { + EstimateGasData, + MetaTransactionData, + SafeVersion, + UserOperation +} from '@safe-global/safe-core-sdk-types' import { ethers } from 'ethers' import SafeOperation from './SafeOperation' @@ -61,44 +66,6 @@ export type Safe4337ExecutableProps = { executable: SafeOperation } -export type SafeUserOperation = { - safe: string - nonce: bigint - initCode: string - callData: string - callGasLimit: bigint - verificationGasLimit: bigint - preVerificationGas: bigint - maxFeePerGas: bigint - maxPriorityFeePerGas: bigint - paymasterAndData: string - validAfter: number - validUntil: number - entryPoint: string -} - -export type UserOperation = { - sender: string - nonce: string - initCode: string - callData: string - callGasLimit: bigint - verificationGasLimit: bigint - preVerificationGas: bigint - maxFeePerGas: bigint - maxPriorityFeePerGas: bigint - paymasterAndData: string - signature: string -} - -export type EstimateGasData = { - maxFeePerGas?: bigint - maxPriorityFeePerGas?: bigint - preVerificationGas?: bigint - verificationGasLimit?: bigint - callGasLimit?: bigint -} - export type EstimateSponsoredGasData = { paymasterAndData: string } & EstimateGasData diff --git a/packages/relay-kit/src/packs/safe-4337/utils.ts b/packages/relay-kit/src/packs/safe-4337/utils.ts index c3d132872..1e2db76c2 100644 --- a/packages/relay-kit/src/packs/safe-4337/utils.ts +++ b/packages/relay-kit/src/packs/safe-4337/utils.ts @@ -1,5 +1,5 @@ import { ethers } from 'ethers' -import { UserOperation } from './types' +import { UserOperation } from '@safe-global/safe-core-sdk-types' /** * Gets the EIP-4337 bundler provider. diff --git a/packages/safe-core-sdk-types/src/types.ts b/packages/safe-core-sdk-types/src/types.ts index 30eb4e7e8..4430d18ce 100644 --- a/packages/safe-core-sdk-types/src/types.ts +++ b/packages/safe-core-sdk-types/src/types.ts @@ -252,3 +252,51 @@ export interface MetaTransactionOptions { gasToken?: string isSponsored?: boolean } + +export type UserOperation = { + sender: string + nonce: string + initCode: string + callData: string + callGasLimit: bigint + verificationGasLimit: bigint + preVerificationGas: bigint + maxFeePerGas: bigint + maxPriorityFeePerGas: bigint + paymasterAndData: string + signature: string +} + +export type SafeUserOperation = { + safe: string + nonce: bigint + initCode: string + callData: string + callGasLimit: bigint + verificationGasLimit: bigint + preVerificationGas: bigint + maxFeePerGas: bigint + maxPriorityFeePerGas: bigint + paymasterAndData: string + validAfter: number + validUntil: number + entryPoint: string +} + +export type EstimateGasData = { + maxFeePerGas?: bigint + maxPriorityFeePerGas?: bigint + preVerificationGas?: bigint + verificationGasLimit?: bigint + callGasLimit?: bigint +} + +export interface SafeOperation { + readonly data: SafeUserOperation + readonly signatures: Map + getSignature(signer: string): SafeSignature | undefined + addSignature(signature: SafeSignature): void + encodedSignatures(): string + addEstimations(estimations: EstimateGasData): void + toUserOperation(): UserOperation +} From 2097ef096351041081c778dd948025ed07cce481 Mon Sep 17 00:00:00 2001 From: Tim Schwarz <4171783+tmjssz@users.noreply.github.com> Date: Fri, 3 May 2024 10:24:36 +0200 Subject: [PATCH 11/24] Mock bundler client calls for addSafeOperation endpoint tests --- .../tests/e2e/addSafeOperation.test.ts | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/packages/api-kit/tests/e2e/addSafeOperation.test.ts b/packages/api-kit/tests/e2e/addSafeOperation.test.ts index e1c82da75..2a119c769 100644 --- a/packages/api-kit/tests/e2e/addSafeOperation.test.ts +++ b/packages/api-kit/tests/e2e/addSafeOperation.test.ts @@ -1,27 +1,26 @@ import chai from 'chai' import chaiAsPromised from 'chai-as-promised' -import dotenv from 'dotenv' import { Wallet, ethers } from 'ethers' +import sinon from 'sinon' +import sinonChai from 'sinon-chai' import { EthAdapter } from '@safe-global/safe-core-sdk-types' import SafeApiKit from '@safe-global/api-kit' import { Safe4337Pack } from '@safe-global/relay-kit' import { generateTransferCallData } from '@safe-global/relay-kit/src/packs/safe-4337/testing-utils/helpers' -import { getSafe4337ModuleDeployment } from '@safe-global/safe-modules-deployments' +import { RPC_4337_CALLS } from '@safe-global/relay-kit/packs/safe-4337/constants' import { EthersAdapter } from 'packages/protocol-kit' -import { getServiceClient } from '../utils/setupServiceClient' +import { getSafe4337ModuleDeployment } from '@safe-global/safe-modules-deployments' import config from '../utils/config' - -dotenv.config() - -const { PIMLICO_API_KEY } = process.env +import { getServiceClient } from '../utils/setupServiceClient' chai.use(chaiAsPromised) +chai.use(sinonChai) const SIGNER_PK = '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676' const SAFE_ADDRESS = '0x60C4Ab82D06Fd7dFE9517e17736C2Dcc77443EF0' // 1/1 Safe (v1.4.1) with signer above as owner + 4337 module enabled const PAYMASTER_TOKEN_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' const PAYMASTER_ADDRESS = '0x0000000000325602a77416A16136FDafd04b299f' -const BUNDLER_URL = `https://api.pimlico.io/v1/sepolia/rpc?apikey=${PIMLICO_API_KEY}` +const BUNDLER_URL = `https://bundler.url` const TX_SERVICE_URL = 'https://safe-transaction-sepolia.staging.5afe.dev/api' let safeApiKit: SafeApiKit @@ -38,6 +37,21 @@ describe('addSafeOperation', () => { operation: 0 } + // Setup mocks for the bundler client + const providerStub = sinon.stub(ethers.JsonRpcProvider.prototype, 'send') + + providerStub.withArgs(RPC_4337_CALLS.CHAIN_ID, []).returns(Promise.resolve('0xaa36a7')) + providerStub + .withArgs(RPC_4337_CALLS.SUPPORTED_ENTRY_POINTS, []) + .returns(Promise.resolve(['0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789'])) + providerStub + .withArgs('pimlico_getUserOperationGasPrice', []) + .returns( + Promise.resolve({ fast: { maxFeePerGas: '0x3b9aca00', maxPriorityFeePerGas: '0x3b9aca00' } }) + ) + + providerStub.callThrough() + before(async () => { ;({ safeApiKit, ethAdapter, signer } = await getServiceClient(SIGNER_PK, TX_SERVICE_URL)) @@ -150,6 +164,14 @@ describe('addSafeOperation', () => { }) it('should add a new SafeOperation', async () => { + providerStub.withArgs(RPC_4337_CALLS.ESTIMATE_USER_OPERATION_GAS, sinon.match.any).returns( + Promise.resolve({ + preVerificationGas: BigInt(Date.now()), + callGasLimit: BigInt(Date.now()), + verificationGasLimit: BigInt(Date.now()) + }) + ) + const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] }) const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) From 236d0fc83af8661d78a335f4e528f7ec92498036 Mon Sep 17 00:00:00 2001 From: Tim Schwarz <4171783+tmjssz@users.noreply.github.com> Date: Wed, 8 May 2024 17:48:51 +0200 Subject: [PATCH 12/24] Use mock data in realistic format for unit test --- packages/api-kit/tests/endpoint/index.test.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/api-kit/tests/endpoint/index.test.ts b/packages/api-kit/tests/endpoint/index.test.ts index 39eaa99d3..0ad9f25df 100644 --- a/packages/api-kit/tests/endpoint/index.test.ts +++ b/packages/api-kit/tests/endpoint/index.test.ts @@ -675,17 +675,18 @@ describe('Endpoint tests', () => { const safeOperation = { data: { nonce: 42, - initCode: '0x123initcode', - callData: '0xcallData123', - callGasLimit: '123', - verificationGasLimit: '234', - preVerificationGas: '345', - maxFeePerGas: '456', - maxPriorityFeePerGas: '567', - paymasterAndData: '0xpaymasterAndData123', - entryPoint: '0xentryPoint', - validAfter: 'validAfter', - validUntil: 'validUntil' + initCode: '0xfbc38024f74946d9ec31e0c8658dd65e335c6e57c14575250787ec5fb270c08a', + callData: + '0x7bb374280000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c72380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044a9059cbb00000000000000000000000060c4ab82d06fd7dfe9517e17736c2dcc77443ef000000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000', + callGasLimit: '150799', + verificationGasLimit: '200691', + preVerificationGas: '50943', + maxFeePerGas: '1949282597', + maxPriorityFeePerGas: '1380000000', + paymasterAndData: '0xdff7fa1077bce740a6a212b3995990682c0ba66d', + entryPoint: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789', + validAfter: '2024-01-01T00:00:00Z', + validUntil: '2024-04-12T00:00:00Z' }, getSignature: () => ({ data: signature }) } as unknown as SafeOperation From 483d90bce22ac412ffb248bba7114a16b5d9438b Mon Sep 17 00:00:00 2001 From: Tim Schwarz <4171783+tmjssz@users.noreply.github.com> Date: Wed, 8 May 2024 17:50:00 +0200 Subject: [PATCH 13/24] Small fix to remove `eslint-disable-next-line` line --- packages/api-kit/tests/endpoint/index.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/api-kit/tests/endpoint/index.test.ts b/packages/api-kit/tests/endpoint/index.test.ts index 0ad9f25df..8c3a05ada 100644 --- a/packages/api-kit/tests/endpoint/index.test.ts +++ b/packages/api-kit/tests/endpoint/index.test.ts @@ -702,16 +702,16 @@ describe('Endpoint tests', () => { ) .to.be.eventually.deep.equals({ data: { success: true } }) - // We need to get the object without the "callGasLimit" prop because the api endpoint expects it as "callDataGasLimit" - /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ - const { callGasLimit, ...safeOperationWithoutCallGasLimit } = safeOperation.data + // We need to rename the "callGasLimit" prop here because the api endpoint expects it as "callDataGasLimit" + const { callGasLimit: callDataGasLimit, ...safeOperationWithoutCallGasLimit } = + safeOperation.data chai.expect(fetchData).to.have.been.calledWith({ url: `${txServiceBaseUrl}/v1/safes/${safeAddress}/safe-operations/`, method: 'post', body: { ...safeOperationWithoutCallGasLimit, - callDataGasLimit: safeOperation.data.callGasLimit, + callDataGasLimit, signature, moduleAddress } From 83f494e9b9157ba9d72f28934596634e03370fca Mon Sep 17 00:00:00 2001 From: Tim Schwarz <4171783+tmjssz@users.noreply.github.com> Date: Wed, 8 May 2024 18:02:05 +0200 Subject: [PATCH 14/24] Assert length of SafeOperations in the getSafeOperation e2e test --- packages/api-kit/tests/e2e/getSafeOperation.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/api-kit/tests/e2e/getSafeOperation.test.ts b/packages/api-kit/tests/e2e/getSafeOperation.test.ts index 529759824..c69ac1ec3 100644 --- a/packages/api-kit/tests/e2e/getSafeOperation.test.ts +++ b/packages/api-kit/tests/e2e/getSafeOperation.test.ts @@ -30,6 +30,8 @@ describe('getSafeOperation', () => { it('should get the SafeOperation', async () => { const safeOperations = await safeApiKit.getSafeOperationsByAddress(SAFE_ADDRESS) + chai.expect(safeOperations.results.length).to.have.above(0) + const safeOperationHash = safeOperations.results[0].safeOperationHash const safeOperation = await safeApiKit.getSafeOperation(safeOperationHash) From b02b85efa34e682f2a575687e396dabdd3083f95 Mon Sep 17 00:00:00 2001 From: Tim Schwarz <4171783+tmjssz@users.noreply.github.com> Date: Wed, 8 May 2024 18:06:19 +0200 Subject: [PATCH 15/24] Rename `isEmptyHexData` util function to `isEmptyData` --- packages/api-kit/src/SafeApiKit.ts | 6 +++--- packages/api-kit/src/utils/index.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/api-kit/src/SafeApiKit.ts b/packages/api-kit/src/SafeApiKit.ts index c24b20f76..0c05523ec 100644 --- a/packages/api-kit/src/SafeApiKit.ts +++ b/packages/api-kit/src/SafeApiKit.ts @@ -37,7 +37,7 @@ import { SafeMultisigTransactionResponse } from '@safe-global/safe-core-sdk-types' import { TRANSACTION_SERVICE_URLS } from './utils/config' -import { isEmptyHexData } from './utils' +import { isEmptyData } from './utils' export interface SafeApiKitConfig { /** chainId - The chainId */ @@ -807,14 +807,14 @@ class SafeApiKit { method: HttpMethod.Post, body: { nonce: Number(data.nonce), - initCode: isEmptyHexData(data.initCode) ? null : data.initCode, + initCode: isEmptyData(data.initCode) ? null : data.initCode, callData: data.callData, callDataGasLimit: data.callGasLimit.toString(), verificationGasLimit: data.verificationGasLimit.toString(), preVerificationGas: data.preVerificationGas.toString(), maxFeePerGas: data.maxFeePerGas.toString(), maxPriorityFeePerGas: data.maxPriorityFeePerGas.toString(), - paymasterAndData: isEmptyHexData(data.paymasterAndData) ? null : data.paymasterAndData, + paymasterAndData: isEmptyData(data.paymasterAndData) ? null : data.paymasterAndData, entryPoint: data.entryPoint, validAfter: !data.validAfter ? null : data.validAfter, validUntil: !data.validUntil ? null : data.validUntil, diff --git a/packages/api-kit/src/utils/index.ts b/packages/api-kit/src/utils/index.ts index 9c583cf8a..9160cbb7e 100644 --- a/packages/api-kit/src/utils/index.ts +++ b/packages/api-kit/src/utils/index.ts @@ -1,3 +1,3 @@ import { EMPTY_DATA } from './constants' -export const isEmptyHexData = (input: string) => !input || input === EMPTY_DATA +export const isEmptyData = (input: string) => !input || input === EMPTY_DATA From 46fd0c4ec5b4559359b11f71229f030aab869f6f Mon Sep 17 00:00:00 2001 From: Tim Schwarz <4171783+tmjssz@users.noreply.github.com> Date: Wed, 8 May 2024 19:08:27 +0200 Subject: [PATCH 16/24] Extend getSafeOperationsByAddress parameters --- packages/api-kit/src/SafeApiKit.ts | 27 ++++++++++++++++--- .../src/types/safeTransactionServiceTypes.ts | 11 ++++++++ .../tests/e2e/addSafeOperation.test.ts | 8 ++++-- .../tests/e2e/getSafeOperation.test.ts | 4 ++- .../e2e/getSafeOperationsByAddress.test.ts | 8 +++--- packages/api-kit/tests/endpoint/index.test.ts | 2 +- 6 files changed, 50 insertions(+), 10 deletions(-) diff --git a/packages/api-kit/src/SafeApiKit.ts b/packages/api-kit/src/SafeApiKit.ts index 0c05523ec..406e56318 100644 --- a/packages/api-kit/src/SafeApiKit.ts +++ b/packages/api-kit/src/SafeApiKit.ts @@ -6,6 +6,7 @@ import { AllTransactionsOptions, DeleteSafeDelegateProps, GetSafeDelegateProps, + GetSafeOperationListProps, GetSafeOperationListResponse, SafeSingletonResponse, GetSafeMessageListProps, @@ -722,19 +723,39 @@ class SafeApiKit { /** * Get the SafeOperations that were sent from a particular address. - * @param safeAddress - The Safe address to retrieve SafeOperations for + * @param getSafeOperationsProps - The parameters to filter the list of SafeOperations * @throws "Safe address must not be empty" * @throws "Invalid Ethereum address {safeAddress}" * @returns The SafeOperations sent from the given Safe's address */ - async getSafeOperationsByAddress(safeAddress: string): Promise { + async getSafeOperationsByAddress({ + safeAddress, + ordering, + limit, + offset + }: GetSafeOperationListProps): Promise { if (!safeAddress) { throw new Error('Safe address must not be empty') } + const { address } = this.#getEip3770Address(safeAddress) + const url = new URL(`${this.#txServiceBaseUrl}/v1/safes/${address}/safe-operations/`) + + if (ordering) { + url.searchParams.set('ordering', ordering) + } + + if (limit) { + url.searchParams.set('limit', limit) + } + + if (offset) { + url.searchParams.set('offset', offset) + } + return sendRequest({ - url: `${this.#txServiceBaseUrl}/v1/safes/${address}/safe-operations/`, + url: url.toString(), method: HttpMethod.Get }) } diff --git a/packages/api-kit/src/types/safeTransactionServiceTypes.ts b/packages/api-kit/src/types/safeTransactionServiceTypes.ts index 348d19b4e..ed3b83523 100644 --- a/packages/api-kit/src/types/safeTransactionServiceTypes.ts +++ b/packages/api-kit/src/types/safeTransactionServiceTypes.ts @@ -323,6 +323,17 @@ export type SafeOperationResponse = { readonly preparedSignature?: string } +export type GetSafeOperationListProps = { + /** Address of the Safe to get SafeOperations for */ + safeAddress: string + /** Which field to use when ordering the results */ + ordering?: string + /** Maximum number of results to return per page */ + limit?: string + /** Initial index from which to return the results */ + offset?: string +} + export type GetSafeOperationListResponse = { readonly count: number readonly next?: string diff --git a/packages/api-kit/tests/e2e/addSafeOperation.test.ts b/packages/api-kit/tests/e2e/addSafeOperation.test.ts index 2a119c769..fdd19b271 100644 --- a/packages/api-kit/tests/e2e/addSafeOperation.test.ts +++ b/packages/api-kit/tests/e2e/addSafeOperation.test.ts @@ -175,7 +175,9 @@ describe('addSafeOperation', () => { const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] }) const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) - const safeOperationsBefore = await safeApiKit.getSafeOperationsByAddress(SAFE_ADDRESS) + const safeOperationsBefore = await safeApiKit.getSafeOperationsByAddress({ + safeAddress: SAFE_ADDRESS + }) const initialNumSafeOperations = safeOperationsBefore.results.length await chai.expect( @@ -187,7 +189,9 @@ describe('addSafeOperation', () => { }) ).to.be.fulfilled - const safeOperationsAfter = await safeApiKit.getSafeOperationsByAddress(SAFE_ADDRESS) + const safeOperationsAfter = await safeApiKit.getSafeOperationsByAddress({ + safeAddress: SAFE_ADDRESS + }) chai.expect(safeOperationsAfter.results.length).to.equal(initialNumSafeOperations + 1) }) }) diff --git a/packages/api-kit/tests/e2e/getSafeOperation.test.ts b/packages/api-kit/tests/e2e/getSafeOperation.test.ts index c69ac1ec3..dcb1054d0 100644 --- a/packages/api-kit/tests/e2e/getSafeOperation.test.ts +++ b/packages/api-kit/tests/e2e/getSafeOperation.test.ts @@ -29,7 +29,9 @@ describe('getSafeOperation', () => { }) it('should get the SafeOperation', async () => { - const safeOperations = await safeApiKit.getSafeOperationsByAddress(SAFE_ADDRESS) + const safeOperations = await safeApiKit.getSafeOperationsByAddress({ + safeAddress: SAFE_ADDRESS + }) chai.expect(safeOperations.results.length).to.have.above(0) const safeOperationHash = safeOperations.results[0].safeOperationHash diff --git a/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts b/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts index 4dbf2f5df..3120ba181 100644 --- a/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts +++ b/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts @@ -19,19 +19,21 @@ describe('getSafeOperationsByAddress', () => { describe('should fail', () => { it('should fail if safeAddress is empty', async () => { await chai - .expect(safeApiKit.getSafeOperationsByAddress('')) + .expect(safeApiKit.getSafeOperationsByAddress({ safeAddress: '' })) .to.be.rejectedWith('Safe address must not be empty') }) it('should fail if safeAddress is invalid', async () => { await chai - .expect(safeApiKit.getSafeOperationsByAddress('0x123')) + .expect(safeApiKit.getSafeOperationsByAddress({ safeAddress: '0x123' })) .to.be.rejectedWith('Invalid Ethereum address 0x123') }) }) it('should get the SafeOperation list', async () => { - const safeOperations = await safeApiKit.getSafeOperationsByAddress(SAFE_ADDRESS) + const safeOperations = await safeApiKit.getSafeOperationsByAddress({ + safeAddress: SAFE_ADDRESS + }) chai.expect(safeOperations).to.have.property('count').greaterThan(1) chai.expect(safeOperations).to.have.property('results').to.be.an('array') diff --git a/packages/api-kit/tests/endpoint/index.test.ts b/packages/api-kit/tests/endpoint/index.test.ts index 8c3a05ada..6259f6dd1 100644 --- a/packages/api-kit/tests/endpoint/index.test.ts +++ b/packages/api-kit/tests/endpoint/index.test.ts @@ -648,7 +648,7 @@ describe('Endpoint tests', () => { it('getSafeOperationsByAddress', async () => { await chai - .expect(safeApiKit.getSafeOperationsByAddress(safeAddress)) + .expect(safeApiKit.getSafeOperationsByAddress({ safeAddress })) .to.be.eventually.deep.equals({ data: { success: true } }) chai.expect(fetchData).to.have.been.calledWith({ url: `${txServiceBaseUrl}/v1/safes/${safeAddress}/safe-operations/`, From 56315ef201dbcf52b949ac255490ec7f25b0f9ee Mon Sep 17 00:00:00 2001 From: Tim Schwarz <4171783+tmjssz@users.noreply.github.com> Date: Tue, 14 May 2024 10:38:56 +0200 Subject: [PATCH 17/24] Refactor `addSafeOperation` function params to remove internal coupling with Safe4337 module The function expects a `UserOperation` object now, instead of a `SafeOperation` object. --- packages/api-kit/src/SafeApiKit.ts | 44 +++++----- .../src/types/safeTransactionServiceTypes.ts | 19 +++-- .../tests/e2e/addSafeOperation.test.ts | 85 +++++++++---------- packages/api-kit/tests/endpoint/index.test.ts | 62 +++++++------- 4 files changed, 108 insertions(+), 102 deletions(-) diff --git a/packages/api-kit/src/SafeApiKit.ts b/packages/api-kit/src/SafeApiKit.ts index 406e56318..60f857e95 100644 --- a/packages/api-kit/src/SafeApiKit.ts +++ b/packages/api-kit/src/SafeApiKit.ts @@ -785,13 +785,14 @@ class SafeApiKit { * @throws "Invalid Safe address {safeAddress}" * @throws "Module address must not be empty" * @throws "Invalid module address {moduleAddress}" - * @throws "SafeOperation is not signed by the given signer {signerAddress}" + * @throws "Signature must not be empty" */ async addSafeOperation({ + entryPoint, moduleAddress: moduleAddressProp, + options, safeAddress: safeAddressProp, - safeOperation, - signer + userOperation }: AddSafeOperationProps): Promise { let safeAddress: string, moduleAddress: string @@ -814,32 +815,29 @@ class SafeApiKit { throw new Error(`Invalid module address ${moduleAddressProp}`) } - const signerAddress = await signer.getAddress() - const signature = safeOperation.getSignature(signerAddress) - - if (!signature) { - throw new Error(`SafeOperation is not signed by the given signer ${signerAddress}`) + if (isEmptyData(userOperation.signature)) { + throw new Error('Signature must not be empty') } - const { data } = safeOperation - return sendRequest({ url: `${this.#txServiceBaseUrl}/v1/safes/${safeAddress}/safe-operations/`, method: HttpMethod.Post, body: { - nonce: Number(data.nonce), - initCode: isEmptyData(data.initCode) ? null : data.initCode, - callData: data.callData, - callDataGasLimit: data.callGasLimit.toString(), - verificationGasLimit: data.verificationGasLimit.toString(), - preVerificationGas: data.preVerificationGas.toString(), - maxFeePerGas: data.maxFeePerGas.toString(), - maxPriorityFeePerGas: data.maxPriorityFeePerGas.toString(), - paymasterAndData: isEmptyData(data.paymasterAndData) ? null : data.paymasterAndData, - entryPoint: data.entryPoint, - validAfter: !data.validAfter ? null : data.validAfter, - validUntil: !data.validUntil ? null : data.validUntil, - signature: signature.data, + nonce: Number(userOperation.nonce), + initCode: isEmptyData(userOperation.initCode) ? null : userOperation.initCode, + callData: userOperation.callData, + callDataGasLimit: userOperation.callGasLimit.toString(), + verificationGasLimit: userOperation.verificationGasLimit.toString(), + preVerificationGas: userOperation.preVerificationGas.toString(), + maxFeePerGas: userOperation.maxFeePerGas.toString(), + maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas.toString(), + paymasterAndData: isEmptyData(userOperation.paymasterAndData) + ? null + : userOperation.paymasterAndData, + entryPoint, + validAfter: !options?.validAfter ? null : options?.validAfter, + validUntil: !options?.validUntil ? null : options?.validUntil, + signature: userOperation.signature, moduleAddress } }) diff --git a/packages/api-kit/src/types/safeTransactionServiceTypes.ts b/packages/api-kit/src/types/safeTransactionServiceTypes.ts index ed3b83523..755e4995c 100644 --- a/packages/api-kit/src/types/safeTransactionServiceTypes.ts +++ b/packages/api-kit/src/types/safeTransactionServiceTypes.ts @@ -1,8 +1,8 @@ import { Signer, TypedDataDomain, TypedDataField } from 'ethers' import { SafeMultisigTransactionResponse, - SafeOperation, - SafeTransactionData + SafeTransactionData, + UserOperation } from '@safe-global/safe-core-sdk-types' export type SafeServiceInfoResponse = { @@ -342,12 +342,19 @@ export type GetSafeOperationListResponse = { } export type AddSafeOperationProps = { + /** Address of the EntryPoint contract */ + entryPoint: string /** Address of the Safe4337Module contract */ moduleAddress: string /** Address of the Safe to add a SafeOperation for */ safeAddress: string - /** Signed SafeOperation object to add */ - safeOperation: SafeOperation - /** Signer instance */ - signer: Signer + /** UserOperation object to add */ + userOperation: UserOperation + /** Options object */ + options?: { + /** The UserOperation will remain valid until this block's timestamp */ + validUntil?: number + /** The UserOperation will be valid after this block's timestamp */ + validAfter?: number + } } diff --git a/packages/api-kit/tests/e2e/addSafeOperation.test.ts b/packages/api-kit/tests/e2e/addSafeOperation.test.ts index fdd19b271..0163ff825 100644 --- a/packages/api-kit/tests/e2e/addSafeOperation.test.ts +++ b/packages/api-kit/tests/e2e/addSafeOperation.test.ts @@ -3,7 +3,7 @@ import chaiAsPromised from 'chai-as-promised' import { Wallet, ethers } from 'ethers' import sinon from 'sinon' import sinonChai from 'sinon-chai' -import { EthAdapter } from '@safe-global/safe-core-sdk-types' +import { EthAdapter, SafeOperation } from '@safe-global/safe-core-sdk-types' import SafeApiKit from '@safe-global/api-kit' import { Safe4337Pack } from '@safe-global/relay-kit' import { generateTransferCallData } from '@safe-global/relay-kit/src/packs/safe-4337/testing-utils/helpers' @@ -49,6 +49,13 @@ describe('addSafeOperation', () => { .returns( Promise.resolve({ fast: { maxFeePerGas: '0x3b9aca00', maxPriorityFeePerGas: '0x3b9aca00' } }) ) + providerStub.withArgs(RPC_4337_CALLS.ESTIMATE_USER_OPERATION_GAS, sinon.match.any).returns( + Promise.resolve({ + preVerificationGas: BigInt(Date.now()), + callGasLimit: BigInt(Date.now()), + verificationGasLimit: BigInt(Date.now()) + }) + ) providerStub.callThrough() @@ -80,18 +87,32 @@ describe('addSafeOperation', () => { })?.networkAddresses[chainId] as string }) + const getAddSafeOperationProps = async (safeOperation: SafeOperation) => { + const userOperation = safeOperation.toUserOperation() + userOperation.signature = safeOperation.encodedSignatures() + return { + entryPoint: safeOperation.data.entryPoint, + moduleAddress, + safeAddress: SAFE_ADDRESS, + userOperation, + options: { + validAfter: safeOperation.data.validAfter, + validUntil: safeOperation.data.validUntil + } + } + } + describe('should fail', () => { it('if safeAddress is empty', async () => { const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] }) const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) + const addSafeOperationProps = await getAddSafeOperationProps(signedSafeOperation) await chai .expect( safeApiKit.addSafeOperation({ - moduleAddress, - safeAddress: '', - safeOperation: signedSafeOperation, - signer + ...addSafeOperationProps, + safeAddress: '' }) ) .to.be.rejectedWith('Safe address must not be empty') @@ -100,14 +121,13 @@ describe('addSafeOperation', () => { it('if safeAddress is invalid', async () => { const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] }) const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) + const addSafeOperationProps = await getAddSafeOperationProps(signedSafeOperation) await chai .expect( safeApiKit.addSafeOperation({ - moduleAddress, - safeAddress: '0x123', - safeOperation: signedSafeOperation, - signer + ...addSafeOperationProps, + safeAddress: '0x123' }) ) .to.be.rejectedWith('Invalid Safe address 0x123') @@ -116,14 +136,13 @@ describe('addSafeOperation', () => { it('if moduleAddress is empty', async () => { const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] }) const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) + const addSafeOperationProps = await getAddSafeOperationProps(signedSafeOperation) await chai .expect( safeApiKit.addSafeOperation({ - moduleAddress: '', - safeAddress: SAFE_ADDRESS, - safeOperation: signedSafeOperation, - signer + ...addSafeOperationProps, + moduleAddress: '' }) ) .to.be.rejectedWith('Module address must not be empty') @@ -132,14 +151,13 @@ describe('addSafeOperation', () => { it('if moduleAddress is invalid', async () => { const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] }) const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) + const addSafeOperationProps = await getAddSafeOperationProps(signedSafeOperation) await chai .expect( safeApiKit.addSafeOperation({ - moduleAddress: '0x234', - safeAddress: SAFE_ADDRESS, - safeOperation: signedSafeOperation, - signer + ...addSafeOperationProps, + moduleAddress: '0x234' }) ) .to.be.rejectedWith('Invalid module address 0x234') @@ -147,47 +165,26 @@ describe('addSafeOperation', () => { it('if the SafeOperation is not signed', async () => { const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] }) + const addSafeOperationProps = await getAddSafeOperationProps(safeOperation) await chai - .expect( - safeApiKit.addSafeOperation({ - moduleAddress, - safeAddress: SAFE_ADDRESS, - safeOperation, - signer - }) - ) - .to.be.rejectedWith( - 'SafeOperation is not signed by the given signer 0x56e2C102c664De6DfD7315d12c0178b61D16F171' - ) + .expect(safeApiKit.addSafeOperation(addSafeOperationProps)) + .to.be.rejectedWith('Signature must not be empty') }) }) it('should add a new SafeOperation', async () => { - providerStub.withArgs(RPC_4337_CALLS.ESTIMATE_USER_OPERATION_GAS, sinon.match.any).returns( - Promise.resolve({ - preVerificationGas: BigInt(Date.now()), - callGasLimit: BigInt(Date.now()), - verificationGasLimit: BigInt(Date.now()) - }) - ) - const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] }) const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) + const addSafeOperationProps = await getAddSafeOperationProps(signedSafeOperation) + // Get the number of SafeOperations before adding a new one const safeOperationsBefore = await safeApiKit.getSafeOperationsByAddress({ safeAddress: SAFE_ADDRESS }) const initialNumSafeOperations = safeOperationsBefore.results.length - await chai.expect( - safeApiKit.addSafeOperation({ - moduleAddress, - safeAddress: SAFE_ADDRESS, - safeOperation: signedSafeOperation, - signer - }) - ).to.be.fulfilled + await chai.expect(safeApiKit.addSafeOperation(addSafeOperationProps)).to.be.fulfilled const safeOperationsAfter = await safeApiKit.getSafeOperationsByAddress({ safeAddress: SAFE_ADDRESS diff --git a/packages/api-kit/tests/endpoint/index.test.ts b/packages/api-kit/tests/endpoint/index.test.ts index 6259f6dd1..8357472de 100644 --- a/packages/api-kit/tests/endpoint/index.test.ts +++ b/packages/api-kit/tests/endpoint/index.test.ts @@ -7,7 +7,7 @@ import SafeApiKit, { } from '@safe-global/api-kit/index' import * as httpRequests from '@safe-global/api-kit/utils/httpRequests' import Safe from '@safe-global/protocol-kit' -import { EthAdapter, SafeOperation } from '@safe-global/safe-core-sdk-types' +import { EthAdapter, UserOperation } from '@safe-global/safe-core-sdk-types' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import sinon from 'sinon' @@ -669,50 +669,54 @@ describe('Endpoint tests', () => { }) it('addSafeOperation', async () => { - const signature = '0xsignature' const moduleAddress = '0xa581c4A4DB7175302464fF3C06380BC3270b4037' - const safeOperation = { - data: { - nonce: 42, - initCode: '0xfbc38024f74946d9ec31e0c8658dd65e335c6e57c14575250787ec5fb270c08a', - callData: - '0x7bb374280000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c72380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044a9059cbb00000000000000000000000060c4ab82d06fd7dfe9517e17736c2dcc77443ef000000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000', - callGasLimit: '150799', - verificationGasLimit: '200691', - preVerificationGas: '50943', - maxFeePerGas: '1949282597', - maxPriorityFeePerGas: '1380000000', - paymasterAndData: '0xdff7fa1077bce740a6a212b3995990682c0ba66d', - entryPoint: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789', - validAfter: '2024-01-01T00:00:00Z', - validUntil: '2024-04-12T00:00:00Z' - }, - getSignature: () => ({ data: signature }) - } as unknown as SafeOperation + const userOperation: UserOperation = { + sender: safeAddress, + nonce: '42', + initCode: '0xfbc38024f74946d9ec31e0c8658dd65e335c6e57c14575250787ec5fb270c08a', + callData: + '0x7bb374280000000000000000000000001c7d4b196cb0c7b01d743fbc6116a902379c72380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044a9059cbb00000000000000000000000060c4ab82d06fd7dfe9517e17736c2dcc77443ef000000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000000000', + callGasLimit: 150799n, + verificationGasLimit: 200691n, + preVerificationGas: 50943n, + maxFeePerGas: 1949282597n, + maxPriorityFeePerGas: 1380000000n, + paymasterAndData: '0xdff7fa1077bce740a6a212b3995990682c0ba66d', + signature: '0xsignature' + } + + const entryPoint = '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789' + const options = { validAfter: 123, validUntil: 234 } await chai .expect( safeApiKit.addSafeOperation({ + entryPoint, moduleAddress, + options, safeAddress, - safeOperation, - signer + userOperation }) ) .to.be.eventually.deep.equals({ data: { success: true } }) - // We need to rename the "callGasLimit" prop here because the api endpoint expects it as "callDataGasLimit" - const { callGasLimit: callDataGasLimit, ...safeOperationWithoutCallGasLimit } = - safeOperation.data - chai.expect(fetchData).to.have.been.calledWith({ url: `${txServiceBaseUrl}/v1/safes/${safeAddress}/safe-operations/`, method: 'post', body: { - ...safeOperationWithoutCallGasLimit, - callDataGasLimit, - signature, + nonce: Number(userOperation.nonce), + initCode: userOperation.initCode, + callData: userOperation.callData, + callDataGasLimit: userOperation.callGasLimit.toString(), + verificationGasLimit: userOperation.verificationGasLimit.toString(), + preVerificationGas: userOperation.preVerificationGas.toString(), + maxFeePerGas: userOperation.maxFeePerGas.toString(), + maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas.toString(), + paymasterAndData: userOperation.paymasterAndData, + entryPoint, + ...options, + signature: userOperation.signature, moduleAddress } }) From 05054d381d24b820ec179ee44d35ce30d9812224 Mon Sep 17 00:00:00 2001 From: Tim <4171783+tmjssz@users.noreply.github.com> Date: Tue, 14 May 2024 18:31:20 +0200 Subject: [PATCH 18/24] Update packages/api-kit/.env.example Remove unused PIMLICO_API_KEY Co-authored-by: Daniel <25051234+dasanra@users.noreply.github.com> --- packages/api-kit/.env.example | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/api-kit/.env.example b/packages/api-kit/.env.example index e7285b3f9..c519ed0be 100644 --- a/packages/api-kit/.env.example +++ b/packages/api-kit/.env.example @@ -1,3 +1,2 @@ MNEMONIC= -PK= -PIMLICO_API_KEY= \ No newline at end of file +PK= \ No newline at end of file From cd124d8e88fe854a9d367f5079e8d74ffb454955 Mon Sep 17 00:00:00 2001 From: Tim Schwarz <4171783+tmjssz@users.noreply.github.com> Date: Wed, 15 May 2024 14:21:18 +0200 Subject: [PATCH 19/24] Adapt SafeOperation response object types to latest API changes --- .../src/types/safeTransactionServiceTypes.ts | 12 ++++--- .../e2e/getSafeOperationsByAddress.test.ts | 32 ++++++++++--------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/packages/api-kit/src/types/safeTransactionServiceTypes.ts b/packages/api-kit/src/types/safeTransactionServiceTypes.ts index 755e4995c..d96bf883e 100644 --- a/packages/api-kit/src/types/safeTransactionServiceTypes.ts +++ b/packages/api-kit/src/types/safeTransactionServiceTypes.ts @@ -297,13 +297,10 @@ export type SafeOperationConfirmation = { readonly signatureType: string } -export type SafeOperationResponse = { - readonly created: string - readonly modified: string +export type UserOperationResponse = { readonly ethereumTxHash: string readonly sender: string readonly userOperationHash: string - readonly safeOperationHash: string readonly nonce: number readonly initCode: null | string readonly callData: null | string @@ -316,11 +313,18 @@ export type SafeOperationResponse = { readonly paymasterData: null | string readonly signature: string readonly entryPoint: string +} + +export type SafeOperationResponse = { + readonly created: string + readonly modified: string + readonly safeOperationHash: string readonly validAfter: string readonly validUntil: string readonly moduleAddress: string readonly confirmations?: Array readonly preparedSignature?: string + readonly userOperation?: UserOperationResponse } export type GetSafeOperationListProps = { diff --git a/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts b/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts index 3120ba181..0cf2e02b4 100644 --- a/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts +++ b/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts @@ -41,27 +41,29 @@ describe('getSafeOperationsByAddress', () => { safeOperations.results.every((safeOperation) => { chai.expect(safeOperation).to.have.property('created') chai.expect(safeOperation).to.have.property('modified') - chai.expect(safeOperation).to.have.property('ethereumTxHash') - chai.expect(safeOperation).to.have.property('sender').to.eq(SAFE_ADDRESS) - chai.expect(safeOperation).to.have.property('userOperationHash') chai.expect(safeOperation).to.have.property('safeOperationHash') - chai.expect(safeOperation).to.have.property('nonce') - chai.expect(safeOperation).to.have.property('initCode') - chai.expect(safeOperation).to.have.property('callData') - chai.expect(safeOperation).to.have.property('callDataGasLimit') - chai.expect(safeOperation).to.have.property('verificationGasLimit') - chai.expect(safeOperation).to.have.property('preVerificationGas') - chai.expect(safeOperation).to.have.property('maxFeePerGas') - chai.expect(safeOperation).to.have.property('maxPriorityFeePerGas') - chai.expect(safeOperation).to.have.property('paymaster') - chai.expect(safeOperation).to.have.property('paymasterData') - chai.expect(safeOperation).to.have.property('signature') - chai.expect(safeOperation).to.have.property('entryPoint') chai.expect(safeOperation).to.have.property('validAfter') chai.expect(safeOperation).to.have.property('validUntil') chai.expect(safeOperation).to.have.property('moduleAddress') chai.expect(safeOperation).to.have.property('confirmations').to.be.an('array') chai.expect(safeOperation).to.have.property('preparedSignature') + chai.expect(safeOperation).to.have.property('userOperation') + + chai.expect(safeOperation.userOperation).to.have.property('ethereumTxHash') + chai.expect(safeOperation.userOperation).to.have.property('sender').to.eq(SAFE_ADDRESS) + chai.expect(safeOperation.userOperation).to.have.property('userOperationHash') + chai.expect(safeOperation.userOperation).to.have.property('nonce') + chai.expect(safeOperation.userOperation).to.have.property('initCode') + chai.expect(safeOperation.userOperation).to.have.property('callData') + chai.expect(safeOperation.userOperation).to.have.property('callDataGasLimit') + chai.expect(safeOperation.userOperation).to.have.property('verificationGasLimit') + chai.expect(safeOperation.userOperation).to.have.property('preVerificationGas') + chai.expect(safeOperation.userOperation).to.have.property('maxFeePerGas') + chai.expect(safeOperation.userOperation).to.have.property('maxPriorityFeePerGas') + chai.expect(safeOperation.userOperation).to.have.property('paymaster') + chai.expect(safeOperation.userOperation).to.have.property('paymasterData') + chai.expect(safeOperation.userOperation).to.have.property('signature') + chai.expect(safeOperation.userOperation).to.have.property('entryPoint') }) }) }) From 9a4ad4f7094b46f8bde9350cfe6bb417b7de2921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Thu, 16 May 2024 13:19:54 +0200 Subject: [PATCH 20/24] Update new tests to use the new getKits utils --- .../tests/e2e/addSafeOperation.test.ts | 27 +++++++++---------- .../tests/e2e/getSafeOperation.test.ts | 5 ++-- .../e2e/getSafeOperationsByAddress.test.ts | 5 ++-- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/packages/api-kit/tests/e2e/addSafeOperation.test.ts b/packages/api-kit/tests/e2e/addSafeOperation.test.ts index 0163ff825..41997ddcc 100644 --- a/packages/api-kit/tests/e2e/addSafeOperation.test.ts +++ b/packages/api-kit/tests/e2e/addSafeOperation.test.ts @@ -1,17 +1,17 @@ import chai from 'chai' import chaiAsPromised from 'chai-as-promised' -import { Wallet, ethers } from 'ethers' +import { ethers } from 'ethers' import sinon from 'sinon' import sinonChai from 'sinon-chai' -import { EthAdapter, SafeOperation } from '@safe-global/safe-core-sdk-types' +import { SafeOperation } from '@safe-global/safe-core-sdk-types' +import Safe from '@safe-global/protocol-kit' import SafeApiKit from '@safe-global/api-kit' import { Safe4337Pack } from '@safe-global/relay-kit' import { generateTransferCallData } from '@safe-global/relay-kit/src/packs/safe-4337/testing-utils/helpers' import { RPC_4337_CALLS } from '@safe-global/relay-kit/packs/safe-4337/constants' -import { EthersAdapter } from 'packages/protocol-kit' import { getSafe4337ModuleDeployment } from '@safe-global/safe-modules-deployments' import config from '../utils/config' -import { getServiceClient } from '../utils/setupServiceClient' +import { getKits } from '../utils/setupKits' chai.use(chaiAsPromised) chai.use(sinonChai) @@ -24,9 +24,8 @@ const BUNDLER_URL = `https://bundler.url` const TX_SERVICE_URL = 'https://safe-transaction-sepolia.staging.5afe.dev/api' let safeApiKit: SafeApiKit -let ethAdapter: EthAdapter +let protocolKit: Safe let safe4337Pack: Safe4337Pack -let signer: Wallet let moduleAddress: string describe('addSafeOperation', () => { @@ -60,16 +59,16 @@ describe('addSafeOperation', () => { providerStub.callThrough() before(async () => { - ;({ safeApiKit, ethAdapter, signer } = await getServiceClient(SIGNER_PK, TX_SERVICE_URL)) - - const ethersAdapter = new EthersAdapter({ - ethers, - signerOrProvider: signer - }) + ;({ safeApiKit, protocolKit } = await getKits({ + safeAddress: SAFE_ADDRESS, + signer: SIGNER_PK, + txServiceUrl: TX_SERVICE_URL + })) safe4337Pack = await Safe4337Pack.init({ + provider: protocolKit.getSafeProvider().provider, + signer: protocolKit.getSafeProvider().signer, options: { safeAddress: SAFE_ADDRESS }, - ethersAdapter, rpcUrl: config.JSON_RPC, bundlerUrl: BUNDLER_URL, paymasterOptions: { @@ -78,7 +77,7 @@ describe('addSafeOperation', () => { } }) - const chainId = (await ethAdapter.getChainId()).toString() + const chainId = (await protocolKit.getSafeProvider().getChainId()).toString() moduleAddress = getSafe4337ModuleDeployment({ released: true, diff --git a/packages/api-kit/tests/e2e/getSafeOperation.test.ts b/packages/api-kit/tests/e2e/getSafeOperation.test.ts index dcb1054d0..3706dc122 100644 --- a/packages/api-kit/tests/e2e/getSafeOperation.test.ts +++ b/packages/api-kit/tests/e2e/getSafeOperation.test.ts @@ -1,19 +1,18 @@ import SafeApiKit from '@safe-global/api-kit' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' -import { getServiceClient } from '../utils/setupServiceClient' +import { getApiKit } from '../utils/setupKits' chai.use(chaiAsPromised) const SAFE_ADDRESS = '0x60C4Ab82D06Fd7dFE9517e17736C2Dcc77443EF0' // v1.4.1 -const SIGNER_PK = '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676' const TX_SERVICE_URL = 'https://safe-transaction-sepolia.staging.5afe.dev/api' let safeApiKit: SafeApiKit describe('getSafeOperation', () => { before(async () => { - ;({ safeApiKit } = await getServiceClient(SIGNER_PK, TX_SERVICE_URL)) + safeApiKit = getApiKit(TX_SERVICE_URL) }) describe('should fail', () => { diff --git a/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts b/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts index 0cf2e02b4..a8b2caae4 100644 --- a/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts +++ b/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts @@ -1,19 +1,18 @@ import SafeApiKit from '@safe-global/api-kit' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' -import { getServiceClient } from '../utils/setupServiceClient' +import { getApiKit } from '../utils/getKits' chai.use(chaiAsPromised) const SAFE_ADDRESS = '0x60C4Ab82D06Fd7dFE9517e17736C2Dcc77443EF0' // v1.4.1 -const SIGNER_PK = '0x83a415ca62e11f5fa5567e98450d0f82ae19ff36ef876c10a8d448c788a53676' const TX_SERVICE_URL = 'https://safe-transaction-sepolia.staging.5afe.dev/api' let safeApiKit: SafeApiKit describe('getSafeOperationsByAddress', () => { before(async () => { - ;({ safeApiKit } = await getServiceClient(SIGNER_PK, TX_SERVICE_URL)) + safeApiKit = getApiKit(TX_SERVICE_URL) }) describe('should fail', () => { From 29b6cd1c487464f89b9b0741a92d638c54a3cedb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Thu, 16 May 2024 13:24:36 +0200 Subject: [PATCH 21/24] Fix import --- packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts b/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts index a8b2caae4..addc2c1be 100644 --- a/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts +++ b/packages/api-kit/tests/e2e/getSafeOperationsByAddress.test.ts @@ -1,7 +1,7 @@ import SafeApiKit from '@safe-global/api-kit' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' -import { getApiKit } from '../utils/getKits' +import { getApiKit } from '../utils/setupKits' chai.use(chaiAsPromised) From 8099ed401f8ab88034aeee75db7b3f361640d186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Thu, 16 May 2024 14:09:44 +0200 Subject: [PATCH 22/24] Rename master-copies --- packages/api-kit/src/SafeApiKit.ts | 2 +- packages/api-kit/tests/endpoint/index.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/api-kit/src/SafeApiKit.ts b/packages/api-kit/src/SafeApiKit.ts index 362b15d33..349420324 100644 --- a/packages/api-kit/src/SafeApiKit.ts +++ b/packages/api-kit/src/SafeApiKit.ts @@ -101,7 +101,7 @@ class SafeApiKit { */ async getServiceSingletonsInfo(): Promise { return sendRequest({ - url: `${this.#txServiceBaseUrl}/v1/about/master-copies`, + url: `${this.#txServiceBaseUrl}/v1/about/singletons`, method: HttpMethod.Get }) } diff --git a/packages/api-kit/tests/endpoint/index.test.ts b/packages/api-kit/tests/endpoint/index.test.ts index 1f005d423..6dd5ce15f 100644 --- a/packages/api-kit/tests/endpoint/index.test.ts +++ b/packages/api-kit/tests/endpoint/index.test.ts @@ -67,7 +67,7 @@ describe('Endpoint tests', () => { .expect(safeApiKit.getServiceSingletonsInfo()) .to.be.eventually.deep.equals({ data: { success: true } }) chai.expect(fetchData).to.have.been.calledWith({ - url: `${txServiceBaseUrl}/v1/about/master-copies`, + url: `${txServiceBaseUrl}/v1/about/singletons`, method: 'get' }) }) From 6ac7ea921dd8525a48abb76ca6843ef275114275 Mon Sep 17 00:00:00 2001 From: Tim Schwarz <4171783+tmjssz@users.noreply.github.com> Date: Thu, 16 May 2024 17:05:58 +0200 Subject: [PATCH 23/24] Rename `SafeOperation` class to `EthSafeOperation` This is for better differentiation with the corresponding interface. --- .../src/packs/safe-4337/Safe4337Pack.test.ts | 10 +++---- .../src/packs/safe-4337/Safe4337Pack.ts | 30 +++++++++---------- .../src/packs/safe-4337/SafeOperation.test.ts | 12 ++++---- .../src/packs/safe-4337/SafeOperation.ts | 6 ++-- .../relay-kit/src/packs/safe-4337/types.ts | 6 ++-- 5 files changed, 32 insertions(+), 32 deletions(-) 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 808da64d7..212d87d1c 100644 --- a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.test.ts +++ b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.test.ts @@ -6,7 +6,7 @@ import { } from '@safe-global/safe-modules-deployments' import { MetaTransactionData, OperationType } from '@safe-global/safe-core-sdk-types' import { Safe4337Pack } from './Safe4337Pack' -import SafeOperation from './SafeOperation' +import EthSafeOperation from './SafeOperation' import * as constants from './constants' import * as fixtures from './testing-utils/fixtures' import { createSafe4337Pack, generateTransferCallData } from './testing-utils/helpers' @@ -308,7 +308,7 @@ describe('Safe4337Pack', () => { transactions }) - expect(safeOperation).toBeInstanceOf(SafeOperation) + expect(safeOperation).toBeInstanceOf(EthSafeOperation) expect(safeOperation.data).toMatchObject({ safe: fixtures.SAFE_ADDRESS_v1_4_1, entryPoint: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789', @@ -338,7 +338,7 @@ describe('Safe4337Pack', () => { transactions: [transferUSDC] }) - expect(safeOperation).toBeInstanceOf(SafeOperation) + expect(safeOperation).toBeInstanceOf(EthSafeOperation) expect(safeOperation.data).toMatchObject({ safe: fixtures.SAFE_ADDRESS_v1_4_1, entryPoint: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789', @@ -397,7 +397,7 @@ describe('Safe4337Pack', () => { transactions: [transferUSDC] }) - expect(sponsoredSafeOperation).toBeInstanceOf(SafeOperation) + expect(sponsoredSafeOperation).toBeInstanceOf(EthSafeOperation) expect(sponsoredSafeOperation.data).toMatchObject({ safe: fixtures.SAFE_ADDRESS_v1_4_1, entryPoint: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789', @@ -470,7 +470,7 @@ describe('Safe4337Pack', () => { const batch = [transferUSDC, approveTransaction] - expect(sponsoredSafeOperation).toBeInstanceOf(SafeOperation) + expect(sponsoredSafeOperation).toBeInstanceOf(EthSafeOperation) expect(sponsoredSafeOperation.data).toMatchObject({ safe: fixtures.SAFE_ADDRESS_v1_4_1, entryPoint: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789', diff --git a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts index 21c7468df..aa5a78a9a 100644 --- a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts +++ b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts @@ -19,7 +19,7 @@ import { getAddModulesLibDeployment, getSafe4337ModuleDeployment } from '@safe-global/safe-modules-deployments' -import SafeOperation from './SafeOperation' +import EthSafeOperation from './SafeOperation' import { EstimateFeeProps, Safe4337CreateTransactionProps, @@ -54,9 +54,9 @@ const MAX_ERC20_AMOUNT_TO_APPROVE = */ export class Safe4337Pack extends RelayKitBasePack<{ EstimateFeeProps: EstimateFeeProps - EstimateFeeResult: SafeOperation + EstimateFeeResult: EthSafeOperation CreateTransactionProps: Safe4337CreateTransactionProps - CreateTransactionResult: SafeOperation + CreateTransactionResult: EthSafeOperation ExecuteTransactionProps: Safe4337ExecutableProps ExecuteTransactionResult: string }> { @@ -270,15 +270,15 @@ export class Safe4337Pack extends RelayKitBasePack<{ * Estimates gas for the SafeOperation. * * @param {EstimateFeeProps} props - The parameters for the gas estimation. - * @param {SafeOperation} props.safeOperation - The SafeOperation to estimate the gas. + * @param {EthSafeOperation} props.safeOperation - The SafeOperation to estimate the gas. * @param {IFeeEstimator} props.feeEstimator - The function to estimate the gas. - * @return {Promise} The Promise object that will be resolved into the gas estimation. + * @return {Promise} The Promise object that will be resolved into the gas estimation. */ async getEstimateFee({ safeOperation, feeEstimator = new PimlicoFeeEstimator() - }: EstimateFeeProps): Promise { + }: EstimateFeeProps): Promise { const setupEstimationData = await feeEstimator?.setupEstimation?.({ bundlerUrl: this.#BUNDLER_URL, entryPoint: this.#ENTRYPOINT_ADDRESS, @@ -340,12 +340,12 @@ export class Safe4337Pack extends RelayKitBasePack<{ * * @param {MetaTransactionData[]} transactions - The transactions to batch in a SafeOperation. * @param options - Optional configuration options for the transaction creation. - * @return {Promise} The Promise object will resolve a SafeOperation. + * @return {Promise} The Promise object will resolve a SafeOperation. */ async createTransaction({ transactions, options = {} - }: Safe4337CreateTransactionProps): Promise { + }: Safe4337CreateTransactionProps): Promise { const safeAddress = await this.protocolKit.getAddress() const nonce = await this.#getAccountNonce(safeAddress) @@ -403,7 +403,7 @@ export class Safe4337Pack extends RelayKitBasePack<{ userOperation.initCode = await this.protocolKit.getInitCode() } - const safeOperation = new SafeOperation(userOperation, { + const safeOperation = new EthSafeOperation(userOperation, { entryPoint: this.#ENTRYPOINT_ADDRESS, validUntil, validAfter @@ -418,14 +418,14 @@ export class Safe4337Pack extends RelayKitBasePack<{ /** * Signs a safe operation. * - * @param {SafeOperation} safeOperation - The SafeOperation to sign. + * @param {EthSafeOperation} safeOperation - The SafeOperation to sign. * @param {SigningMethod} signingMethod - The signing method to use. - * @return {Promise} The Promise object will resolve to the signed SafeOperation. + * @return {Promise} The Promise object will resolve to the signed SafeOperation. */ async signSafeOperation( - safeOperation: SafeOperation, + safeOperation: EthSafeOperation, signingMethod: SigningMethod = SigningMethod.ETH_SIGN_TYPED_DATA_V4 - ): Promise { + ): Promise { const owners = await this.protocolKit.getOwners() const signerAddress = await this.protocolKit.getSafeProvider().getSignerAddress() if (!signerAddress) { @@ -455,7 +455,7 @@ export class Safe4337Pack extends RelayKitBasePack<{ signature = await this.protocolKit.signHash(safeOpHash) } - const signedSafeOperation = new SafeOperation(safeOperation.toUserOperation(), { + const signedSafeOperation = new EthSafeOperation(safeOperation.toUserOperation(), { entryPoint: this.#ENTRYPOINT_ADDRESS, validUntil: safeOperation.data.validUntil, validAfter: safeOperation.data.validAfter @@ -473,7 +473,7 @@ export class Safe4337Pack extends RelayKitBasePack<{ /** * Executes the relay transaction. * - * @param {SafeOperation} safeOperation - The SafeOperation to execute. + * @param {EthSafeOperation} safeOperation - The SafeOperation to execute. * @return {Promise} The user operation hash. */ async executeTransaction({ diff --git a/packages/relay-kit/src/packs/safe-4337/SafeOperation.test.ts b/packages/relay-kit/src/packs/safe-4337/SafeOperation.test.ts index 85f1df10e..048b61527 100644 --- a/packages/relay-kit/src/packs/safe-4337/SafeOperation.test.ts +++ b/packages/relay-kit/src/packs/safe-4337/SafeOperation.test.ts @@ -1,11 +1,11 @@ import { ethers } from 'ethers' import { EthSafeSignature } from '@safe-global/protocol-kit' -import SafeOperation from './SafeOperation' +import EthSafeOperation from './SafeOperation' import * as fixtures from './testing-utils/fixtures' describe('SafeOperation', () => { it('should create a SafeOperation from an UserOperation', () => { - const safeOperation = new SafeOperation(fixtures.USER_OPERATION, { + const safeOperation = new EthSafeOperation(fixtures.USER_OPERATION, { entryPoint: fixtures.ENTRYPOINTS[0] }) @@ -29,7 +29,7 @@ describe('SafeOperation', () => { }) it('should add and retrieve signatures', () => { - const safeOperation = new SafeOperation(fixtures.USER_OPERATION, { + const safeOperation = new EthSafeOperation(fixtures.USER_OPERATION, { entryPoint: fixtures.ENTRYPOINTS[0] }) @@ -44,7 +44,7 @@ describe('SafeOperation', () => { }) it('should encode the signatures', () => { - const safeOperation = new SafeOperation(fixtures.USER_OPERATION, { + const safeOperation = new EthSafeOperation(fixtures.USER_OPERATION, { entryPoint: fixtures.ENTRYPOINTS[0] }) @@ -55,7 +55,7 @@ describe('SafeOperation', () => { }) it('should add estimations', () => { - const safeOperation = new SafeOperation(fixtures.USER_OPERATION, { + const safeOperation = new EthSafeOperation(fixtures.USER_OPERATION, { entryPoint: fixtures.ENTRYPOINTS[0] }) @@ -83,7 +83,7 @@ describe('SafeOperation', () => { }) it('should convert to UserOperation', () => { - const safeOperation = new SafeOperation(fixtures.USER_OPERATION, { + const safeOperation = new EthSafeOperation(fixtures.USER_OPERATION, { entryPoint: fixtures.ENTRYPOINTS[0] }) diff --git a/packages/relay-kit/src/packs/safe-4337/SafeOperation.ts b/packages/relay-kit/src/packs/safe-4337/SafeOperation.ts index b7dc1a9b0..2fa0db5d9 100644 --- a/packages/relay-kit/src/packs/safe-4337/SafeOperation.ts +++ b/packages/relay-kit/src/packs/safe-4337/SafeOperation.ts @@ -1,7 +1,7 @@ import { ethers } from 'ethers' import { EstimateGasData, - SafeOperation as SafeOperationType, + SafeOperation, SafeSignature, SafeUserOperation, UserOperation @@ -10,7 +10,7 @@ import { buildSignatureBytes } from '@safe-global/protocol-kit' type SafeOperationOptions = { entryPoint: string; validAfter?: number; validUntil?: number } -class SafeOperation implements SafeOperationType { +class EthSafeOperation implements SafeOperation { data: SafeUserOperation signatures: Map = new Map() @@ -81,4 +81,4 @@ class SafeOperation implements SafeOperationType { } } -export default SafeOperation +export default EthSafeOperation diff --git a/packages/relay-kit/src/packs/safe-4337/types.ts b/packages/relay-kit/src/packs/safe-4337/types.ts index 6ec23d6b0..1b78b3348 100644 --- a/packages/relay-kit/src/packs/safe-4337/types.ts +++ b/packages/relay-kit/src/packs/safe-4337/types.ts @@ -6,7 +6,7 @@ import { UserOperation } from '@safe-global/safe-core-sdk-types' import { ethers } from 'ethers' -import SafeOperation from './SafeOperation' +import EthSafeOperation from './SafeOperation' type ExistingSafeOptions = { safeAddress: string @@ -64,7 +64,7 @@ export type Safe4337CreateTransactionProps = { } export type Safe4337ExecutableProps = { - executable: SafeOperation + executable: EthSafeOperation } export type EstimateSponsoredGasData = { @@ -149,6 +149,6 @@ export interface IFeeEstimator { } export type EstimateFeeProps = { - safeOperation: SafeOperation + safeOperation: EthSafeOperation feeEstimator?: IFeeEstimator } From 26142c1228130d7bfa6cd0c3417bff7d7359a695 Mon Sep 17 00:00:00 2001 From: Daniel <25051234+dasanra@users.noreply.github.com> Date: Tue, 21 May 2024 16:10:29 +0200 Subject: [PATCH 24/24] chore: add deprecations re-exporting deprecated types --- packages/relay-kit/src/deprecated.ts | 14 ++++++++++++++ packages/relay-kit/src/index.ts | 2 ++ 2 files changed, 16 insertions(+) create mode 100644 packages/relay-kit/src/deprecated.ts diff --git a/packages/relay-kit/src/deprecated.ts b/packages/relay-kit/src/deprecated.ts new file mode 100644 index 000000000..6153fe043 --- /dev/null +++ b/packages/relay-kit/src/deprecated.ts @@ -0,0 +1,14 @@ +export type { + /** + * @deprecated Please import { EstimateGasData } from @safe-global/safe-core-sdk-types + */ + EstimateGasData, + /** + * @deprecated Please import { SafeUserOperation } from @safe-global/safe-core-sdk-types + */ + SafeUserOperation, + /** + * @deprecated Please import { UserOperation } from @safe-global/safe-core-sdk-types + */ + UserOperation +} from '@safe-global/safe-core-sdk-types' diff --git a/packages/relay-kit/src/index.ts b/packages/relay-kit/src/index.ts index efa1730a3..47c72b993 100644 --- a/packages/relay-kit/src/index.ts +++ b/packages/relay-kit/src/index.ts @@ -1,3 +1,5 @@ +export * from './deprecated' + export * from './packs/gelato/GelatoRelayPack' export * from './packs/gelato/types'