From fbc495715164af83ec771a8873aa6a207910f933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Sat, 20 Jul 2024 09:39:37 +0200 Subject: [PATCH] feat: Safe kit testing (#911) --- packages/protocol-kit/src/index.ts | 6 +- packages/relay-kit/src/index.ts | 2 + packages/safe-kit/src/SafeClient.test.ts | 256 +++++++++++++++++- packages/safe-kit/src/SafeClient.ts | 116 +++++--- .../messages/SafeMessageClient.test.ts | 182 +++++++++++++ .../extensions/messages/SafeMessageClient.ts | 43 ++- .../messages/offChainMessages.test.ts | 39 +++ .../extensions/messages/offChainMessages.ts | 34 +-- .../messages/onChainMessages.test.ts | 38 +++ .../extensions/messages/onChainMessages.ts | 24 +- .../SafeOperationClient.test.ts | 205 ++++++++++++++ .../safe-operations/SafeOperationClient.ts | 50 ++-- .../safe-operations/safeOperations.test.ts | 69 +++++ .../safe-operations/safeOperations.ts | 40 ++- packages/safe-kit/src/types.ts | 39 +++ .../safe-kit/src/utils/proposeTransaction.ts | 12 +- .../safe-kit/src/utils/sendTransaction.ts | 18 +- playground/safe-kit/send-off-chain-message.ts | 10 +- playground/safe-kit/send-on-chain-message.ts | 8 +- playground/safe-kit/send-safe-operation.ts | 29 +- playground/safe-kit/send-transactions.ts | 8 +- 21 files changed, 1046 insertions(+), 182 deletions(-) create mode 100644 packages/safe-kit/src/extensions/messages/SafeMessageClient.test.ts create mode 100644 packages/safe-kit/src/extensions/messages/offChainMessages.test.ts create mode 100644 packages/safe-kit/src/extensions/messages/onChainMessages.test.ts create mode 100644 packages/safe-kit/src/extensions/safe-operations/SafeOperationClient.test.ts create mode 100644 packages/safe-kit/src/extensions/safe-operations/safeOperations.test.ts diff --git a/packages/protocol-kit/src/index.ts b/packages/protocol-kit/src/index.ts index 55f076393..53a3dc80d 100644 --- a/packages/protocol-kit/src/index.ts +++ b/packages/protocol-kit/src/index.ts @@ -36,6 +36,8 @@ import { validateEthereumAddress, validateEip3770Address } from './utils' +import EthSafeTransaction from './utils/transactions/SafeTransaction' +import EthSafeMessage from './utils/messages/SafeMessage' import { SafeTransactionOptionalProps } from './utils/transactions/types' import { encodeMultiSendData, standardizeSafeTransactionData } from './utils/transactions/utils' import { @@ -105,7 +107,9 @@ export { getEip712MessageTypes, hashSafeMessage, generateTypedData, - SafeProvider + SafeProvider, + EthSafeTransaction, + EthSafeMessage } export * from './types' diff --git a/packages/relay-kit/src/index.ts b/packages/relay-kit/src/index.ts index 47c72b993..2139b10da 100644 --- a/packages/relay-kit/src/index.ts +++ b/packages/relay-kit/src/index.ts @@ -4,6 +4,8 @@ export * from './packs/gelato/GelatoRelayPack' export * from './packs/gelato/types' export * from './packs/safe-4337/Safe4337Pack' +export { default as EthSafeOperation } from './packs/safe-4337/SafeOperation' + export * from './packs/safe-4337/estimators' export * from './packs/safe-4337/types' diff --git a/packages/safe-kit/src/SafeClient.test.ts b/packages/safe-kit/src/SafeClient.test.ts index 467a65b0c..98f393058 100644 --- a/packages/safe-kit/src/SafeClient.test.ts +++ b/packages/safe-kit/src/SafeClient.test.ts @@ -1,29 +1,263 @@ -import { SafeClient } from './SafeClient' // Adjust the import path based on your directory structure -import Safe from '@safe-global/protocol-kit' +import Safe, * as protocolKitModule from '@safe-global/protocol-kit' import SafeApiKit from '@safe-global/api-kit' -// Mock dependencies +import * as utils from './utils' +import { SafeClient } from './SafeClient' +import { MESSAGES, SafeClientTxStatus } from './constants' + jest.mock('@safe-global/protocol-kit') jest.mock('@safe-global/api-kit') -jest.mock('@safe-global/safe-kit/utils', () => ({ - createSafeClientResult: jest.fn(), - sendTransaction: jest.fn(), - proposeTransaction: jest.fn(), - waitSafeTxReceipt: jest.fn() -})) +jest.mock('./utils', () => { + return { + ...jest.requireActual('./utils'), + sendTransaction: jest.fn().mockResolvedValue('0xSafeDeploymentEthereumHash'), + proposeTransaction: jest.fn().mockResolvedValue('0xSafeTxHash'), + waitSafeTxReceipt: jest.fn() + } +}) + +const TRANSACTION = { to: '0xEthereumAddres', value: '0', data: '0x' } +const DEPLOYMENT_TRANSACTION = { to: '0xMultisig', value: '0', data: '0x' } +const TRANSACTION_BATCH = [TRANSACTION] +const SAFE_ADDRESS = '0xSafeAddress' +const SAFE_TX_HASH = '0xSafeTxHash' +const DEPLOYMENT_ETHEREUM_TX_HASH = '0xSafeDeploymentEthereumHash' +const ETHEREUM_TX_HASH = '0xEthereumTxHash' +const SAFE_TRANSACTION = new protocolKitModule.EthSafeTransaction({ + ...TRANSACTION, + operation: 0, + safeTxGas: '0', + baseGas: '0', + gasPrice: '0', + gasToken: '0x', + refundReceiver: '0x', + nonce: 0 +}) +const SAFE_PROVIDER = { + provider: 'http://ethereum.provider', + signer: '0xSignerAddress' +} +const PENDING_TRANSACTIONS = [{ safeTxHash: '0xPendingSafeTxHash' }] describe('SafeClient', () => { let safeClient: SafeClient - let protocolKit: jest.Mocked + let protocolKit: Safe let apiKit: jest.Mocked beforeEach(() => { - protocolKit = new Safe() as jest.Mocked + protocolKit = new Safe() apiKit = new SafeApiKit({ chainId: 1n }) as jest.Mocked + safeClient = new SafeClient(protocolKit, apiKit) + + protocolKit.getAddress = jest.fn().mockResolvedValue(SAFE_ADDRESS) + protocolKit.createTransaction = jest.fn().mockResolvedValue(SAFE_TRANSACTION) + protocolKit.signTransaction = jest.fn().mockResolvedValue(SAFE_TRANSACTION) + protocolKit.executeTransaction = jest.fn().mockResolvedValue({ hash: ETHEREUM_TX_HASH }) + protocolKit.connect = jest.fn().mockResolvedValue(protocolKit) + protocolKit.getSafeProvider = jest.fn().mockResolvedValue(SAFE_PROVIDER) + protocolKit.createSafeDeploymentTransaction = jest + .fn() + .mockResolvedValue(DEPLOYMENT_TRANSACTION) + protocolKit.wrapSafeTransactionIntoDeploymentBatch = jest + .fn() + .mockResolvedValue(DEPLOYMENT_TRANSACTION) + }) + + afterEach(() => { + jest.clearAllMocks() }) it('should allow to instantiate a SafeClient', () => { expect(safeClient).toBeInstanceOf(SafeClient) + expect(safeClient.protocolKit).toBe(protocolKit) + expect(safeClient.apiKit).toBe(apiKit) + }) + + describe('send', () => { + it('should propose the transaction if Safe account exists and has threshold > 1', async () => { + protocolKit.isSafeDeployed = jest.fn().mockResolvedValue(true) + protocolKit.getThreshold = jest.fn().mockResolvedValue(2) + + const result = await safeClient.send({ transactions: TRANSACTION_BATCH }) + + expect(protocolKit.createTransaction).toHaveBeenCalledWith({ + transactions: TRANSACTION_BATCH + }) + + expect(utils.proposeTransaction).toHaveBeenCalledWith({ + safeTransaction: SAFE_TRANSACTION, + protocolKit, + apiKit + }) + expect(result).toMatchObject({ + description: MESSAGES[SafeClientTxStatus.PENDING_SIGNATURES], + safeAddress: SAFE_ADDRESS, + status: SafeClientTxStatus.PENDING_SIGNATURES, + transactions: { + ethereumTxHash: undefined, + safeTxHash: SAFE_TX_HASH + } + }) + }) + + it('should execute the transaction if Safe account exists and has threshold === 1', async () => { + protocolKit.isSafeDeployed = jest.fn().mockResolvedValue(true) + protocolKit.getThreshold = jest.fn().mockResolvedValue(1) + + const result = await safeClient.send({ transactions: TRANSACTION_BATCH }) + + expect(protocolKit.createTransaction).toHaveBeenCalledWith({ + transactions: TRANSACTION_BATCH + }) + + expect(protocolKit.signTransaction).toHaveBeenCalledWith(SAFE_TRANSACTION) + expect(protocolKit.executeTransaction).toHaveBeenCalledWith(SAFE_TRANSACTION, {}) + + expect(result).toMatchObject({ + description: MESSAGES[SafeClientTxStatus.EXECUTED], + safeAddress: SAFE_ADDRESS, + status: SafeClientTxStatus.EXECUTED, + transactions: { + ethereumTxHash: ETHEREUM_TX_HASH, + safeTxHash: undefined + } + }) + }) + + it('should deploy and propose the transaction if Safe account does not exist and has threshold > 1', async () => { + protocolKit.isSafeDeployed = jest.fn().mockResolvedValue(false) + protocolKit.getThreshold = jest.fn().mockResolvedValue(2) + + const result = await safeClient.send({ transactions: TRANSACTION_BATCH }) + + expect(protocolKit.createSafeDeploymentTransaction).toHaveBeenCalledWith(undefined, {}) + expect(utils.sendTransaction).toHaveBeenCalledWith({ + transaction: DEPLOYMENT_TRANSACTION, + protocolKit + }) + expect(protocolKit.connect).toHaveBeenCalled() + expect(protocolKit.signTransaction).toHaveBeenCalledWith(SAFE_TRANSACTION) + expect(utils.proposeTransaction).toHaveBeenCalledWith({ + safeTransaction: SAFE_TRANSACTION, + protocolKit, + apiKit + }) + expect(result).toMatchObject({ + description: MESSAGES[SafeClientTxStatus.DEPLOYED_AND_PENDING_SIGNATURES], + safeAddress: SAFE_ADDRESS, + status: SafeClientTxStatus.DEPLOYED_AND_PENDING_SIGNATURES, + transactions: { + ethereumTxHash: undefined, + safeTxHash: SAFE_TX_HASH + }, + safeAccountDeployment: { + ethereumTxHash: DEPLOYMENT_ETHEREUM_TX_HASH + } + }) + }) + + it('should deploy and execute the transaction if Safe account does not exist and has threshold === 1', async () => { + protocolKit.isSafeDeployed = jest.fn().mockResolvedValue(false) + protocolKit.getThreshold = jest.fn().mockResolvedValue(1) + + const result = await safeClient.send({ transactions: TRANSACTION_BATCH }) + + expect(protocolKit.signTransaction).toHaveBeenCalledWith(SAFE_TRANSACTION) + expect(protocolKit.wrapSafeTransactionIntoDeploymentBatch).toHaveBeenCalledWith( + SAFE_TRANSACTION, + {} + ) + expect(protocolKit.connect).toHaveBeenCalled() + expect(result).toMatchObject({ + description: MESSAGES[SafeClientTxStatus.DEPLOYED_AND_EXECUTED], + safeAddress: SAFE_ADDRESS, + status: SafeClientTxStatus.DEPLOYED_AND_EXECUTED, + transactions: { + ethereumTxHash: DEPLOYMENT_ETHEREUM_TX_HASH, + safeTxHash: undefined + }, + safeAccountDeployment: { + ethereumTxHash: DEPLOYMENT_ETHEREUM_TX_HASH + } + }) + }) + }) + + describe('confirm', () => { + it('should confirm the transaction when enough signatures', async () => { + const TRANSACTION_RESPONSE = { + confirmations: [{ signature: '0x1' }, { signature: '0x2' }], + confirmationsRequired: 2 + } + + apiKit.getTransaction = jest.fn().mockResolvedValue(TRANSACTION_RESPONSE) + + const result = await safeClient.confirm({ safeTxHash: SAFE_TX_HASH }) + + expect(apiKit.getTransaction).toHaveBeenCalledWith(SAFE_TX_HASH) + expect(protocolKit.signTransaction).toHaveBeenCalledWith(TRANSACTION_RESPONSE) + expect(apiKit.confirmTransaction).toHaveBeenCalledWith(SAFE_TX_HASH, undefined) + expect(protocolKit.executeTransaction).toHaveBeenCalledWith(TRANSACTION_RESPONSE) + expect(result).toMatchObject({ + description: MESSAGES[SafeClientTxStatus.EXECUTED], + safeAddress: SAFE_ADDRESS, + status: SafeClientTxStatus.EXECUTED, + transactions: { + ethereumTxHash: ETHEREUM_TX_HASH, + safeTxHash: SAFE_TX_HASH + } + }) + }) + + it('should indicate more signatures are required when threshold is not matched', async () => { + const TRANSACTION_RESPONSE = { + confirmations: [{ signature: '0x1' }], + confirmationsRequired: 2 + } + + apiKit.getTransaction = jest.fn().mockResolvedValue(TRANSACTION_RESPONSE) + + const result = await safeClient.confirm({ safeTxHash: SAFE_TX_HASH }) + + expect(apiKit.getTransaction).toHaveBeenCalledWith(SAFE_TX_HASH) + expect(protocolKit.signTransaction).toHaveBeenCalledWith(TRANSACTION_RESPONSE) + expect(apiKit.confirmTransaction).toHaveBeenCalledWith(SAFE_TX_HASH, undefined) + + expect(result).toMatchObject({ + description: MESSAGES[SafeClientTxStatus.PENDING_SIGNATURES], + safeAddress: SAFE_ADDRESS, + status: SafeClientTxStatus.PENDING_SIGNATURES, + transactions: { + ethereumTxHash: undefined, + safeTxHash: SAFE_TX_HASH + } + }) + }) + }) + + describe('getPendingTransactions', () => { + it('should return the pending transactions for the Safe address', async () => { + apiKit.getPendingTransactions = jest.fn().mockResolvedValue(PENDING_TRANSACTIONS) + + const result = await safeClient.getPendingTransactions() + + expect(protocolKit.getAddress).toHaveBeenCalled() + expect(apiKit.getPendingTransactions).toHaveBeenCalledWith(SAFE_ADDRESS) + expect(result).toBe(PENDING_TRANSACTIONS) + }) + }) + + describe('extend', () => { + it('should enable the extension of the SafeClient with additional functionality', async () => { + const extendedClient = safeClient.extend(() => ({ + extendedFunction: () => 'extendedFunction', + extendedProp: 'extendedProp' + })) + + expect(extendedClient).toBeInstanceOf(SafeClient) + expect(extendedClient.extendedFunction()).toEqual('extendedFunction') + expect(extendedClient.extendedProp).toEqual('extendedProp') + }) }) }) diff --git a/packages/safe-kit/src/SafeClient.ts b/packages/safe-kit/src/SafeClient.ts index 8bede0780..7e8ba7e0d 100644 --- a/packages/safe-kit/src/SafeClient.ts +++ b/packages/safe-kit/src/SafeClient.ts @@ -2,9 +2,9 @@ import Safe from '@safe-global/protocol-kit' import SafeApiKit, { SafeMultisigTransactionListResponse } from '@safe-global/api-kit' import { SafeTransaction, - TransactionBase, TransactionOptions, - TransactionResult + TransactionResult, + Transaction } from '@safe-global/safe-core-sdk-types' import { @@ -14,7 +14,11 @@ import { waitSafeTxReceipt } from '@safe-global/safe-kit/utils' import { SafeClientTxStatus } from '@safe-global/safe-kit/constants' -import { SafeClientResult } from '@safe-global/safe-kit/types' +import { + ConfirmTransactionProps, + SafeClientResult, + SendTransactionProps +} from '@safe-global/safe-kit/types' /** * @class @@ -38,15 +42,25 @@ export class SafeClient { /** * Sends transactions through the Safe protocol. + * You can send an array to transactions { to, value, data} that we will convert to a transaction batch * - * @param {TransactionBase[]} transactions An array of transactions to be sent. - * @param {TransactionOptions} [options] Optional transaction options. + * @param {SendTransactionProps} props The SendTransactionProps object. + * @param {TransactionBase[]} props.transactions An array of transactions to be sent. + * @param {string} props.transactions[].to The recipient address of the transaction. + * @param {string} props.transactions[].value The value of the transaction. + * @param {string} props.transactions[].data The data of the transaction. + * @param {string} props.from The sender address of the transaction. + * @param {number | string} props.gasLimit The gas limit of the transaction. + * @param {number | string} props.gasPrice The gas price of the transaction. + * @param {number | string} props.maxFeePerGas The max fee per gas of the transaction. + * @param {number | string} props.maxPriorityFeePerGas The max priority fee per gas of the transaction. + * @param {number} props.nonce The nonce of the transaction. * @returns {Promise} A promise that resolves to the result of the transaction. */ - async send( - transactions: TransactionBase[], - options?: TransactionOptions - ): Promise { + async send({ + transactions, + ...transactionOptions + }: SendTransactionProps): Promise { const isSafeDeployed = await this.protocolKit.isSafeDeployed() const isMultisigSafe = (await this.protocolKit.getThreshold()) > 1 @@ -55,20 +69,20 @@ export class SafeClient { if (isSafeDeployed) { if (isMultisigSafe) { // If the threshold is greater than 1, we need to propose the transaction first - return this.#proposeTransaction(safeTransaction) + return this.#proposeTransaction({ safeTransaction }) } else { // If the threshold is 1, we can execute the transaction - return this.#executeTransaction(safeTransaction, options) + return this.#executeTransaction({ safeTransaction, ...transactionOptions }) } } else { if (isMultisigSafe) { // If the threshold is greater than 1, we need to deploy the Safe account first and // afterwards propose the transaction // The transaction should be confirmed with other owners until the threshold is reached - return this.#deployAndProposeTransaction(safeTransaction, options) + return this.#deployAndProposeTransaction({ safeTransaction, ...transactionOptions }) } else { // If the threshold is 1, we can deploy the Safe account and execute the transaction in one step - return this.#deployAndExecuteTransaction(safeTransaction, options) + return this.#deployAndExecuteTransaction({ safeTransaction, ...transactionOptions }) } } } @@ -76,11 +90,12 @@ export class SafeClient { /** * Confirms a transaction by its safe transaction hash. * - * @param {string} safeTxHash The hash of the safe transaction to confirm. + * @param {ConfirmTransactionProps} props The ConfirmTransactionProps object. + * @param {string} props.safeTxHash The hash of the safe transaction to confirm. * @returns {Promise} A promise that resolves to the result of the confirmed transaction. * @throws {Error} If the transaction confirmation fails. */ - async confirm(safeTxHash: string): Promise { + async confirm({ safeTxHash }: ConfirmTransactionProps): Promise { let transactionResponse = await this.apiKit.getTransaction(safeTxHash) const safeAddress = await this.protocolKit.getAddress() const signedTransaction = await this.protocolKit.signTransaction(transactionResponse) @@ -154,15 +169,21 @@ export class SafeClient { * @param {TransactionOptions} options Optional transaction options * @returns A promise that resolves to the result of the transaction */ - async #deployAndExecuteTransaction( - safeTransaction: SafeTransaction, - options?: TransactionOptions - ): Promise { + async #deployAndExecuteTransaction({ + safeTransaction, + ...transactionOptions + }: { safeTransaction: SafeTransaction } & TransactionOptions): Promise { safeTransaction = await this.protocolKit.signTransaction(safeTransaction) const transactionBatchWithDeployment = - await this.protocolKit.wrapSafeTransactionIntoDeploymentBatch(safeTransaction, options) - const hash = await sendTransaction(transactionBatchWithDeployment, {}, this.protocolKit) + await this.protocolKit.wrapSafeTransactionIntoDeploymentBatch( + safeTransaction, + transactionOptions + ) + const hash = await sendTransaction({ + transaction: transactionBatchWithDeployment, + protocolKit: this.protocolKit + }) await this.#reconnectSafe() @@ -177,24 +198,31 @@ export class SafeClient { /** * Deploys and proposes a transaction in one step. * - * @param safeTransaction The safe transaction to be proposed - * @param options Optional transaction options + * @param {SafeTransaction} safeTransaction The safe transaction to be proposed + * @param {TransactionOptions} transactionOptions Optional transaction options * @returns A promise that resolves to the result of the transaction */ - async #deployAndProposeTransaction( - safeTransaction: SafeTransaction, - options?: TransactionOptions - ): Promise { - const safeDeploymentTransaction = await this.protocolKit.createSafeDeploymentTransaction( - undefined, - options - ) - const hash = await sendTransaction(safeDeploymentTransaction, options || {}, this.protocolKit) + async #deployAndProposeTransaction({ + safeTransaction, + ...transactionOptions + }: { + safeTransaction: SafeTransaction + } & TransactionOptions): Promise { + const safeDeploymentTransaction: Transaction = + await this.protocolKit.createSafeDeploymentTransaction(undefined, transactionOptions) + const hash = await sendTransaction({ + transaction: { ...safeDeploymentTransaction }, + protocolKit: this.protocolKit + }) await this.#reconnectSafe() safeTransaction = await this.protocolKit.signTransaction(safeTransaction) - const safeTxHash = await proposeTransaction(safeTransaction, this.protocolKit, this.apiKit) + const safeTxHash = await proposeTransaction({ + safeTransaction, + protocolKit: this.protocolKit, + apiKit: this.apiKit + }) return createSafeClientResult({ safeAddress: await this.protocolKit.getAddress(), @@ -208,13 +236,16 @@ export class SafeClient { * Executes a transaction. * * @param {SafeTransaction} safeTransaction The safe transaction to be executed - * @param {TransactionOptions} options Optional transaction options + * @param {TransactionOptions} transactionOptions Optional transaction options * @returns A promise that resolves to the result of the transaction */ - async #executeTransaction(safeTransaction: SafeTransaction, options?: TransactionOptions) { + async #executeTransaction({ + safeTransaction, + ...transactionOptions + }: { safeTransaction: SafeTransaction } & TransactionOptions): Promise { safeTransaction = await this.protocolKit.signTransaction(safeTransaction) - const { hash } = await this.protocolKit.executeTransaction(safeTransaction, options) + const { hash } = await this.protocolKit.executeTransaction(safeTransaction, transactionOptions) return createSafeClientResult({ safeAddress: await this.protocolKit.getAddress(), @@ -223,8 +254,17 @@ export class SafeClient { }) } - async #proposeTransaction(safeTransaction: SafeTransaction) { - const safeTxHash = await proposeTransaction(safeTransaction, this.protocolKit, this.apiKit) + /** + * Proposes a transaction to the Safe. + * @param { SafeTransaction } safeTransaction The safe transaction to propose + * @returns The SafeClientResult + */ + async #proposeTransaction({ safeTransaction }: { safeTransaction: SafeTransaction }) { + const safeTxHash = await proposeTransaction({ + safeTransaction, + protocolKit: this.protocolKit, + apiKit: this.apiKit + }) return createSafeClientResult({ safeAddress: await this.protocolKit.getAddress(), diff --git a/packages/safe-kit/src/extensions/messages/SafeMessageClient.test.ts b/packages/safe-kit/src/extensions/messages/SafeMessageClient.test.ts new file mode 100644 index 000000000..840026a7d --- /dev/null +++ b/packages/safe-kit/src/extensions/messages/SafeMessageClient.test.ts @@ -0,0 +1,182 @@ +import Safe, * as protocolKitModule from '@safe-global/protocol-kit' +import SafeApiKit from '@safe-global/api-kit' + +import * as utils from '../../utils' +import { SafeMessageClient } from './SafeMessageClient' +import { MESSAGES, SafeClientTxStatus } from '../../constants' + +jest.mock('@safe-global/protocol-kit') +jest.mock('@safe-global/api-kit') +jest.mock('../../utils', () => { + return { + ...jest.requireActual('../../utils'), + sendTransaction: jest.fn().mockResolvedValue('0xSafeDeploymentEthereumHash'), + proposeTransaction: jest.fn().mockResolvedValue('0xSafeTxHash'), + waitSafeTxReceipt: jest.fn() + } +}) + +const MESSAGE = 'I am the owner of this Safe' +const MESSAGE_RESPONSE = { message: MESSAGE, confirmations: [{ signature: '0xSignature' }] } +const DEPLOYMENT_TRANSACTION = { to: '0xMultisig', value: '0', data: '0x' } +const SAFE_ADDRESS = '0xSafeAddress' +const SAFE_MESSAGE = new protocolKitModule.EthSafeMessage(MESSAGE) +const SAFE_PROVIDER = { + provider: 'http://ethereum.provider', + signer: '0xSignerAddress' +} + +describe('SafeClient', () => { + let safeMessageClient: SafeMessageClient + let protocolKit: Safe + let apiKit: jest.Mocked + + beforeEach(() => { + protocolKit = new Safe() + apiKit = new SafeApiKit({ chainId: 1n }) as jest.Mocked + safeMessageClient = new SafeMessageClient(protocolKit, apiKit) + + apiKit.getMessage = jest.fn().mockResolvedValue(MESSAGE_RESPONSE) + + protocolKit.createMessage = jest.fn().mockReturnValue(SAFE_MESSAGE) + protocolKit.signMessage = jest.fn().mockResolvedValue(SAFE_MESSAGE) + protocolKit.getAddress = jest.fn().mockResolvedValue(SAFE_ADDRESS) + protocolKit.connect = jest.fn().mockResolvedValue(protocolKit) + protocolKit.getContractVersion = jest.fn().mockResolvedValue('1.1.1') + protocolKit.getChainId = jest.fn().mockResolvedValue(1n) + protocolKit.getSafeMessageHash = jest.fn().mockResolvedValue('0xSafeMessageHash') + protocolKit.getSafeProvider = jest.fn().mockResolvedValue(SAFE_PROVIDER) + protocolKit.createSafeDeploymentTransaction = jest + .fn() + .mockResolvedValue(DEPLOYMENT_TRANSACTION) + + protocolKit.wrapSafeTransactionIntoDeploymentBatch = jest + .fn() + .mockResolvedValue(DEPLOYMENT_TRANSACTION) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it('should allow to instantiate a SafeClient', () => { + expect(safeMessageClient).toBeInstanceOf(SafeMessageClient) + expect(safeMessageClient.protocolKit).toBe(protocolKit) + expect(safeMessageClient.apiKit).toBe(apiKit) + }) + + describe('sendMessage', () => { + it('should add a confirmed message if the Safe exists and the threshold === 1', async () => { + protocolKit.isSafeDeployed = jest.fn().mockResolvedValue(true) + protocolKit.getThreshold = jest.fn().mockResolvedValue(1) + + const result = await safeMessageClient.sendMessage({ message: MESSAGE }) + + expect(protocolKit.createMessage).toHaveBeenCalledWith(MESSAGE) + expect(protocolKit.signMessage).toHaveBeenCalledWith(SAFE_MESSAGE) + expect(apiKit.addMessage).toHaveBeenCalledWith(SAFE_ADDRESS, { + message: SAFE_MESSAGE.data, + signature: SAFE_MESSAGE.encodedSignatures() + }) + + expect(result).toMatchObject({ + description: MESSAGES[SafeClientTxStatus.MESSAGE_CONFIRMED], + safeAddress: SAFE_ADDRESS, + status: SafeClientTxStatus.MESSAGE_CONFIRMED, + messages: { + messageHash: '0xSafeMessageHash' + } + }) + }) + + it('should add a pending confirmation message if the Safe exists and the threshold > 1', async () => { + protocolKit.isSafeDeployed = jest.fn().mockResolvedValue(true) + protocolKit.getThreshold = jest.fn().mockResolvedValue(2) + + const result = await safeMessageClient.sendMessage({ message: MESSAGE }) + + expect(protocolKit.createMessage).toHaveBeenCalledWith(MESSAGE) + expect(protocolKit.signMessage).toHaveBeenCalledWith(SAFE_MESSAGE) + expect(apiKit.addMessage).toHaveBeenCalledWith(SAFE_ADDRESS, { + message: SAFE_MESSAGE.data, + signature: SAFE_MESSAGE.encodedSignatures() + }) + + expect(result).toMatchObject({ + description: MESSAGES[SafeClientTxStatus.MESSAGE_PENDING_SIGNATURES], + safeAddress: SAFE_ADDRESS, + status: SafeClientTxStatus.MESSAGE_PENDING_SIGNATURES, + messages: { + messageHash: '0xSafeMessageHash' + } + }) + }) + + it('should deploy and add the message if Safe account does not exist and has threshold > 1', async () => { + protocolKit.isSafeDeployed = jest.fn().mockResolvedValue(false) + protocolKit.getThreshold = jest.fn().mockResolvedValue(1) + + const result = await safeMessageClient.sendMessage({ message: MESSAGE }) + + expect(protocolKit.createSafeDeploymentTransaction).toHaveBeenCalledWith(undefined) + expect(utils.sendTransaction).toHaveBeenCalledWith({ + transaction: DEPLOYMENT_TRANSACTION, + protocolKit + }) + expect(protocolKit.connect).toHaveBeenCalled() + + expect(protocolKit.createMessage).toHaveBeenCalledWith(MESSAGE) + expect(protocolKit.signMessage).toHaveBeenCalledWith(SAFE_MESSAGE) + expect(apiKit.addMessage).toHaveBeenCalledWith(SAFE_ADDRESS, { + message: SAFE_MESSAGE.data, + signature: SAFE_MESSAGE.encodedSignatures() + }) + + expect(result).toMatchObject({ + description: MESSAGES[SafeClientTxStatus.DEPLOYED_AND_MESSAGE_CONFIRMED], + safeAddress: SAFE_ADDRESS, + status: SafeClientTxStatus.DEPLOYED_AND_MESSAGE_CONFIRMED, + messages: { + messageHash: '0xSafeMessageHash' + }, + safeAccountDeployment: { + ethereumTxHash: '0xSafeDeploymentEthereumHash' + } + }) + }) + }) + + describe('confirmMessage', () => { + it('should confirm the message', async () => { + protocolKit.isSafeDeployed = jest.fn().mockResolvedValue(false) + protocolKit.getThreshold = jest.fn().mockResolvedValue(1) + + const result = await safeMessageClient.confirmMessage({ messageHash: '0xSafeMessageHash' }) + + expect(protocolKit.createMessage).toHaveBeenCalledWith(MESSAGE_RESPONSE.message) + expect(protocolKit.signMessage).toHaveBeenCalledWith(SAFE_MESSAGE) + expect(apiKit.addMessageSignature).toHaveBeenCalledWith('0xSafeMessageHash', undefined) + expect(result).toMatchObject({ + description: MESSAGES[SafeClientTxStatus.MESSAGE_CONFIRMED], + safeAddress: SAFE_ADDRESS, + status: SafeClientTxStatus.MESSAGE_CONFIRMED, + messages: { + messageHash: '0xSafeMessageHash' + } + }) + }) + }) + + describe('getPendingMessages', () => { + it('should return the pending messages for the Safe address', async () => { + const PENDING_MESSAGES = [{ messageHash: 'messageHash' }] + + apiKit.getMessages = jest.fn().mockResolvedValue(PENDING_MESSAGES) + + const result = await safeMessageClient.getPendingMessages() + + expect(apiKit.getMessages).toHaveBeenCalledWith(SAFE_ADDRESS, undefined) + expect(result).toBe(PENDING_MESSAGES) + }) + }) +}) diff --git a/packages/safe-kit/src/extensions/messages/SafeMessageClient.ts b/packages/safe-kit/src/extensions/messages/SafeMessageClient.ts index f40a11f0b..63fe0c834 100644 --- a/packages/safe-kit/src/extensions/messages/SafeMessageClient.ts +++ b/packages/safe-kit/src/extensions/messages/SafeMessageClient.ts @@ -1,13 +1,17 @@ import Safe, { hashSafeMessage } from '@safe-global/protocol-kit' import SafeApiKit, { EIP712TypedData as ApiKitEIP712TypedData, - GetSafeMessageListProps, + ListOptions, SafeMessageListResponse } from '@safe-global/api-kit' -import { EIP712TypedData, SafeMessage } from '@safe-global/safe-core-sdk-types' +import { SafeMessage } from '@safe-global/safe-core-sdk-types' import { createSafeClientResult, sendTransaction } from '@safe-global/safe-kit/utils' import { SafeClientTxStatus } from '@safe-global/safe-kit/constants' -import { SafeClientResult } from '@safe-global/safe-kit/types' +import { + ConfirmOffChainMessageProps, + SafeClientResult, + SendOffChainMessageProps +} from '@safe-global/safe-kit/types' /** * @class @@ -30,27 +34,29 @@ export class SafeMessageClient { /** * Send off-chain messages using the Transaction service * - * @param {string | EIP712TypedData} message The message to be sent. Can be a raw string or an EIP712TypedData object + * @param {SendOffChainMessageProps} props The message properties + * @param {string | EIP712TypedData} props.message The message to be sent. Can be a raw string or an EIP712TypedData object * @returns {Promise} A SafeClientResult. You can get the messageHash to confirmMessage() afterwards from the messages property */ - async sendMessage(message: string | EIP712TypedData): Promise { + async sendMessage({ message }: SendOffChainMessageProps): Promise { const isSafeDeployed = await this.protocolKit.isSafeDeployed() const safeMessage = this.protocolKit.createMessage(message) if (isSafeDeployed) { - return this.#addMessage(safeMessage) + return this.#addMessage({ safeMessage }) } else { - return this.#deployAndAddMessage(safeMessage) + return this.#deployAndAddMessage({ safeMessage }) } } /** * Confirms an off-chain message using the Transaction service * - * @param {string} messageHash The messageHash. Returned from the sendMessage() method inside the SafeClientResult messages property + * @param {ConfirmOffChainMessageProps} props The confirmation properties + * @param {string} props.messageHash The messageHash. Returned from the sendMessage() method inside the SafeClientResult messages property * @returns {Promise} A SafeClientResult with the result of the confirmation */ - async confirmMessage(messageHash: string): Promise { + async confirmMessage({ messageHash }: ConfirmOffChainMessageProps): Promise { let messageResponse = await this.apiKit.getMessage(messageHash) const safeAddress = await this.protocolKit.getAddress() const threshold = await this.protocolKit.getThreshold() @@ -74,10 +80,10 @@ export class SafeMessageClient { /** * Get the list of pending off-chain messages. This messages can be confirmed using the confirmMessage() method * - * @param {GetSafeMessageListProps} [options] Optional query parameters for pagination + * @param {ListOptions} options The pagination options * @returns {Promise} A list of pending messages */ - async getPendingMessages(options?: GetSafeMessageListProps): Promise { + async getPendingMessages(options?: ListOptions): Promise { const safeAddress = await this.protocolKit.getAddress() return this.apiKit.getMessages(safeAddress, options) @@ -92,21 +98,28 @@ export class SafeMessageClient { * @param {SafeTransaction} safeMessage The safe message * @returns {Promise} The SafeClientResult */ - async #deployAndAddMessage(safeMessage: SafeMessage): Promise { + async #deployAndAddMessage({ + safeMessage + }: { + safeMessage: SafeMessage + }): Promise { let deploymentTxHash const threshold = await this.protocolKit.getThreshold() const safeDeploymentTransaction = await this.protocolKit.createSafeDeploymentTransaction(undefined) try { - deploymentTxHash = await sendTransaction(safeDeploymentTransaction, {}, this.protocolKit) + deploymentTxHash = await sendTransaction({ + transaction: safeDeploymentTransaction, + protocolKit: this.protocolKit + }) await this.#updateProtocolKitWithDeployedSafe() } catch (error) { throw new Error('Could not deploy the Safe account') } try { - const { messages } = await this.#addMessage(safeMessage) + const { messages } = await this.#addMessage({ safeMessage }) const messageResponse = await this.apiKit.getMessage(messages?.messageHash || '0x') return createSafeClientResult({ @@ -131,7 +144,7 @@ export class SafeMessageClient { * @param {SafeMessage} safeMessage The message * @returns {Promise} The SafeClientResult */ - async #addMessage(safeMessage: SafeMessage): Promise { + async #addMessage({ safeMessage }: { safeMessage: SafeMessage }): Promise { const safeAddress = await this.protocolKit.getAddress() const threshold = await this.protocolKit.getThreshold() const signedMessage = await this.protocolKit.signMessage(safeMessage) diff --git a/packages/safe-kit/src/extensions/messages/offChainMessages.test.ts b/packages/safe-kit/src/extensions/messages/offChainMessages.test.ts new file mode 100644 index 000000000..ee40c1b27 --- /dev/null +++ b/packages/safe-kit/src/extensions/messages/offChainMessages.test.ts @@ -0,0 +1,39 @@ +import Safe from '@safe-global/protocol-kit' +import SafeApiKit from '@safe-global/api-kit' +import { offChainMessages } from './offChainMessages' +import { SafeClient } from '../../SafeClient' + +jest.mock('@safe-global/protocol-kit') +jest.mock('@safe-global/api-kit') +jest.mock('../../utils', () => { + return { + ...jest.requireActual('../../utils'), + sendTransaction: jest.fn().mockResolvedValue('0xSafeDeploymentEthereumHash'), + proposeTransaction: jest.fn().mockResolvedValue('0xSafeTxHash'), + waitSafeTxReceipt: jest.fn() + } +}) + +describe('onChainMessages', () => { + let protocolKit: Safe + let apiKit: jest.Mocked + let safeClient: SafeClient + + beforeEach(() => { + protocolKit = new Safe() + apiKit = new SafeApiKit({ chainId: 1n }) as jest.Mocked + safeClient = new SafeClient(protocolKit, apiKit) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it('should extend the SafeClient with the offChainMessages methods', async () => { + const safeMessagesClient = safeClient.extend(offChainMessages()) + + expect(safeMessagesClient.sendOffChainMessage).toBeDefined() + expect(safeMessagesClient.confirmOffChainMessage).toBeDefined() + expect(safeMessagesClient.getPendingOffChainMessages).toBeDefined() + }) +}) diff --git a/packages/safe-kit/src/extensions/messages/offChainMessages.ts b/packages/safe-kit/src/extensions/messages/offChainMessages.ts index 4750fc3bf..5e04ea864 100644 --- a/packages/safe-kit/src/extensions/messages/offChainMessages.ts +++ b/packages/safe-kit/src/extensions/messages/offChainMessages.ts @@ -1,9 +1,12 @@ -import { GetSafeMessageListProps, SafeMessageListResponse } from '@safe-global/api-kit' -import { EIP712TypedData } from '@safe-global/safe-core-sdk-types' +import { ListOptions, SafeMessageListResponse } from '@safe-global/api-kit' import { SafeClient } from '@safe-global/safe-kit/SafeClient' import { SafeMessageClient } from '@safe-global/safe-kit/extensions/messages/SafeMessageClient' -import { SafeClientResult } from '@safe-global/safe-kit/types' +import { + ConfirmOffChainMessageProps, + SafeClientResult, + SendOffChainMessageProps +} from '@safe-global/safe-kit/types' /** * Extend the SafeClient with the ability to use off-chain messages @@ -15,8 +18,8 @@ import { SafeClientResult } from '@safe-global/safe-kit/types' * offChainMessages() * ) * - * const { messages } = await safeMessagesClient.sendMessage(...) - * await safeMessagesClient.confirm(messages?.messageHash) + * const { messages } = await safeMessagesClient.sendOffChainMessage({ message }) + * await safeMessagesClient.confirmOffChainMessage({ messageHash: messages?.messageHash}) */ export function offChainMessages() { return (client: SafeClient) => { @@ -26,30 +29,27 @@ export function offChainMessages() { /** * Creates an off-chain message using the Transaction service * - * @param {string | EIP712TypedData} message The message to be sent, can be a raw string or an EIP712TypedData object - * @returns {Promise} A SafeClientResult. You can get the messageHash to confirmMessage() afterwards from the messages property - */ - async sendOffChainMessage(message: string | EIP712TypedData): Promise { - return safeMessageClient.sendMessage(message) + * @param {SendOffChainMessageProps} props The message properties + * @returns {Promise} A SafeClientResult. You can get the messageHash to confirmMessage() afterwards from the messages property */ + async sendOffChainMessage(props: SendOffChainMessageProps): Promise { + return safeMessageClient.sendMessage(props) }, /** * Confirms an off-chain message using the Transaction service * - * @param {string} messageHash The messageHash. Returned from the sendMessage() method inside the SafeClientResult messages property + * @param {ConfirmOffChainMessageProps} props The confirmation properties * @returns {Promise} A SafeClientResult with the result of the confirmation */ - async confirmOffChainMessage(messageHash: string): Promise { - return safeMessageClient.confirmMessage(messageHash) + async confirmOffChainMessage(props: ConfirmOffChainMessageProps): Promise { + return safeMessageClient.confirmMessage(props) }, /** * Get the list of pending off-chain messages. This messages can be confirmed using the confirmMessage() method * - * @param {GetSafeMessageListProps} [options] Optional query parameters for pagination + * @param {ListOptions} options The pagination options * @returns {Promise} A list of pending messages */ - async getPendingOffChainMessages( - options?: GetSafeMessageListProps - ): Promise { + async getPendingOffChainMessages(options?: ListOptions): Promise { return safeMessageClient.getPendingMessages(options) } } diff --git a/packages/safe-kit/src/extensions/messages/onChainMessages.test.ts b/packages/safe-kit/src/extensions/messages/onChainMessages.test.ts new file mode 100644 index 000000000..46b22d50c --- /dev/null +++ b/packages/safe-kit/src/extensions/messages/onChainMessages.test.ts @@ -0,0 +1,38 @@ +import Safe from '@safe-global/protocol-kit' +import SafeApiKit from '@safe-global/api-kit' + +import { onChainMessages } from './onChainMessages' +import { SafeClient } from '../../SafeClient' + +jest.mock('@safe-global/protocol-kit') +jest.mock('@safe-global/api-kit') +jest.mock('../../utils', () => { + return { + ...jest.requireActual('../../utils'), + sendTransaction: jest.fn().mockResolvedValue('0xSafeDeploymentEthereumHash'), + proposeTransaction: jest.fn().mockResolvedValue('0xSafeTxHash'), + waitSafeTxReceipt: jest.fn() + } +}) + +describe('onChainMessages', () => { + let protocolKit: Safe + let apiKit: jest.Mocked + let safeClient: SafeClient + + beforeEach(() => { + protocolKit = new Safe() + apiKit = new SafeApiKit({ chainId: 1n }) as jest.Mocked + safeClient = new SafeClient(protocolKit, apiKit) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it('should extend the SafeClient with the onChainMessages methods', async () => { + const safeMessagesClient = safeClient.extend(onChainMessages()) + + expect(safeMessagesClient.sendOnChainMessage).toBeDefined() + }) +}) diff --git a/packages/safe-kit/src/extensions/messages/onChainMessages.ts b/packages/safe-kit/src/extensions/messages/onChainMessages.ts index 85bf2b348..b582029f2 100644 --- a/packages/safe-kit/src/extensions/messages/onChainMessages.ts +++ b/packages/safe-kit/src/extensions/messages/onChainMessages.ts @@ -1,12 +1,8 @@ import { hashSafeMessage } from '@safe-global/protocol-kit' -import { - EIP712TypedData, - OperationType, - TransactionOptions -} from '@safe-global/safe-core-sdk-types' +import { OperationType } from '@safe-global/safe-core-sdk-types' import { SafeClient } from '@safe-global/safe-kit/SafeClient' -import { SafeClientResult } from '@safe-global/safe-kit/types' +import { SafeClientResult, SendOnChainMessageProps } from '@safe-global/safe-kit/types' /** * Extend the SafeClient with the ability to use on-chain messages @@ -20,8 +16,8 @@ import { SafeClientResult } from '@safe-global/safe-kit/types' * onChainMessages() * ) * - * const { transactions } = await safeMessageClient.sendMessage(...) - * await safeMessageClient.confirm(transactions?.safeTxHash) + * const { transactions } = await safeMessageClient.sendOnChainMessage({ message }) + * await safeMessageClient.confirm({ safeTxHash: transactions?.safeTxHash}) */ export function onChainMessages() { return (client: SafeClient) => ({ @@ -30,14 +26,12 @@ export function onChainMessages() { * The message can be a string or an EIP712TypedData object * As this method creates a new transaction you can confirm it using the safeTxHash and the confirm() method and * retrieve the pending transactions using the getPendingTransactions() method from the general client - * @param {string | EIP712TypedData} message The message to be sent - * @param {TransactionOptions} options Optional transaction options + * @param {SendOnChainMessageProps} props The message properties * @returns {Promise} A SafeClientResult. You can get the safeTxHash to confirm from the transaction property */ - async sendOnChainMessage( - message: string | EIP712TypedData, - options?: TransactionOptions - ): Promise { + async sendOnChainMessage(props: SendOnChainMessageProps): Promise { + const { message, ...transactionOptions } = props + const signMessageLibContract = await client.protocolKit .getSafeProvider() .getSignMessageLibContract({ @@ -51,7 +45,7 @@ export function onChainMessages() { operation: OperationType.DelegateCall } - return client.send([transaction], options) + return client.send({ transactions: [transaction], ...transactionOptions }) } }) } diff --git a/packages/safe-kit/src/extensions/safe-operations/SafeOperationClient.test.ts b/packages/safe-kit/src/extensions/safe-operations/SafeOperationClient.test.ts new file mode 100644 index 000000000..02a7045a0 --- /dev/null +++ b/packages/safe-kit/src/extensions/safe-operations/SafeOperationClient.test.ts @@ -0,0 +1,205 @@ +import Safe, * as protocolKitModule from '@safe-global/protocol-kit' +import SafeApiKit from '@safe-global/api-kit' +import { Safe4337Pack, EthSafeOperation } from '@safe-global/relay-kit' + +import { SafeOperationClient } from './SafeOperationClient' +import { MESSAGES, SafeClientTxStatus } from '../../constants' + +jest.mock('@safe-global/protocol-kit') +jest.mock('@safe-global/relay-kit') +jest.mock('@safe-global/api-kit') +jest.mock('../../utils', () => { + return { + ...jest.requireActual('../../utils'), + sendTransaction: jest.fn().mockResolvedValue('0xSafeDeploymentEthereumHash'), + proposeTransaction: jest.fn().mockResolvedValue('0xSafeTxHash'), + waitSafeTxReceipt: jest.fn() + } +}) + +const TRANSACTION = { to: '0xEthereumAddres', value: '0', data: '0x' } +const TRANSACTION_BATCH = [TRANSACTION] +const SAFE_ADDRESS = '0xSafeAddress' +const SAFE_OPERATION_HASH = '0xSafeOperationHash' +const USER_OPERATION_HASH = '0xUserOperationHash' +const PENDING_SAFE_OPERATIONS = [{ safeOperationHash: SAFE_OPERATION_HASH }] +const SAFE_OPERATION_RESPONSE = { + confirmations: [ + { + signature: 'OxSignature' + }, + { + signature: 'OxSignature' + } + ] +} +const SAFE_OPERATION = new EthSafeOperation( + { + sender: '0xSenderAddress', + nonce: '0', + initCode: '0xInitCode', + callData: '0xCallData', + callGasLimit: 0n, + verificationGasLimit: 0n, + preVerificationGas: 0n, + maxFeePerGas: 0n, + maxPriorityFeePerGas: 0n, + paymasterAndData: '0xPaymasterAndData', + signature: '0xSignature' + }, + { + chainId: 1n, + entryPoint: '0xEntryPoint', + moduleAddress: '0xModuleAddress' + } +) + +describe('SafeOperationClient', () => { + let safeOperationClient: SafeOperationClient + let protocolKit: Safe + let apiKit: jest.Mocked + let safe4337Pack: Safe4337Pack + + beforeEach(() => { + const bundlerClientMock = { send: jest.fn().mockResolvedValue('1') } as any + + protocolKit = new Safe() + apiKit = new SafeApiKit({ chainId: 1n }) as jest.Mocked + safe4337Pack = new Safe4337Pack({ + protocolKit, + bundlerClient: bundlerClientMock, + bundlerUrl: 'http://bundler.url', + chainId: 1n, + paymasterOptions: undefined, + entryPointAddress: '0xEntryPoint', + safe4337ModuleAddress: '0xModuleAddress' + }) as jest.Mocked + + safe4337Pack.protocolKit = protocolKit + + safeOperationClient = new SafeOperationClient(safe4337Pack, apiKit) + + apiKit.confirmSafeOperation = jest.fn().mockResolvedValue(true) + apiKit.getSafeOperation = jest.fn().mockResolvedValue(SAFE_OPERATION_RESPONSE) + + protocolKit.getAddress = jest.fn().mockResolvedValue(SAFE_ADDRESS) + protocolKit.signHash = jest + .fn() + .mockResolvedValue(new protocolKitModule.EthSafeSignature('0xSigner', '0xSignature')) + + safe4337Pack.createTransaction = jest.fn().mockResolvedValue(SAFE_OPERATION) + safe4337Pack.signSafeOperation = jest.fn().mockResolvedValue(SAFE_OPERATION) + safe4337Pack.executeTransaction = jest.fn().mockResolvedValue(USER_OPERATION_HASH) + safe4337Pack.getUserOperationReceipt = jest + .fn() + .mockResolvedValue({ hash: USER_OPERATION_HASH }) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it('should allow to instantiate a SafeOperationClient', () => { + expect(safeOperationClient).toBeInstanceOf(SafeOperationClient) + expect(safeOperationClient.safe4337Pack).toBe(safe4337Pack) + expect(safeOperationClient.apiKit).toBe(apiKit) + }) + + describe('sendSafeOperation', () => { + it('should save the Safe operation using the Transaction service when threshold is > 1', async () => { + protocolKit.getThreshold = jest.fn().mockResolvedValue(2) + jest.spyOn(SAFE_OPERATION, 'getHash').mockReturnValue(SAFE_OPERATION_HASH) + + const safeOperationResult = await safeOperationClient.sendSafeOperation({ + transactions: TRANSACTION_BATCH + }) + + expect(safe4337Pack.createTransaction).toHaveBeenCalledWith({ + transactions: TRANSACTION_BATCH, + options: {} + }) + expect(safe4337Pack.signSafeOperation).toHaveBeenCalledWith(SAFE_OPERATION) + expect(apiKit.addSafeOperation).toHaveBeenCalledWith(SAFE_OPERATION) + + expect(safeOperationResult).toEqual({ + safeAddress: SAFE_ADDRESS, + description: MESSAGES[SafeClientTxStatus.SAFE_OPERATION_PENDING_SIGNATURES], + status: SafeClientTxStatus.SAFE_OPERATION_PENDING_SIGNATURES, + safeOperations: { safeOperationHash: SAFE_OPERATION_HASH } + }) + }) + + it('should send the Safe operation to the bundler when threshold === 1', async () => { + protocolKit.getThreshold = jest.fn().mockResolvedValue(1) + jest.spyOn(SAFE_OPERATION, 'getHash').mockReturnValue(SAFE_OPERATION_HASH) + + const safeOperationResult = await safeOperationClient.sendSafeOperation({ + transactions: TRANSACTION_BATCH + }) + + expect(safe4337Pack.executeTransaction).toHaveBeenCalledWith({ executable: SAFE_OPERATION }) + + expect(safeOperationResult).toEqual({ + safeAddress: SAFE_ADDRESS, + description: MESSAGES[SafeClientTxStatus.SAFE_OPERATION_EXECUTED], + status: SafeClientTxStatus.SAFE_OPERATION_EXECUTED, + safeOperations: { + safeOperationHash: SAFE_OPERATION_HASH, + userOperationHash: USER_OPERATION_HASH + } + }) + }) + }) + + describe('confirmSafeOperation', () => { + it('should confirm the Safe operation and send it to the bundler when threshold is reached', async () => { + protocolKit.getThreshold = jest.fn().mockResolvedValue(2) + + const safeOperationResult = await safeOperationClient.confirmSafeOperation({ + safeOperationHash: SAFE_OPERATION_HASH + }) + + expect(safe4337Pack.executeTransaction).toHaveBeenCalledWith({ + executable: SAFE_OPERATION_RESPONSE + }) + + expect(safeOperationResult).toEqual({ + safeAddress: SAFE_ADDRESS, + description: MESSAGES[SafeClientTxStatus.SAFE_OPERATION_EXECUTED], + status: SafeClientTxStatus.SAFE_OPERATION_EXECUTED, + safeOperations: { + safeOperationHash: SAFE_OPERATION_HASH, + userOperationHash: USER_OPERATION_HASH + } + }) + }) + it('should indicate more signatures are required when threshold is not reached', async () => { + protocolKit.getThreshold = jest.fn().mockResolvedValue(3) + + const safeOperationResult = await safeOperationClient.confirmSafeOperation({ + safeOperationHash: SAFE_OPERATION_HASH + }) + + expect(safeOperationResult).toEqual({ + safeAddress: SAFE_ADDRESS, + description: MESSAGES[SafeClientTxStatus.SAFE_OPERATION_PENDING_SIGNATURES], + status: SafeClientTxStatus.SAFE_OPERATION_PENDING_SIGNATURES, + safeOperations: { + safeOperationHash: SAFE_OPERATION_HASH + } + }) + }) + }) + + describe('getPendingSafeOperations', () => { + it('should return the pending Safe operations for the Safe address', async () => { + apiKit.getSafeOperationsByAddress = jest.fn().mockResolvedValue(PENDING_SAFE_OPERATIONS) + + const result = await safeOperationClient.getPendingSafeOperations() + + expect(protocolKit.getAddress).toHaveBeenCalled() + expect(apiKit.getSafeOperationsByAddress).toHaveBeenCalledWith({ safeAddress: SAFE_ADDRESS }) + expect(result).toBe(PENDING_SAFE_OPERATIONS) + }) + }) +}) diff --git a/packages/safe-kit/src/extensions/safe-operations/SafeOperationClient.ts b/packages/safe-kit/src/extensions/safe-operations/SafeOperationClient.ts index e93e87a16..b7e9dc0bb 100644 --- a/packages/safe-kit/src/extensions/safe-operations/SafeOperationClient.ts +++ b/packages/safe-kit/src/extensions/safe-operations/SafeOperationClient.ts @@ -1,10 +1,14 @@ import Safe, { buildSignatureBytes } from '@safe-global/protocol-kit' -import SafeApiKit, { GetSafeOperationListResponse } from '@safe-global/api-kit' -import { Safe4337CreateTransactionProps, Safe4337Pack } from '@safe-global/relay-kit' +import SafeApiKit, { ListOptions, GetSafeOperationListResponse } from '@safe-global/api-kit' +import { Safe4337Pack } from '@safe-global/relay-kit' import { createSafeClientResult } from '@safe-global/safe-kit/utils' import { SafeClientTxStatus } from '@safe-global/safe-kit/constants' -import { SafeClientResult } from '@safe-global/safe-kit/types' +import { + ConfirmSafeOperationProps, + SafeClientResult, + SendSafeOperationProps +} from '@safe-global/safe-kit/types' /** * @class @@ -31,17 +35,23 @@ export class SafeOperationClient { * * @param {Safe4337CreateTransactionProps} props The Safe4337CreateTransactionProps object * @param {SafeTransaction[]} props.transactions An array of transactions to be batched - * @param {TransactionOptions} [props.options] Optional transaction options + * @param {TransactionOptions} [props.amountToApprove] The amount to approve for the SafeOperation + * @param {TransactionOptions} [props.validUntil] The validUntil timestamp for the SafeOperation + * @param {TransactionOptions} [props.validAfter] The validAfter timestamp for the SafeOperation + * @param {TransactionOptions} [props.feeEstimator] The feeEstimator to calculate the fees * @returns {Promise} A promise that resolves with the status of the SafeOperation */ async sendSafeOperation({ transactions, - options = {} - }: Safe4337CreateTransactionProps): Promise { + ...sendSafeOperationOptions + }: SendSafeOperationProps): Promise { const safeAddress = await this.protocolKit.getAddress() const isMultisigSafe = (await this.protocolKit.getThreshold()) > 1 - let safeOperation = await this.safe4337Pack.createTransaction({ transactions, options }) + let safeOperation = await this.safe4337Pack.createTransaction({ + transactions, + options: sendSafeOperationOptions + }) safeOperation = await this.safe4337Pack.signSafeOperation(safeOperation) if (isMultisigSafe) { @@ -60,7 +70,7 @@ export class SafeOperationClient { executable: safeOperation }) - await this.#waitForOperationToFinish(userOperationHash) + await this.#waitForOperationToFinish({ userOperationHash }) return createSafeClientResult({ safeAddress, @@ -73,18 +83,18 @@ export class SafeOperationClient { /** * Confirms the stored safeOperation * - * @param {string} safeOperationHash The hash of the safe operation to confirm. + * @param {ConfirmSafeOperationProps} props The confirmation properties + * @param {string} props.safeOperationHash The hash of the safe operation to confirm. * The safeOperationHash can be extracted from the SafeClientResult of the sendSafeOperation method under the safeOperations property * You must confirmSafeOperation() with the other owners and once the threshold is reached the SafeOperation will be sent to the bundler * @returns {Promise} A promise that resolves to the result of the safeOperation. */ - async confirmSafeOperation(safeOperationHash: string): Promise { + async confirmSafeOperation({ + safeOperationHash + }: ConfirmSafeOperationProps): Promise { const safeAddress = await this.protocolKit.getAddress() const threshold = await this.protocolKit.getThreshold() - // TODO: Using signSafeOperation with the API response is not working - // This should be investigated as the safeOperationHash we get in the Safe4337Pack - // seems to be different from the one we get from the API await this.apiKit.confirmSafeOperation( safeOperationHash, buildSignatureBytes([await this.protocolKit.signHash(safeOperationHash)]) @@ -97,7 +107,7 @@ export class SafeOperationClient { executable: confirmedSafeOperation }) - await this.#waitForOperationToFinish(userOperationHash) + await this.#waitForOperationToFinish({ userOperationHash }) return createSafeClientResult({ status: SafeClientTxStatus.SAFE_OPERATION_EXECUTED, @@ -118,12 +128,14 @@ export class SafeOperationClient { * Retrieves the pending Safe operations for the current Safe account * * @async + * @param {ListOptions} options The pagination options * @returns {Promise} A promise that resolves to an array of pending Safe operations. * @throws {Error} If there is an issue retrieving the safe address or pending Safe operations. */ - async getPendingSafeOperations(): Promise { + async getPendingSafeOperations(options?: ListOptions): Promise { const safeAddress = await this.protocolKit.getAddress() - return this.apiKit.getSafeOperationsByAddress({ safeAddress }) + + return this.apiKit.getSafeOperationsByAddress({ safeAddress, ...options }) } /** @@ -131,7 +143,11 @@ export class SafeOperationClient { * @param userOperationHash The userOperationHash to wait for. This comes from the bundler and can be obtained from the * SafeClientResult method under the safeOperations property */ - async #waitForOperationToFinish(userOperationHash: string): Promise { + async #waitForOperationToFinish({ + userOperationHash + }: { + userOperationHash: string + }): Promise { let userOperationReceipt = null while (!userOperationReceipt) { await new Promise((resolve) => setTimeout(resolve, 2000)) diff --git a/packages/safe-kit/src/extensions/safe-operations/safeOperations.test.ts b/packages/safe-kit/src/extensions/safe-operations/safeOperations.test.ts new file mode 100644 index 000000000..f57072da8 --- /dev/null +++ b/packages/safe-kit/src/extensions/safe-operations/safeOperations.test.ts @@ -0,0 +1,69 @@ +import Safe from '@safe-global/protocol-kit' +import SafeApiKit from '@safe-global/api-kit' +import { Safe4337Pack } from '@safe-global/relay-kit' + +import { safeOperations } from './safeOperations' +import { SafeClient } from '../../SafeClient' + +jest.mock('@safe-global/protocol-kit') +jest.mock('@safe-global/api-kit') +jest.mock('@safe-global/relay-kit') +jest.mock('../../utils', () => { + return { + ...jest.requireActual('../../utils'), + sendTransaction: jest.fn().mockResolvedValue('0xSafeDeploymentEthereumHash'), + proposeTransaction: jest.fn().mockResolvedValue('0xSafeTxHash'), + waitSafeTxReceipt: jest.fn() + } +}) + +const SAFE_PROVIDER = { + provider: 'http://ethereum.provider', + signer: '0xSignerAddress' +} + +describe('safeOperations', () => { + let protocolKit: Safe + let apiKit: jest.Mocked + let safeClient: SafeClient + + beforeEach(() => { + protocolKit = new Safe() + apiKit = new SafeApiKit({ chainId: 1n }) as jest.Mocked + safeClient = new SafeClient(protocolKit, apiKit) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it('should extend the SafeClient with the onChainMessages methods', async () => { + protocolKit.isSafeDeployed = jest.fn().mockResolvedValue(false) + protocolKit.getSafeProvider = jest.fn().mockResolvedValue(SAFE_PROVIDER) + protocolKit.getPredictedSafe = jest.fn().mockReturnValue({ + safeDeploymentConfig: {}, + safeAccountConfig: { + owners: ['0xOwnerAddress'], + threshold: 1 + } + }) + jest.spyOn(Safe4337Pack, 'init').mockResolvedValue({ protocolKit } as any) + + const safeOperationsClient = await safeClient.extend( + safeOperations({ bundlerUrl: 'http://bundler.url' }) + ) + + expect(Safe4337Pack.init).toHaveBeenCalledWith( + expect.objectContaining({ + bundlerUrl: 'http://bundler.url', + options: { + owners: ['0xOwnerAddress'], + threshold: 1 + } + }) + ) + expect(safeOperationsClient.sendSafeOperation).toBeDefined() + expect(safeOperationsClient.confirmSafeOperation).toBeDefined() + expect(safeOperationsClient.getPendingSafeOperations).toBeDefined() + }) +}) diff --git a/packages/safe-kit/src/extensions/safe-operations/safeOperations.ts b/packages/safe-kit/src/extensions/safe-operations/safeOperations.ts index d6450cf79..f5ec2983e 100644 --- a/packages/safe-kit/src/extensions/safe-operations/safeOperations.ts +++ b/packages/safe-kit/src/extensions/safe-operations/safeOperations.ts @@ -1,15 +1,15 @@ import { PredictedSafeProps } from '@safe-global/protocol-kit' -import { GetSafeOperationListResponse } from '@safe-global/api-kit' -import { - PaymasterOptions, - Safe4337Pack, - Safe4337CreateTransactionProps -} from '@safe-global/relay-kit' +import { GetSafeOperationListResponse, ListOptions } from '@safe-global/api-kit' +import { PaymasterOptions, Safe4337Pack } from '@safe-global/relay-kit' import { SafeClient } from '@safe-global/safe-kit/SafeClient' import { SafeOperationClient } from '@safe-global/safe-kit/extensions/safe-operations/SafeOperationClient' import { BundlerOptions } from '@safe-global/safe-kit/extensions/safe-operations/types' -import { SafeClientResult } from '@safe-global/safe-kit/types' +import { + ConfirmSafeOperationProps, + SafeClientResult, + SendSafeOperationProps +} from '@safe-global/safe-kit/types' /** * Extend the SafeClient with the ability to use a bundler and a paymaster @@ -21,8 +21,8 @@ import { SafeClientResult } from '@safe-global/safe-kit/types' * safeOperations({ ... }, { ... }) * ) * - * const { safeOperations } = await safeOperationClient.sendSafeOperation(...) - * await safeOperationClient.confirmSafeOperation(safeOperations?.safeOperationHash) + * const { safeOperations } = await safeOperationClient.sendSafeOperation({ transactions }) + * await safeOperationClient.confirmSafeOperation({ safeOperationHash: safeOperations?.safeOperationHash}) */ export function safeOperations( { bundlerUrl }: BundlerOptions, @@ -71,36 +71,30 @@ export function safeOperations( * - If the threshold = 1 the SafeOperation can be submitted to the bundler so it will execute it immediately * * @param {Safe4337CreateTransactionProps} props The Safe4337CreateTransactionProps object - * @param {SafeTransaction[]} props.transactions An array of transactions to be batched - * @param {TransactionOptions} [props.options] Optional transaction options * @returns {Promise} A promise that resolves with the status of the SafeOperation */ - async sendSafeOperation({ - transactions, - options = {} - }: Safe4337CreateTransactionProps): Promise { - return safeOperationClient.sendSafeOperation({ transactions, options }) + async sendSafeOperation(props: SendSafeOperationProps): Promise { + return safeOperationClient.sendSafeOperation(props) }, /** * Confirms the stored safeOperation * - * @param {string} safeOperationHash The hash of the safe operation to confirm. - * The safeOperationHash an be extracted the hash from the SafeClientResult of the sendSafeOperation method under the safeOperations property - * You must conformSafeOperation() with the other owners and once the threshold is reached the SafeOperation will be sent to the bundler + * @param {ConfirmSafeOperationProps} props The ConfirmSafeOperationProps object * @returns {Promise} A promise that resolves to the result of the safeOperation. */ - async confirmSafeOperation(safeOperationHash: string): Promise { - return safeOperationClient.confirmSafeOperation(safeOperationHash) + async confirmSafeOperation(props: ConfirmSafeOperationProps): Promise { + return safeOperationClient.confirmSafeOperation(props) }, /** * Retrieves the pending Safe operations for the current Safe account * * @async + * @param {ListOptions} options The pagination options * @returns {Promise} A promise that resolves to an array of pending Safe operations. * @throws {Error} If there is an issue retrieving the safe address or pending Safe operations. */ - async getPendingSafeOperations(): Promise { - return safeOperationClient.getPendingSafeOperations() + async getPendingSafeOperations(options?: ListOptions): Promise { + return safeOperationClient.getPendingSafeOperations(options) } } } diff --git a/packages/safe-kit/src/types.ts b/packages/safe-kit/src/types.ts index 584c7cb47..d69f87376 100644 --- a/packages/safe-kit/src/types.ts +++ b/packages/safe-kit/src/types.ts @@ -1,6 +1,45 @@ import { SafeProvider } from '@safe-global/protocol-kit' +import { + TransactionBase, + TransactionOptions, + EIP712TypedData, + MetaTransactionData +} from '@safe-global/safe-core-sdk-types' +import { IFeeEstimator } from '@safe-global/relay-kit' import { SafeClientTxStatus } from '@safe-global/safe-kit/constants' +export type SendTransactionProps = { + transactions: TransactionBase[] +} & TransactionOptions + +export type ConfirmTransactionProps = { + safeTxHash: string +} + +export type SendOnChainMessageProps = { + message: string | EIP712TypedData +} & TransactionOptions + +export type SendOffChainMessageProps = { + message: string | EIP712TypedData +} + +export type ConfirmOffChainMessageProps = { + messageHash: string +} + +export type SendSafeOperationProps = { + transactions: MetaTransactionData[] + amountToApprove?: bigint + validUntil?: number + validAfter?: number + feeEstimator?: IFeeEstimator +} + +export type ConfirmSafeOperationProps = { + safeOperationHash: string +} + export type SafeConfig = { owners: string[] threshold: number diff --git a/packages/safe-kit/src/utils/proposeTransaction.ts b/packages/safe-kit/src/utils/proposeTransaction.ts index d738420d1..8d58abb12 100644 --- a/packages/safe-kit/src/utils/proposeTransaction.ts +++ b/packages/safe-kit/src/utils/proposeTransaction.ts @@ -10,11 +10,15 @@ import { SafeTransaction } from '@safe-global/safe-core-sdk-types' * @param {SafeApiKit} apiKit The SafeApiKit instance * @returns The Safe transaction hash */ -export const proposeTransaction = async ( - safeTransaction: SafeTransaction, - protocolKit: Safe, +export const proposeTransaction = async ({ + safeTransaction, + protocolKit, + apiKit +}: { + safeTransaction: SafeTransaction + protocolKit: Safe apiKit: SafeApiKit -): Promise => { +}): Promise => { safeTransaction = await protocolKit.signTransaction(safeTransaction) const signerAddress = (await protocolKit.getSafeProvider().getSignerAddress()) || '0x' diff --git a/packages/safe-kit/src/utils/sendTransaction.ts b/packages/safe-kit/src/utils/sendTransaction.ts index ed1ff1be2..7e540802c 100644 --- a/packages/safe-kit/src/utils/sendTransaction.ts +++ b/packages/safe-kit/src/utils/sendTransaction.ts @@ -1,28 +1,28 @@ import Safe from '@safe-global/protocol-kit' -import { TransactionBase, TransactionOptions } from '@safe-global/safe-core-sdk-types' +import { Transaction } from '@safe-global/safe-core-sdk-types' import { AbstractSigner } from 'ethers' /** * Sends a transaction using the signer (owner) * It's useful to deploy Safe accounts * - * @param {TransactionBase} transaction The transaction. - * @param {TransactionOptions} options Options for executing the transaction. + * @param {Transaction} transaction The transaction. * @param {Safe} protocolKit The protocolKit instance * @returns {Promise} A promise that resolves with the transaction hash */ -export const sendTransaction = async ( - transaction: TransactionBase, - options: TransactionOptions, +export const sendTransaction = async ({ + transaction, + protocolKit +}: { + transaction: Transaction protocolKit: Safe -): Promise => { +}): Promise => { const signer = (await protocolKit .getSafeProvider() .getExternalSigner()) as unknown as AbstractSigner const txResponsePromise = await signer.sendTransaction({ from: (await protocolKit.getSafeProvider().getSignerAddress()) || '0x', - ...transaction, - ...options + ...transaction }) const txResponse = await txResponsePromise.wait() diff --git a/playground/safe-kit/send-off-chain-message.ts b/playground/safe-kit/send-off-chain-message.ts index 0ed7e655e..699115369 100644 --- a/playground/safe-kit/send-off-chain-message.ts +++ b/playground/safe-kit/send-off-chain-message.ts @@ -83,7 +83,7 @@ async function send(): Promise { ) console.log('-Signer Address:', signerAddress) - const txResult = await safeClientWithMessages.sendOffChainMessage(MESSAGE) + const txResult = await safeClientWithMessages.sendOffChainMessage({ message: MESSAGE }) console.log('-Send result: ', txResult) @@ -109,15 +109,17 @@ async function confirm({ safeAddress, messages }: SafeClientResult, pk: string) const pendingMessages = await safeClientWithMessages.getPendingOffChainMessages() - pendingMessages.results.forEach(async (message) => { + for (const message of pendingMessages.results) { if (message.messageHash !== messages?.messageHash) { return } - const txResult = await safeClientWithMessages.confirmOffChainMessage(message.messageHash) + const txResult = await safeClientWithMessages.confirmOffChainMessage({ + messageHash: message.messageHash + }) console.log('-Confirm result: ', txResult) - }) + } } async function main() { diff --git a/playground/safe-kit/send-on-chain-message.ts b/playground/safe-kit/send-on-chain-message.ts index 98fbfb382..8f8f0f79b 100644 --- a/playground/safe-kit/send-on-chain-message.ts +++ b/playground/safe-kit/send-on-chain-message.ts @@ -82,7 +82,7 @@ async function send(): Promise { ) console.log('-Signer Address:', signerAddress) - const txResult = await safeClientWithMessages.sendOnChainMessage(MESSAGE) + const txResult = await safeClientWithMessages.sendOnChainMessage({ message: MESSAGE }) console.log('-Send result: ', txResult) @@ -106,15 +106,15 @@ async function confirm({ safeAddress, transactions }: SafeClientResult, pk: stri const pendingTransactions = await safeClient.getPendingTransactions() - pendingTransactions.results.forEach(async (transaction) => { + for (const transaction of pendingTransactions.results) { if (transaction.safeTxHash !== transactions?.safeTxHash) { return } - const txResult = await safeClient.confirm(transaction.safeTxHash) + const txResult = await safeClient.confirm({ safeTxHash: transaction.safeTxHash }) console.log('-Confirm result: ', txResult) - }) + } } async function main() { diff --git a/playground/safe-kit/send-safe-operation.ts b/playground/safe-kit/send-safe-operation.ts index 45b6a6fe8..c728fcef7 100644 --- a/playground/safe-kit/send-safe-operation.ts +++ b/playground/safe-kit/send-safe-operation.ts @@ -17,9 +17,6 @@ const RPC_URL = 'https://sepolia.gateway.tenderly.co' const usdcTokenAddress = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238' // SEPOLIA const usdcAmount = 10_000n // 0.01 USDC -// PAYMASTER ADDRESS -const paymasterAddress = '0x0000000000325602a77416A16136FDafd04b299f' // SEPOLIA - // Paymaster URL const PIMLICO_API_KEY = '' const PAYMASTER_URL = `https://api.pimlico.io/v2/sepolia/rpc?apikey=${PIMLICO_API_KEY}` // PIMLICO @@ -37,10 +34,7 @@ async function send(): Promise { }) const safeClientWithSafeOperation = await safeClient.extend( - safeOperations( - { bundlerUrl: BUNDLER_URL }, - { isSponsored: true, paymasterAddress, paymasterUrl: PAYMASTER_URL } - ) + safeOperations({ bundlerUrl: BUNDLER_URL }, { isSponsored: true, paymasterUrl: PAYMASTER_URL }) ) const signerAddress = @@ -65,10 +59,8 @@ async function send(): Promise { const safeOperationResult = await safeClientWithSafeOperation.sendSafeOperation({ transactions, - options: { - validAfter: timestamp - 60_000, - validUntil: timestamp + 60_000 - } + validAfter: timestamp - 60_000, + validUntil: timestamp + 60_000 }) console.log('-Send result: ', safeOperationResult) @@ -96,25 +88,22 @@ async function confirm(safeClientResult: SafeClientResult, pk: string) { console.log('-Signer Address:', signerAddress) const safeClientWithSafeOperation = await safeClient.extend( - safeOperations( - { bundlerUrl: BUNDLER_URL }, - { isSponsored: true, paymasterAddress, paymasterUrl: PAYMASTER_URL } - ) + safeOperations({ bundlerUrl: BUNDLER_URL }, { isSponsored: true, paymasterUrl: PAYMASTER_URL }) ) const pendingSafeOperations = await safeClientWithSafeOperation.getPendingSafeOperations() - pendingSafeOperations.results.forEach(async (safeOperation) => { + for (const safeOperation of pendingSafeOperations.results) { if (safeOperation.safeOperationHash !== safeClientResult.safeOperations?.safeOperationHash) { return } - const safeOperationResult = await safeClientWithSafeOperation.confirmSafeOperation( - safeClientResult.safeOperations?.safeOperationHash - ) + const safeOperationResult = await safeClientWithSafeOperation.confirmSafeOperation({ + safeOperationHash: safeClientResult.safeOperations?.safeOperationHash + }) console.log('-Confirm result: ', safeOperationResult) - }) + } } async function main() { diff --git a/playground/safe-kit/send-transactions.ts b/playground/safe-kit/send-transactions.ts index b260ee974..b95715c08 100644 --- a/playground/safe-kit/send-transactions.ts +++ b/playground/safe-kit/send-transactions.ts @@ -43,7 +43,7 @@ async function send(): Promise { } const transactions = [transferUSDC, transferUSDC] - const txResult = await safeClient.send(transactions) + const txResult = await safeClient.send({ transactions }) console.log('-Send result: ', txResult) @@ -67,15 +67,15 @@ async function confirm({ safeAddress, transactions }: SafeClientResult, pk: stri const pendingTransactions = await safeClient.getPendingTransactions() - pendingTransactions.results.forEach(async (transaction) => { + for (const transaction of pendingTransactions.results) { if (transaction.safeTxHash !== transactions?.safeTxHash) { return } - const txResult = await safeClient.confirm(transaction.safeTxHash) + const txResult = await safeClient.confirm({ safeTxHash: transaction.safeTxHash }) console.log('-Confirm result: ', txResult) - }) + } } async function main() {