diff --git a/packages/api-kit/src/SafeApiKit.ts b/packages/api-kit/src/SafeApiKit.ts index 5cdafc0e4..7be9e42f6 100644 --- a/packages/api-kit/src/SafeApiKit.ts +++ b/packages/api-kit/src/SafeApiKit.ts @@ -23,7 +23,6 @@ import { SafeMultisigTransactionEstimate, SafeMultisigTransactionEstimateResponse, SafeMultisigTransactionListResponse, - SafeOperationResponse, SafeServiceInfoResponse, SignatureResponse, TokenInfoListResponse, @@ -36,10 +35,14 @@ import { validateEip3770Address, validateEthereumAddress } from '@safe-global/pr import { Eip3770Address, SafeMultisigConfirmationListResponse, - SafeMultisigTransactionResponse + SafeMultisigTransactionResponse, + SafeOperationResponse, + SafeOperation, + isSafeOperation } from '@safe-global/safe-core-sdk-types' import { TRANSACTION_SERVICE_URLS } from './utils/config' import { isEmptyData } from './utils' +import { getAddSafeOperationProps } from './utils/safeOperation' export interface SafeApiKitConfig { /** chainId - The chainId */ @@ -786,15 +789,23 @@ class SafeApiKit { * @throws "Invalid module address {moduleAddress}" * @throws "Signature must not be empty" */ - async addSafeOperation({ - entryPoint, - moduleAddress: moduleAddressProp, - options, - safeAddress: safeAddressProp, - userOperation - }: AddSafeOperationProps): Promise { + async addSafeOperation(safeOperation: AddSafeOperationProps | SafeOperation): Promise { let safeAddress: string, moduleAddress: string + let addSafeOperationProps: AddSafeOperationProps + + if (isSafeOperation(safeOperation)) { + addSafeOperationProps = await getAddSafeOperationProps(safeOperation) + } else { + addSafeOperationProps = safeOperation + } + const { + entryPoint, + moduleAddress: moduleAddressProp, + options, + safeAddress: safeAddressProp, + userOperation + } = addSafeOperationProps if (!safeAddressProp) { throw new Error('Safe address must not be empty') } diff --git a/packages/api-kit/src/types/safeTransactionServiceTypes.ts b/packages/api-kit/src/types/safeTransactionServiceTypes.ts index 6a16bcc96..e65fa6782 100644 --- a/packages/api-kit/src/types/safeTransactionServiceTypes.ts +++ b/packages/api-kit/src/types/safeTransactionServiceTypes.ts @@ -2,7 +2,8 @@ import { Signer, TypedDataDomain, TypedDataField } from 'ethers' import { SafeMultisigTransactionResponse, SafeTransactionData, - UserOperation + UserOperation, + SafeOperationResponse } from '@safe-global/safe-core-sdk-types' export type SafeServiceInfoResponse = { @@ -289,44 +290,6 @@ export type EIP712TypedData = { message: Record } -export type SafeOperationConfirmation = { - readonly created: string - readonly modified: string - readonly owner: string - readonly signature: string - readonly signatureType: string -} - -export type UserOperationResponse = { - readonly ethereumTxHash: string - readonly sender: string - readonly userOperationHash: string - readonly nonce: number - readonly initCode: null | string - readonly callData: null | string - readonly callGasLimit: 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 -} - -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 = { /** Address of the Safe to get SafeOperations for */ safeAddress: string diff --git a/packages/api-kit/src/utils/safeOperation.ts b/packages/api-kit/src/utils/safeOperation.ts new file mode 100644 index 000000000..9804aad40 --- /dev/null +++ b/packages/api-kit/src/utils/safeOperation.ts @@ -0,0 +1,17 @@ +import { SafeOperation } from '@safe-global/safe-core-sdk-types' + +export const getAddSafeOperationProps = async (safeOperation: SafeOperation) => { + const userOperation = safeOperation.toUserOperation() + userOperation.signature = safeOperation.encodedSignatures() // Without validity dates + + return { + entryPoint: safeOperation.data.entryPoint, + moduleAddress: safeOperation.moduleAddress, + safeAddress: safeOperation.data.safe, + userOperation, + options: { + validAfter: safeOperation.data.validAfter, + validUntil: safeOperation.data.validUntil + } + } +} diff --git a/packages/api-kit/tests/e2e/addSafeOperation.test.ts b/packages/api-kit/tests/e2e/addSafeOperation.test.ts index ff4f48e49..5cf3fa722 100644 --- a/packages/api-kit/tests/e2e/addSafeOperation.test.ts +++ b/packages/api-kit/tests/e2e/addSafeOperation.test.ts @@ -3,15 +3,14 @@ import chaiAsPromised from 'chai-as-promised' import { ethers } from 'ethers' import sinon from 'sinon' import sinonChai from 'sinon-chai' -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 { getSafe4337ModuleDeployment } from '@safe-global/safe-modules-deployments' import config from '../utils/config' import { getKits } from '../utils/setupKits' +import { getAddSafeOperationProps } from '@safe-global/api-kit/utils/safeOperation' chai.use(chaiAsPromised) chai.use(sinonChai) @@ -26,7 +25,6 @@ const TX_SERVICE_URL = 'https://safe-transaction-sepolia.staging.5afe.dev/api' let safeApiKit: SafeApiKit let protocolKit: Safe let safe4337Pack: Safe4337Pack -let moduleAddress: string describe('addSafeOperation', () => { const transferUSDC = { @@ -76,31 +74,8 @@ describe('addSafeOperation', () => { paymasterAddress: PAYMASTER_ADDRESS } }) - - const chainId = (await protocolKit.getSafeProvider().getChainId()).toString() - - moduleAddress = getSafe4337ModuleDeployment({ - released: true, - version: '0.2.0', - network: chainId - })?.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] }) @@ -172,7 +147,7 @@ describe('addSafeOperation', () => { }) }) - it('should add a new SafeOperation', async () => { + it('should add a new SafeOperation using an standard UserOperation and props', async () => { const safeOperation = await safe4337Pack.createTransaction({ transactions: [transferUSDC] }) const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) const addSafeOperationProps = await getAddSafeOperationProps(signedSafeOperation) @@ -190,4 +165,24 @@ describe('addSafeOperation', () => { }) chai.expect(safeOperationsAfter.count).to.equal(initialNumSafeOperations + 1) }) + + it('should add a new SafeOperation using a SafeOperation object from the relay-kit', async () => { + const safeOperation = await safe4337Pack.createTransaction({ + transactions: [transferUSDC, transferUSDC] + }) + const signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) + + // Get the number of SafeOperations before adding a new one + const safeOperationsBefore = await safeApiKit.getSafeOperationsByAddress({ + safeAddress: SAFE_ADDRESS + }) + const initialNumSafeOperations = safeOperationsBefore.count + + await chai.expect(safeApiKit.addSafeOperation(signedSafeOperation)).to.be.fulfilled + + const safeOperationsAfter = await safeApiKit.getSafeOperationsByAddress({ + safeAddress: SAFE_ADDRESS + }) + chai.expect(safeOperationsAfter.count).to.equal(initialNumSafeOperations + 1) + }) }) 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 cb0a733bc..4814ed2ef 100644 --- a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.test.ts +++ b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.test.ts @@ -10,9 +10,9 @@ import EthSafeOperation from './SafeOperation' import * as constants from './constants' import * as fixtures from './testing-utils/fixtures' import { createSafe4337Pack, generateTransferCallData } from './testing-utils/helpers' +import * as utils from './utils' import dotenv from 'dotenv' -import * as utils from './utils' dotenv.config() @@ -497,7 +497,7 @@ describe('Safe4337Pack', () => { }) }) - it('should all to sign a SafeOperation', async () => { + it('should allow to sign a SafeOperation', async () => { const transferUSDC = { to: fixtures.PAYMASTER_TOKEN_ADDRESS, data: generateTransferCallData(fixtures.SAFE_ADDRESS_v1_4_1, 100_000n), @@ -527,6 +527,34 @@ describe('Safe4337Pack', () => { }) }) + it('should allow to sign a SafeOperation using a SafeOperationResponse object from the api to add a signature', async () => { + const safe4337Pack = await createSafe4337Pack({ + options: { + safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 + } + }) + + expect(await safe4337Pack.signSafeOperation(fixtures.SAFE_OPERATION_RESPONSE)).toMatchObject({ + signatures: new Map() + .set( + fixtures.OWNER_1.toLowerCase(), + new protocolKit.EthSafeSignature( + fixtures.OWNER_1, + '0xcb28e74375889e400a4d8aca46b8c59e1cf8825e373c26fa99c2fd7c078080e64fe30eaf1125257bdfe0b358b5caef68aa0420478145f52decc8e74c979d43ab1c', + false + ) + ) + .set( + fixtures.OWNER_2.toLowerCase(), + new protocolKit.EthSafeSignature( + fixtures.OWNER_2, + '0xcb28e74375889e400a4d8aca46b8c59e1cf8825e373c26fa99c2fd7c078080e64fe30eaf1125257bdfe0b358b5caef68aa0420478145f52decc8e74c979d43ab1d', + false + ) + ) + }) + }) + it('should allow to send an UserOperation to a bundler', async () => { const transferUSDC = { to: fixtures.PAYMASTER_TOKEN_ADDRESS, @@ -554,6 +582,35 @@ describe('Safe4337Pack', () => { ]) }) + it('should allow to send a UserOperation to the bundler using a SafeOperationResponse object from the api', async () => { + const safe4337Pack = await createSafe4337Pack({ + options: { + safeAddress: fixtures.SAFE_ADDRESS_v1_4_1 + } + }) + + await safe4337Pack.executeTransaction({ executable: fixtures.SAFE_OPERATION_RESPONSE }) + + expect(sendMock).toHaveBeenCalledWith(constants.RPC_4337_CALLS.SEND_USER_OPERATION, [ + utils.userOperationToHexValues({ + sender: '0xE322e721bCe76cE7FCf3A475f139A9314571ad3D', + nonce: '3', + initCode: '0x', + callData: + '0x7bb37428000000000000000000000000e322e721bce76ce7fcf3a475f139a9314571ad3d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + callGasLimit: 122497n, + verificationGasLimit: 123498n, + preVerificationGas: 50705n, + maxFeePerGas: 105183831060n, + maxPriorityFeePerGas: 1380000000n, + paymasterAndData: '0x', + signature: + '0x000000000000000000000000cb28e74375889e400a4d8aca46b8c59e1cf8825e373c26fa99c2fd7c078080e64fe30eaf1125257bdfe0b358b5caef68aa0420478145f52decc8e74c979d43ab1d' + }), + fixtures.ENTRYPOINTS[0] + ]) + }) + it('should return a UserOperation based on a userOpHash', async () => { const safe4337Pack = await createSafe4337Pack({ options: { diff --git a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts index aa5a78a9a..e99f5f971 100644 --- a/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts +++ b/packages/relay-kit/src/packs/safe-4337/Safe4337Pack.ts @@ -13,7 +13,9 @@ import { OperationType, SafeSignature, UserOperation, - SafeUserOperation + SafeOperationResponse, + SafeOperationConfirmation, + isSafeOperationResponse } from '@safe-global/safe-core-sdk-types' import { getAddModulesLibDeployment, @@ -33,11 +35,18 @@ import { import { DEFAULT_SAFE_VERSION, DEFAULT_SAFE_MODULES_VERSION, - EIP712_SAFE_OPERATION_TYPE, INTERFACES, RPC_4337_CALLS } from './constants' -import { getEip1193Provider, getEip4337BundlerProvider, userOperationToHexValues } from './utils' +import { + addDummySignature, + calculateSafeUserOperationHash, + encodeMultiSendCallData, + getEip1193Provider, + getEip4337BundlerProvider, + signSafeOp, + userOperationToHexValues +} from './utils' import { PimlicoFeeEstimator } from './estimators/PimlicoFeeEstimator' const MAX_ERC20_AMOUNT_TO_APPROVE = @@ -291,7 +300,12 @@ export class Safe4337Pack extends RelayKitBasePack<{ const estimateUserOperationGas = await this.#bundlerClient.send( RPC_4337_CALLS.ESTIMATE_USER_OPERATION_GAS, - [userOperationToHexValues(safeOperation.toUserOperation()), this.#ENTRYPOINT_ADDRESS] + [ + userOperationToHexValues( + addDummySignature(safeOperation.toUserOperation(), await this.protocolKit.getOwners()) + ), + this.#ENTRYPOINT_ADDRESS + ] ) if (estimateUserOperationGas) { @@ -347,7 +361,7 @@ export class Safe4337Pack extends RelayKitBasePack<{ options = {} }: Safe4337CreateTransactionProps): Promise { const safeAddress = await this.protocolKit.getAddress() - const nonce = await this.#getAccountNonce(safeAddress) + const nonce = await this.#getSafeNonceFromEntrypoint(safeAddress) const { amountToApprove, validUntil, validAfter, feeEstimator } = options @@ -376,7 +390,7 @@ export class Safe4337Pack extends RelayKitBasePack<{ ? this.#encodeExecuteUserOpCallData({ to: multiSendAddress, value: '0', - data: this.#encodeMultiSendCallData(transactions), + data: encodeMultiSendCallData(transactions), operation: OperationType.DelegateCall }) : this.#encodeExecuteUserOpCallData(transactions[0]) @@ -404,6 +418,7 @@ export class Safe4337Pack extends RelayKitBasePack<{ } const safeOperation = new EthSafeOperation(userOperation, { + moduleAddress: this.#SAFE_4337_MODULE_ADDRESS, entryPoint: this.#ENTRYPOINT_ADDRESS, validUntil, validAfter @@ -415,17 +430,67 @@ export class Safe4337Pack extends RelayKitBasePack<{ }) } + /** + * Converts a SafeOperationResponse to an EthSafeOperation. + * + * @param {SafeOperationResponse} safeOperationResponse - The SafeOperationResponse to convert to EthSafeOperation + * @returns {EthSafeOperation} - The EthSafeOperation object + */ + #toSafeOperation(safeOperationResponse: SafeOperationResponse): EthSafeOperation { + const { validUntil, validAfter, userOperation } = safeOperationResponse + + const safeOperation = new EthSafeOperation( + { + sender: userOperation?.sender || '0x', + nonce: userOperation?.nonce?.toString() || '0', + initCode: userOperation?.initCode || '', + callData: userOperation?.callData || '', + callGasLimit: BigInt(userOperation?.callGasLimit || 0n), + verificationGasLimit: BigInt(userOperation?.verificationGasLimit || 0), + preVerificationGas: BigInt(userOperation?.preVerificationGas || 0), + maxFeePerGas: BigInt(userOperation?.maxFeePerGas || 0), + maxPriorityFeePerGas: BigInt(userOperation?.maxPriorityFeePerGas || 0), + paymasterAndData: userOperation?.paymasterData || '0x', + signature: userOperation?.signature || '0x' + }, + { + moduleAddress: this.#SAFE_4337_MODULE_ADDRESS, + entryPoint: userOperation?.entryPoint || this.#ENTRYPOINT_ADDRESS, + validAfter: validAfter ? new Date(validAfter).getTime() : undefined, + validUntil: validUntil ? new Date(validUntil).getTime() : undefined + } + ) + + if (safeOperationResponse.confirmations) { + safeOperationResponse.confirmations.forEach((confirmation: SafeOperationConfirmation) => { + safeOperation.addSignature(new EthSafeSignature(confirmation.owner, confirmation.signature)) + }) + } + + return safeOperation + } + /** * Signs a safe operation. * - * @param {EthSafeOperation} safeOperation - The SafeOperation to sign. + * @param {EthSafeOperation | SafeOperationResponse} safeOperation - The SafeOperation to sign. It can be: + * - A response from the API (Tx Service) + * - An instance of EthSafeOperation * @param {SigningMethod} signingMethod - The signing method to use. * @return {Promise} The Promise object will resolve to the signed SafeOperation. */ async signSafeOperation( - safeOperation: EthSafeOperation, + safeOperation: EthSafeOperation | SafeOperationResponse, signingMethod: SigningMethod = SigningMethod.ETH_SIGN_TYPED_DATA_V4 ): Promise { + let safeOp: EthSafeOperation + + if (isSafeOperationResponse(safeOperation)) { + safeOp = this.#toSafeOperation(safeOperation) + } else { + safeOp = safeOperation + } + const owners = await this.protocolKit.getOwners() const signerAddress = await this.protocolKit.getSafeProvider().getSignerAddress() if (!signerAddress) { @@ -441,27 +506,35 @@ export class Safe4337Pack extends RelayKitBasePack<{ } let signature: SafeSignature - if ( signingMethod === SigningMethod.ETH_SIGN_TYPED_DATA_V4 || signingMethod === SigningMethod.ETH_SIGN_TYPED_DATA_V3 || signingMethod === SigningMethod.ETH_SIGN_TYPED_DATA ) { - signature = await this.#signTypedData(safeOperation.data) + signature = await signSafeOp( + safeOp.data, + this.protocolKit.getSafeProvider(), + this.#SAFE_4337_MODULE_ADDRESS + ) } else { const chainId = await this.protocolKit.getSafeProvider().getChainId() - const safeOpHash = this.#getSafeUserOperationHash(safeOperation.data, chainId) + const safeOpHash = calculateSafeUserOperationHash( + safeOp.data, + chainId, + this.#SAFE_4337_MODULE_ADDRESS + ) signature = await this.protocolKit.signHash(safeOpHash) } - const signedSafeOperation = new EthSafeOperation(safeOperation.toUserOperation(), { + const signedSafeOperation = new EthSafeOperation(safeOp.toUserOperation(), { + moduleAddress: this.#SAFE_4337_MODULE_ADDRESS, entryPoint: this.#ENTRYPOINT_ADDRESS, - validUntil: safeOperation.data.validUntil, - validAfter: safeOperation.data.validAfter + validUntil: safeOp.data.validUntil, + validAfter: safeOp.data.validAfter }) - signedSafeOperation.signatures.forEach((signature: SafeSignature) => { + safeOp.signatures.forEach((signature: SafeSignature) => { signedSafeOperation.addSignature(signature) }) @@ -473,15 +546,27 @@ export class Safe4337Pack extends RelayKitBasePack<{ /** * Executes the relay transaction. * - * @param {EthSafeOperation} safeOperation - The SafeOperation to execute. + * @param {Safe4337ExecutableProps} props - The parameters for the transaction execution. + * @param {EthSafeOperation | SafeOperationResponse} props.executable - The SafeOperation to execute. It can be: + * - A response from the API (Tx Service) + * - An instance of EthSafeOperation * @return {Promise} The user operation hash. */ - async executeTransaction({ - executable: safeOperation - }: Safe4337ExecutableProps): Promise { + async executeTransaction({ executable }: Safe4337ExecutableProps): Promise { + let safeOperation: EthSafeOperation + + if (isSafeOperationResponse(executable)) { + safeOperation = this.#toSafeOperation(executable) + } else { + safeOperation = executable + } + const userOperation = safeOperation.toUserOperation() - return this.#sendUserOperation(userOperation) + return this.#bundlerClient.send(RPC_4337_CALLS.SEND_USER_OPERATION, [ + userOperationToHexValues(userOperation), + this.#ENTRYPOINT_ADDRESS + ]) } /** @@ -491,12 +576,7 @@ export class Safe4337Pack extends RelayKitBasePack<{ * @returns {UserOperation} - null in case the UserOperation is not yet included in a block, or a full UserOperation, with the addition of entryPoint, blockNumber, blockHash and transactionHash */ async getUserOperationByHash(userOpHash: string): Promise { - const userOperation = await this.#bundlerClient.send( - RPC_4337_CALLS.GET_USER_OPERATION_BY_HASH, - [userOpHash] - ) - - return userOperation + return this.#bundlerClient.send(RPC_4337_CALLS.GET_USER_OPERATION_BY_HASH, [userOpHash]) } /** @@ -506,12 +586,7 @@ export class Safe4337Pack extends RelayKitBasePack<{ * @returns {UserOperationReceipt} - null in case the UserOperation is not yet included in a block, or UserOperationReceipt object */ async getUserOperationReceipt(userOpHash: string): Promise { - const userOperationReceipt = await this.#bundlerClient.send( - RPC_4337_CALLS.GET_USER_OPERATION_RECEIPT, - [userOpHash] - ) - - return userOperationReceipt + return this.#bundlerClient.send(RPC_4337_CALLS.GET_USER_OPERATION_RECEIPT, [userOpHash]) } /** @@ -521,12 +596,7 @@ export class Safe4337Pack extends RelayKitBasePack<{ * @returns {string[]} - The supported entry points. */ async getSupportedEntryPoints(): Promise { - const supportedEntryPoints = await this.#bundlerClient.send( - RPC_4337_CALLS.SUPPORTED_ENTRY_POINTS, - [] - ) - - return supportedEntryPoints + return this.#bundlerClient.send(RPC_4337_CALLS.SUPPORTED_ENTRY_POINTS, []) } /** @@ -535,78 +605,16 @@ export class Safe4337Pack extends RelayKitBasePack<{ * @returns {string} - The chain id. */ async getChainId(): Promise { - const chainId = await this.#bundlerClient.send(RPC_4337_CALLS.CHAIN_ID, []) - - return chainId - } - - /** - * Gets the safe user operation hash. - * - * @param {SafeUserOperation} safeUserOperation - The SafeUserOperation. - * @param {bigint} chainId - The chain id. - * @return {string} The hash of the safe operation. - */ - #getSafeUserOperationHash(safeUserOperation: SafeUserOperation, chainId: bigint): string { - return ethers.TypedDataEncoder.hash( - { - chainId, - verifyingContract: this.#SAFE_4337_MODULE_ADDRESS - }, - EIP712_SAFE_OPERATION_TYPE, - safeUserOperation - ) - } - - /** - * Send the UserOperation to the bundler. - * - * @param {UserOperation} userOpWithSignature - The signed UserOperation to send to the bundler. - * @return {Promise} The hash. - */ - async #sendUserOperation(userOpWithSignature: UserOperation): Promise { - return await this.#bundlerClient.send(RPC_4337_CALLS.SEND_USER_OPERATION, [ - userOperationToHexValues(userOpWithSignature), - this.#ENTRYPOINT_ADDRESS - ]) - } - - /** - * Signs typed data. - * - * @param {SafeUserOperation} safeUserOperation - Safe user operation to sign. - * @return {Promise} The SafeSignature object containing the data and the signatures. - */ - async #signTypedData(safeUserOperation: SafeUserOperation): Promise { - const safeProvider = this.protocolKit.getSafeProvider() - const signer = (await safeProvider.getExternalSigner()) as ethers.Signer - const chainId = await safeProvider.getChainId() - const signerAddress = await signer.getAddress() - const signature = await signer.signTypedData( - { - chainId, - verifyingContract: this.#SAFE_4337_MODULE_ADDRESS - }, - EIP712_SAFE_OPERATION_TYPE, - { - ...safeUserOperation, - nonce: ethers.toBeHex(safeUserOperation.nonce), - validAfter: ethers.toBeHex(safeUserOperation.validAfter), - validUntil: ethers.toBeHex(safeUserOperation.validUntil), - maxFeePerGas: ethers.toBeHex(safeUserOperation.maxFeePerGas), - maxPriorityFeePerGas: ethers.toBeHex(safeUserOperation.maxPriorityFeePerGas) - } - ) - return new EthSafeSignature(signerAddress, signature) + return this.#bundlerClient.send(RPC_4337_CALLS.CHAIN_ID, []) } /** * Gets account nonce from the bundler. * - * @param {string} sender - Account address for which the nonce is to be fetched. + * @param {string} safeAddress - Account address for which the nonce is to be fetched. * @returns {Promise} The Promise object will resolve to the account nonce. */ - async #getAccountNonce(sender: string): Promise { + async #getSafeNonceFromEntrypoint(safeAddress: string): Promise { const abi = [ { inputs: [ @@ -622,7 +630,7 @@ export class Safe4337Pack extends RelayKitBasePack<{ const contract = new ethers.Contract(this.#ENTRYPOINT_ADDRESS || '0x', abi, this.#publicClient) - const newNonce = await contract.getNonce(sender, BigInt(0)) + const newNonce = await contract.getNonce(safeAddress, BigInt(0)) return newNonce.toString() } @@ -641,18 +649,4 @@ export class Safe4337Pack extends RelayKitBasePack<{ transaction.operation || OperationType.Call ]) } - - /** - * Encodes multi-send data from transactions batch. - * - * @param {MetaTransactionData[]} transactions - an array of transaction to to be encoded. - * @return {string} The encoded data string. - */ - #encodeMultiSendCallData(transactions: MetaTransactionData[]): string { - return INTERFACES.encodeFunctionData('multiSend', [ - encodeMultiSendData( - transactions.map((tx) => ({ ...tx, operation: tx.operation ?? OperationType.Call })) - ) - ]) - } } 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 048b61527..b39077514 100644 --- a/packages/relay-kit/src/packs/safe-4337/SafeOperation.test.ts +++ b/packages/relay-kit/src/packs/safe-4337/SafeOperation.test.ts @@ -6,6 +6,7 @@ import * as fixtures from './testing-utils/fixtures' describe('SafeOperation', () => { it('should create a SafeOperation from an UserOperation', () => { const safeOperation = new EthSafeOperation(fixtures.USER_OPERATION, { + moduleAddress: fixtures.MODULE_ADDRESS, entryPoint: fixtures.ENTRYPOINTS[0] }) @@ -30,6 +31,7 @@ describe('SafeOperation', () => { it('should add and retrieve signatures', () => { const safeOperation = new EthSafeOperation(fixtures.USER_OPERATION, { + moduleAddress: fixtures.MODULE_ADDRESS, entryPoint: fixtures.ENTRYPOINTS[0] }) @@ -45,6 +47,7 @@ describe('SafeOperation', () => { it('should encode the signatures', () => { const safeOperation = new EthSafeOperation(fixtures.USER_OPERATION, { + moduleAddress: fixtures.MODULE_ADDRESS, entryPoint: fixtures.ENTRYPOINTS[0] }) @@ -56,6 +59,7 @@ describe('SafeOperation', () => { it('should add estimations', () => { const safeOperation = new EthSafeOperation(fixtures.USER_OPERATION, { + moduleAddress: fixtures.MODULE_ADDRESS, entryPoint: fixtures.ENTRYPOINTS[0] }) @@ -84,6 +88,7 @@ describe('SafeOperation', () => { it('should convert to UserOperation', () => { const safeOperation = new EthSafeOperation(fixtures.USER_OPERATION, { + moduleAddress: fixtures.MODULE_ADDRESS, 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 2fa0db5d9..e54cf328a 100644 --- a/packages/relay-kit/src/packs/safe-4337/SafeOperation.ts +++ b/packages/relay-kit/src/packs/safe-4337/SafeOperation.ts @@ -8,16 +8,23 @@ import { } from '@safe-global/safe-core-sdk-types' import { buildSignatureBytes } from '@safe-global/protocol-kit' -type SafeOperationOptions = { entryPoint: string; validAfter?: number; validUntil?: number } +type SafeOperationOptions = { + moduleAddress: string + entryPoint: string + validAfter?: number + validUntil?: number +} class EthSafeOperation implements SafeOperation { data: SafeUserOperation signatures: Map = new Map() + moduleAddress: string constructor( userOperation: UserOperation, - { entryPoint, validAfter, validUntil }: SafeOperationOptions + { entryPoint, validAfter, validUntil, moduleAddress }: SafeOperationOptions ) { + this.moduleAddress = moduleAddress this.data = { safe: userOperation.sender, nonce: BigInt(userOperation.nonce), diff --git a/packages/relay-kit/src/packs/safe-4337/testing-utils/fixtures.ts b/packages/relay-kit/src/packs/safe-4337/testing-utils/fixtures.ts index 88fdf1e69..aa493510c 100644 --- a/packages/relay-kit/src/packs/safe-4337/testing-utils/fixtures.ts +++ b/packages/relay-kit/src/packs/safe-4337/testing-utils/fixtures.ts @@ -9,7 +9,7 @@ export const SAFE_ADDRESS_4337_FALLBACKHANDLER_NOT_ENABLED = export const PAYMASTER_ADDRESS = '0x0000000000325602a77416A16136FDafd04b299f' export const PAYMASTER_TOKEN_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' export const CHAIN_ID = '0xaa36a7' - +export const MODULE_ADDRESS = '0xa581c4A4DB7175302464fF3C06380BC3270b4037' export const RPC_URL = 'https://sepolia.gateway.tenderly.co' export const BUNDLER_URL = 'https://bundler.url' export const PAYMASTER_URL = 'https://paymaster.url' @@ -94,6 +94,46 @@ export const GAS_ESTIMATION = { callGasLimit: '0x186A0' } +export const SAFE_OPERATION_RESPONSE = { + created: '2024-05-31T10:12:21.169031Z', + modified: '2024-05-31T10:12:21.169031Z', + safeOperationHash: '0x5a62b1d61f8fca5f766e9456523bb42765d318058b5f235f967ffe3c2af8b1d7', + validAfter: null, + validUntil: null, + moduleAddress: '0xa581c4A4DB7175302464fF3C06380BC3270b4037', + confirmations: [ + { + created: '2024-05-31T10:12:21.184585Z', + modified: '2024-05-31T10:12:21.184585Z', + owner: '0x3059EfD1BCe33be41eeEfd5fb6D520d7fEd54E43', + signature: + '0xcb28e74375889e400a4d8aca46b8c59e1cf8825e373c26fa99c2fd7c078080e64fe30eaf1125257bdfe0b358b5caef68aa0420478145f52decc8e74c979d43ab1d', + signatureType: 'EOA' + } + ], + preparedSignature: + '0xcb28e74375889e400a4d8aca46b8c59e1cf8825e373c26fa99c2fd7c078080e64fe30eaf1125257bdfe0b358b5caef68aa0420478145f52decc8e74c979d43ab1c', + userOperation: { + ethereumTxHash: null, + sender: '0xE322e721bCe76cE7FCf3A475f139A9314571ad3D', + userOperationHash: '0x5d23b7d96a718582601183b1849a4c76b2a13d3787f15074d62a0b6e4a3f76a1', + nonce: 3, + initCode: '0x', + callData: + '0x7bb37428000000000000000000000000e322e721bce76ce7fcf3a475f139a9314571ad3d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + callGasLimit: 122497, + verificationGasLimit: 123498, + preVerificationGas: 50705, + maxFeePerGas: 105183831060, + maxPriorityFeePerGas: 1380000000, + paymaster: null, + paymasterData: null, + signature: + '0x54158da2d357241ee1c5c8fca9c4e1bfa6b92a60bd0ed1bea56f4092b008435153d6264a8a8c00925383ecaeaf9d839a2dc1ff006703c65b7f05d0ce8cdd57ab1b', + entryPoint: '0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789' + } +} + export const SPONSORED_GAS_ESTIMATION = { paymasterAndData: '0x1405B3659a11a16459fc27Fa1925b60388C38Ce1', ...GAS_ESTIMATION diff --git a/packages/relay-kit/src/packs/safe-4337/types.ts b/packages/relay-kit/src/packs/safe-4337/types.ts index 1b78b3348..225e9b205 100644 --- a/packages/relay-kit/src/packs/safe-4337/types.ts +++ b/packages/relay-kit/src/packs/safe-4337/types.ts @@ -2,6 +2,7 @@ import Safe, { SafeProviderConfig } from '@safe-global/protocol-kit' import { EstimateGasData, MetaTransactionData, + SafeOperationResponse, SafeVersion, UserOperation } from '@safe-global/safe-core-sdk-types' @@ -64,7 +65,7 @@ export type Safe4337CreateTransactionProps = { } export type Safe4337ExecutableProps = { - executable: EthSafeOperation + executable: EthSafeOperation | SafeOperationResponse } export type EstimateSponsoredGasData = { diff --git a/packages/relay-kit/src/packs/safe-4337/utils.ts b/packages/relay-kit/src/packs/safe-4337/utils.ts index 1e2db76c2..959865ac4 100644 --- a/packages/relay-kit/src/packs/safe-4337/utils.ts +++ b/packages/relay-kit/src/packs/safe-4337/utils.ts @@ -1,5 +1,18 @@ +import { + SafeUserOperation, + OperationType, + MetaTransactionData, + SafeSignature, + UserOperation +} from '@safe-global/safe-core-sdk-types' +import { + EthSafeSignature, + SafeProvider, + encodeMultiSendData, + buildSignatureBytes +} from '@safe-global/protocol-kit' import { ethers } from 'ethers' -import { UserOperation } from '@safe-global/safe-core-sdk-types' +import { EIP712_SAFE_OPERATION_TYPE, INTERFACES } from './constants' /** * Gets the EIP-4337 bundler provider. @@ -29,6 +42,78 @@ export function getEip1193Provider(rpcUrl: string): ethers.JsonRpcProvider { return provider } +/** + * Signs typed data. + * + * @param {SafeUserOperation} safeUserOperation - Safe user operation to sign. + * @param {SafeProvider} safeProvider - Safe provider. + * @param {string} safe4337ModuleAddress - Safe 4337 module address. + * @return {Promise} The SafeSignature object containing the data and the signatures. + */ +export async function signSafeOp( + safeUserOperation: SafeUserOperation, + safeProvider: SafeProvider, + safe4337ModuleAddress: string +): Promise { + const signer = (await safeProvider.getExternalSigner()) as ethers.Signer + const chainId = await safeProvider.getChainId() + const signerAddress = await signer.getAddress() + const signature = await signer.signTypedData( + { + chainId, + verifyingContract: safe4337ModuleAddress + }, + EIP712_SAFE_OPERATION_TYPE, + { + ...safeUserOperation, + nonce: ethers.toBeHex(safeUserOperation.nonce), + validAfter: ethers.toBeHex(safeUserOperation.validAfter), + validUntil: ethers.toBeHex(safeUserOperation.validUntil), + maxFeePerGas: ethers.toBeHex(safeUserOperation.maxFeePerGas), + maxPriorityFeePerGas: ethers.toBeHex(safeUserOperation.maxPriorityFeePerGas) + } + ) + + return new EthSafeSignature(signerAddress, signature) +} + +/** + * Encodes multi-send data from transactions batch. + * + * @param {MetaTransactionData[]} transactions - an array of transaction to to be encoded. + * @return {string} The encoded data string. + */ +export function encodeMultiSendCallData(transactions: MetaTransactionData[]): string { + return INTERFACES.encodeFunctionData('multiSend', [ + encodeMultiSendData( + transactions.map((tx) => ({ ...tx, operation: tx.operation ?? OperationType.Call })) + ) + ]) +} + +/** + * Gets the safe user operation hash. + * + * @param {SafeUserOperation} safeUserOperation - The SafeUserOperation. + * @param {bigint} chainId - The chain id. + * @param {string} safe4337ModuleAddress - The Safe 4337 module address. + * @return {string} The hash of the safe operation. + */ +export function calculateSafeUserOperationHash( + safeUserOperation: SafeUserOperation, + chainId: bigint, + safe4337ModuleAddress: string +): string { + return ethers.TypedDataEncoder.hash( + { + chainId, + verifyingContract: safe4337ModuleAddress + }, + EIP712_SAFE_OPERATION_TYPE, + safeUserOperation + ) +} + /** * Converts various bigint values from a UserOperation to their hexadecimal representation. * @@ -48,3 +133,30 @@ export function userOperationToHexValues(userOperation: UserOperation) { return userOperationWithHexValues } + +/** + * This method creates a dummy signature for the SafeOperation based the owners. + * This is useful for gas estimations + * @param userOperation - The user operation + * @param safeOwners - The safe owner addresses + * @returns The user operation with the dummy signature + */ +export function addDummySignature( + userOperation: UserOperation, + safeOwners: string[] +): UserOperation { + const signatures = [] + + for (const owner of safeOwners) { + const dummySignature = `0x000000000000000000000000${owner.slice(2)}000000000000000000000000000000000000000000000000000000000000000001` + signatures.push(new EthSafeSignature(owner, dummySignature)) + } + + return { + ...userOperation, + signature: ethers.solidityPacked( + ['uint48', 'uint48', 'bytes'], + [0, 0, buildSignatureBytes(signatures)] + ) + } +} diff --git a/packages/safe-core-sdk-types/src/types.ts b/packages/safe-core-sdk-types/src/types.ts index 919d8e165..998293f10 100644 --- a/packages/safe-core-sdk-types/src/types.ts +++ b/packages/safe-core-sdk-types/src/types.ts @@ -295,6 +295,7 @@ export type EstimateGasData = { } export interface SafeOperation { + readonly moduleAddress: string readonly data: SafeUserOperation readonly signatures: Map getSignature(signer: string): SafeSignature | undefined @@ -303,3 +304,53 @@ export interface SafeOperation { addEstimations(estimations: EstimateGasData): void toUserOperation(): UserOperation } + +export const isSafeOperation = (response: unknown): response is SafeOperation => { + const safeOperation = response as SafeOperation + + return 'data' in safeOperation && 'signatures' in safeOperation +} + +export type SafeOperationConfirmation = { + readonly created: string + readonly modified: string + readonly owner: string + readonly signature: string + readonly signatureType: string +} + +export type UserOperationResponse = { + readonly ethereumTxHash: null | string + readonly sender: string + readonly userOperationHash: string + readonly nonce: number + readonly initCode: null | string + readonly callData: null | string + readonly callGasLimit: 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 +} + +export type SafeOperationResponse = { + readonly created: string + readonly modified: string + readonly safeOperationHash: string + readonly validAfter: null | string + readonly validUntil: null | string + readonly moduleAddress: string + readonly confirmations?: Array + readonly preparedSignature?: string + readonly userOperation?: UserOperationResponse +} + +export const isSafeOperationResponse = (response: unknown): response is SafeOperationResponse => { + const safeOperationResponse = response as SafeOperationResponse + + return 'userOperation' in safeOperationResponse && 'safeOperationHash' in safeOperationResponse +} diff --git a/playground/config/run.ts b/playground/config/run.ts index d295ee942..5e2a1a150 100644 --- a/playground/config/run.ts +++ b/playground/config/run.ts @@ -16,6 +16,7 @@ const playgroundApiKitPaths = { 'execute-transaction': 'api-kit/execute-transaction' } const playgroundRelayKitPaths = { + 'api-kit-interoperability': 'relay-kit/api-kit-interoperability', 'relay-paid-transaction': 'relay-kit/paid-transaction', 'relay-sponsored-transaction': 'relay-kit/sponsored-transaction', 'usdc-transfer-4337': 'relay-kit/usdc-transfer-4337', diff --git a/playground/relay-kit/api-kit-interoperability.ts b/playground/relay-kit/api-kit-interoperability.ts new file mode 100644 index 000000000..9e75a6263 --- /dev/null +++ b/playground/relay-kit/api-kit-interoperability.ts @@ -0,0 +1,97 @@ +import SafeApiKit from '@safe-global/api-kit' +import { Safe4337Pack } from '@safe-global/relay-kit' +import { sortResultsByCreatedDateDesc, waitForOperationToFinish } from '../utils' + +// Variables +const OWNER_1_PRIVATE_KEY = '' +const OWNER_2_PRIVATE_KEY = '' +const PIMLICO_API_KEY = '' +const SAFE_ADDRESS = '' // Safe 2/N +const CHAIN_NAME = 'sepolia' + +// Constants +const BUNDLER_URL = `https://api.pimlico.io/v1/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` +const PAYMASTER_URL = `https://api.pimlico.io/v2/${CHAIN_NAME}/rpc?apikey=${PIMLICO_API_KEY}` +const RPC_URL = 'https://sepolia.gateway.tenderly.co' +const PAYMASTER_ADDRESS = '0x0000000000325602a77416A16136FDafd04b299f' // SEPOLIA + +const CHAIN_ID = 11155111n + +async function main() { + const apiKit = new SafeApiKit({ + chainId: CHAIN_ID + }) + + let safe4337Pack = await Safe4337Pack.init({ + provider: RPC_URL, + signer: OWNER_1_PRIVATE_KEY, + rpcUrl: RPC_URL, + bundlerUrl: BUNDLER_URL, + options: { + owners: [OWNER_1_PRIVATE_KEY, OWNER_2_PRIVATE_KEY], + safeAddress: SAFE_ADDRESS + } + }) + + const safeOperation = await safe4337Pack.createTransaction({ + transactions: [ + { + to: SAFE_ADDRESS, + value: '0x0', + data: '0x' + } + ] + }) + + let signedSafeOperation = await safe4337Pack.signSafeOperation(safeOperation) + + console.log('SafeOperation signature 1', signedSafeOperation) + await apiKit.addSafeOperation(signedSafeOperation) + + let safeOperations = await apiKit.getSafeOperationsByAddress({ + safeAddress: SAFE_ADDRESS, + ordering: '-created' + }) + + if (safeOperations.results.length >= 0) { + safe4337Pack = await Safe4337Pack.init({ + provider: RPC_URL, + signer: OWNER_2_PRIVATE_KEY, + rpcUrl: RPC_URL, + bundlerUrl: BUNDLER_URL, + paymasterOptions: { + isSponsored: true, + paymasterAddress: PAYMASTER_ADDRESS, + paymasterUrl: PAYMASTER_URL + }, + options: { + safeAddress: SAFE_ADDRESS + } + }) + + signedSafeOperation = await safe4337Pack.signSafeOperation( + sortResultsByCreatedDateDesc(safeOperations).results[0] + ) + + console.log('SafeOperation signature 2', signedSafeOperation) + + // TODO. This should be the place to confirm the safe operation but the api endpoint is not available yet + // Update this once the new endpoint is released + await apiKit.addSafeOperation(signedSafeOperation) + + safeOperations = await apiKit.getSafeOperationsByAddress({ + safeAddress: SAFE_ADDRESS, + ordering: '-created' + }) + + console.log('SafeOperationList', safeOperations) + + const userOperationHash = await safe4337Pack.executeTransaction({ + executable: sortResultsByCreatedDateDesc(safeOperations).results[0] + }) + + await waitForOperationToFinish(userOperationHash, CHAIN_NAME, safe4337Pack) + } +} + +main() diff --git a/playground/relay-kit/usdc-transfer-4337-counterfactual.ts b/playground/relay-kit/usdc-transfer-4337-counterfactual.ts index 2885078d3..368ebbba2 100644 --- a/playground/relay-kit/usdc-transfer-4337-counterfactual.ts +++ b/playground/relay-kit/usdc-transfer-4337-counterfactual.ts @@ -1,5 +1,6 @@ -import { ethers, AbstractSigner } from 'ethers' +import { ethers } from 'ethers' import { Safe4337Pack } from '@safe-global/relay-kit' +import { waitForOperationToFinish, transfer, generateTransferCallData } from '../utils' // Safe owner PK const PRIVATE_KEY = '' @@ -111,40 +112,7 @@ async function main() { executable: signedSafeOperation }) - console.log(`https://jiffyscan.xyz/userOpHash/${userOperationHash}?network=${CHAIN_NAME}`) - - let userOperationReceipt = null - while (!userOperationReceipt) { - await new Promise((resolve) => setTimeout(resolve, 2000)) - userOperationReceipt = await safe4337Pack.getUserOperationReceipt(userOperationHash) - } - - console.group('User Operation Receipt and hash') - console.log('User Operation Receipt', userOperationReceipt) - console.log( - 'User Operation By Hash', - await safe4337Pack.getUserOperationByHash(userOperationHash) - ) - console.groupEnd() + await waitForOperationToFinish(userOperationHash, CHAIN_NAME, safe4337Pack) } main() - -async function transfer(signer: AbstractSigner, tokenAddress: string, to: string, amount: bigint) { - const transferEC20 = { - to: tokenAddress, - data: generateTransferCallData(to, amount), - value: '0' - } - - const transactionResponse = await signer.sendTransaction(transferEC20) - - return await transactionResponse.wait() -} - -const generateTransferCallData = (to: string, value: bigint) => { - const functionAbi = 'function transfer(address _to, uint256 _value) returns (bool)' - const iface = new ethers.Interface([functionAbi]) - - return iface.encodeFunctionData('transfer', [to, value]) -} diff --git a/playground/relay-kit/usdc-transfer-4337-erc20-counterfactual.ts b/playground/relay-kit/usdc-transfer-4337-erc20-counterfactual.ts index 4bcdf7911..ebb8ae2b4 100644 --- a/playground/relay-kit/usdc-transfer-4337-erc20-counterfactual.ts +++ b/playground/relay-kit/usdc-transfer-4337-erc20-counterfactual.ts @@ -1,5 +1,5 @@ -import { ethers } from 'ethers' import { Safe4337Pack } from '@safe-global/relay-kit' +import { waitForOperationToFinish, transfer, generateTransferCallData } from '../utils' // Safe owner PK const PRIVATE_KEY = '' @@ -102,45 +102,7 @@ async function main() { executable: signedSafeOperation }) - console.log(`https://jiffyscan.xyz/userOpHash/${userOperationHash}?network=${CHAIN_NAME}`) - - let userOperationReceipt = null - while (!userOperationReceipt) { - await new Promise((resolve) => setTimeout(resolve, 2000)) - userOperationReceipt = await safe4337Pack.getUserOperationReceipt(userOperationHash) - } - - console.group('User Operation Receipt and hash') - console.log('User Operation Receipt', userOperationReceipt) - console.log( - 'User Operation By Hash', - await safe4337Pack.getUserOperationByHash(userOperationHash) - ) - console.groupEnd() + await waitForOperationToFinish(userOperationHash, CHAIN_NAME, safe4337Pack) } main() - -const generateTransferCallData = (to: string, value: bigint) => { - const functionAbi = 'function transfer(address _to, uint256 _value) returns (bool)' - const iface = new ethers.Interface([functionAbi]) - - return iface.encodeFunctionData('transfer', [to, value]) -} - -async function transfer( - signer: ethers.AbstractSigner, - tokenAddress: string, - to: string, - amount: bigint -) { - const transferEC20 = { - to: tokenAddress, - data: generateTransferCallData(to, amount), - value: '0' - } - - const transactionResponse = await signer.sendTransaction(transferEC20) - - return await transactionResponse.wait() -} diff --git a/playground/relay-kit/usdc-transfer-4337-erc20.ts b/playground/relay-kit/usdc-transfer-4337-erc20.ts index c1b9451a3..69dccc29e 100644 --- a/playground/relay-kit/usdc-transfer-4337-erc20.ts +++ b/playground/relay-kit/usdc-transfer-4337-erc20.ts @@ -1,5 +1,5 @@ -import { ethers } from 'ethers' import { Safe4337Pack } from '@safe-global/relay-kit' +import { waitForOperationToFinish, transfer, generateTransferCallData } from '../utils' // Safe owner PK const PRIVATE_KEY = '' @@ -96,45 +96,7 @@ async function main() { executable: signedSafeOperation }) - console.log(`https://jiffyscan.xyz/userOpHash/${userOperationHash}?network=${CHAIN_NAME}`) - - let userOperationReceipt = null - while (!userOperationReceipt) { - await new Promise((resolve) => setTimeout(resolve, 2000)) - userOperationReceipt = await safe4337Pack.getUserOperationReceipt(userOperationHash) - } - - console.group('User Operation Receipt and hash') - console.log('User Operation Receipt', userOperationReceipt) - console.log( - 'User Operation By Hash', - await safe4337Pack.getUserOperationByHash(userOperationHash) - ) - console.groupEnd() + await waitForOperationToFinish(userOperationHash, CHAIN_NAME, safe4337Pack) } main() - -const generateTransferCallData = (to: string, value: bigint) => { - const functionAbi = 'function transfer(address _to, uint256 _value) returns (bool)' - const iface = new ethers.Interface([functionAbi]) - - return iface.encodeFunctionData('transfer', [to, value]) -} - -async function transfer( - signer: ethers.AbstractSigner, - tokenAddress: string, - to: string, - amount: bigint -) { - const transferEC20 = { - to: tokenAddress, - data: generateTransferCallData(to, amount), - value: '0' - } - - const transactionResponse = await signer.sendTransaction(transferEC20) - - return await transactionResponse.wait() -} diff --git a/playground/relay-kit/usdc-transfer-4337-sponsored-counterfactual.ts b/playground/relay-kit/usdc-transfer-4337-sponsored-counterfactual.ts index 0c43fef1b..056379a13 100644 --- a/playground/relay-kit/usdc-transfer-4337-sponsored-counterfactual.ts +++ b/playground/relay-kit/usdc-transfer-4337-sponsored-counterfactual.ts @@ -1,5 +1,5 @@ -import { ethers } from 'ethers' import { Safe4337Pack } from '@safe-global/relay-kit' +import { waitForOperationToFinish, transfer, generateTransferCallData } from '../utils' // Safe owner PK const PRIVATE_KEY = '' @@ -109,45 +109,7 @@ async function main() { executable: signedSafeOperation }) - console.log(`https://jiffyscan.xyz/userOpHash/${userOperationHash}?network=${CHAIN_NAME}`) - - let userOperationReceipt = null - while (!userOperationReceipt) { - await new Promise((resolve) => setTimeout(resolve, 2000)) - userOperationReceipt = await safe4337Pack.getUserOperationReceipt(userOperationHash) - } - - console.group('User Operation Receipt and hash') - console.log('User Operation Receipt', userOperationReceipt) - console.log( - 'User Operation By Hash', - await safe4337Pack.getUserOperationByHash(userOperationHash) - ) - console.groupEnd() + await waitForOperationToFinish(userOperationHash, CHAIN_NAME, safe4337Pack) } main() - -const generateTransferCallData = (to: string, value: bigint) => { - const functionAbi = 'function transfer(address _to, uint256 _value) returns (bool)' - const iface = new ethers.Interface([functionAbi]) - - return iface.encodeFunctionData('transfer', [to, value]) -} - -async function transfer( - signer: ethers.AbstractSigner, - tokenAddress: string, - to: string, - amount: bigint -) { - const transferEC20 = { - to: tokenAddress, - data: generateTransferCallData(to, amount), - value: '0' - } - - const transactionResponse = await signer.sendTransaction(transferEC20) - - return await transactionResponse.wait() -} diff --git a/playground/relay-kit/usdc-transfer-4337-sponsored.ts b/playground/relay-kit/usdc-transfer-4337-sponsored.ts index 23ff63917..d28127410 100644 --- a/playground/relay-kit/usdc-transfer-4337-sponsored.ts +++ b/playground/relay-kit/usdc-transfer-4337-sponsored.ts @@ -1,5 +1,5 @@ -import { ethers } from 'ethers' import { Safe4337Pack } from '@safe-global/relay-kit' +import { waitForOperationToFinish, generateTransferCallData } from '../utils' // Safe owner PK const PRIVATE_KEY = '' @@ -90,28 +90,7 @@ async function main() { executable: signedSafeOperation }) - console.log(`https://jiffyscan.xyz/userOpHash/${userOperationHash}?network=${CHAIN_NAME}`) - - let userOperationReceipt = null - while (!userOperationReceipt) { - await new Promise((resolve) => setTimeout(resolve, 2000)) - userOperationReceipt = await safe4337Pack.getUserOperationReceipt(userOperationHash) - } - - console.group('User Operation Receipt and hash') - console.log('User Operation Receipt', userOperationReceipt) - console.log( - 'User Operation By Hash', - await safe4337Pack.getUserOperationByHash(userOperationHash) - ) - console.groupEnd() + await waitForOperationToFinish(userOperationHash, CHAIN_NAME, safe4337Pack) } main() - -const generateTransferCallData = (to: string, value: bigint) => { - const functionAbi = 'function transfer(address _to, uint256 _value) returns (bool)' - const iface = new ethers.Interface([functionAbi]) - - return iface.encodeFunctionData('transfer', [to, value]) -} diff --git a/playground/relay-kit/usdc-transfer-4337.ts b/playground/relay-kit/usdc-transfer-4337.ts index 3f25529ab..1d621ae74 100644 --- a/playground/relay-kit/usdc-transfer-4337.ts +++ b/playground/relay-kit/usdc-transfer-4337.ts @@ -1,5 +1,5 @@ -import { ethers } from 'ethers' import { Safe4337Pack } from '@safe-global/relay-kit' +import { waitForOperationToFinish, generateTransferCallData } from 'playground/utils' // Safe owner PK const PRIVATE_KEY = '' @@ -15,6 +15,8 @@ const BUNDLER_URL = `https://api.pimlico.io/v1/sepolia/rpc?apikey=${PIMLICO_API_ // RPC URL const RPC_URL = 'https://sepolia.gateway.tenderly.co' +const CHAIN_NAME = 'sepolia' + // USDC CONTRACT ADDRESS IN SEPOLIA // faucet: https://faucet.circle.com/ const usdcTokenAddress = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' @@ -69,28 +71,7 @@ async function main() { executable: signedSafeOperation }) - console.log(`https://jiffyscan.xyz/userOpHash/${userOperationHash}?network=sepolia`) - - let userOperationReceipt = null - while (!userOperationReceipt) { - await new Promise((resolve) => setTimeout(resolve, 2000)) - userOperationReceipt = await safe4337Pack.getUserOperationReceipt(userOperationHash) - } - - console.group('User Operation Receipt and hash') - console.log('User Operation Receipt', userOperationReceipt) - console.log( - 'User Operation By Hash', - await safe4337Pack.getUserOperationByHash(userOperationHash) - ) - console.groupEnd() + await waitForOperationToFinish(userOperationHash, CHAIN_NAME, safe4337Pack) } main() - -const generateTransferCallData = (to: string, value: bigint) => { - const functionAbi = 'function transfer(address _to, uint256 _value) returns (bool)' - const iface = new ethers.Interface([functionAbi]) - - return iface.encodeFunctionData('transfer', [to, value]) -} diff --git a/playground/tsconfig.json b/playground/tsconfig.json index bed04be9a..906bd7f65 100644 --- a/playground/tsconfig.json +++ b/playground/tsconfig.json @@ -5,5 +5,5 @@ "noImplicitThis": false, "outDir": "dist" }, - "include": ["api-kit/**/*", "config/**/*", "protocol-kit/**/*", "relay-kit/**/*"] + "include": ["api-kit/**/*", "config/**/*", "protocol-kit/**/*", "relay-kit/**/*", "utils.ts"] } diff --git a/playground/utils.ts b/playground/utils.ts new file mode 100644 index 000000000..19634fcf2 --- /dev/null +++ b/playground/utils.ts @@ -0,0 +1,66 @@ +import { ethers } from 'ethers' +import { Safe4337Pack } from '@safe-global/relay-kit' +import { GetSafeOperationListResponse } from '@safe-global/api-kit' + +export async function waitForOperationToFinish( + userOperationHash: string, + chainName: string, + safe4337Pack: Safe4337Pack +) { + console.log(`https://jiffyscan.xyz/userOpHash/${userOperationHash}?network=${chainName}`) + + let userOperationReceipt = null + while (!userOperationReceipt) { + await new Promise((resolve) => setTimeout(resolve, 2000)) + userOperationReceipt = await safe4337Pack.getUserOperationReceipt(userOperationHash) + } + + console.group('User Operation Receipt and hash') + console.log('User Operation Receipt', userOperationReceipt) + console.log( + 'User Operation By Hash', + await safe4337Pack.getUserOperationByHash(userOperationHash) + ) + console.groupEnd() +} + +export function generateTransferCallData(to: string, value: bigint) { + const functionAbi = 'function transfer(address _to, uint256 _value) returns (bool)' + const iface = new ethers.Interface([functionAbi]) + + return iface.encodeFunctionData('transfer', [to, value]) +} + +export async function transfer( + signer: ethers.AbstractSigner, + tokenAddress: string, + to: string, + amount: bigint +) { + const transferEC20 = { + to: tokenAddress, + data: generateTransferCallData(to, amount), + value: '0' + } + + const transactionResponse = await signer.sendTransaction(transferEC20) + + return await transactionResponse.wait() +} + +export function sortResultsByCreatedDateDesc( + data: GetSafeOperationListResponse +): GetSafeOperationListResponse { + if (!data || !Array.isArray(data.results)) { + throw new Error('The provided data is invalid or does not contain a results array.') + } + + data.results.sort((a, b) => { + const dateA = new Date(a.created).getTime() + const dateB = new Date(b.created).getTime() + + return dateB - dateA + }) + + return data +}