From 62a120a7f5cbeec508de887235e033f2499993b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Tue, 20 Aug 2024 13:43:34 +0200 Subject: [PATCH 1/3] Export owner management methods --- packages/protocol-kit/src/Safe.ts | 8 + packages/sdk-starter-kit/src/BaseClient.ts | 262 +++++++++++++ packages/sdk-starter-kit/src/SafeClient.ts | 344 ++++++------------ packages/sdk-starter-kit/src/types.ts | 4 + playground/config/run.ts | 3 +- .../sdk-starter-kit/owner-management.ts | 76 ++++ 6 files changed, 461 insertions(+), 236 deletions(-) create mode 100644 packages/sdk-starter-kit/src/BaseClient.ts create mode 100644 playground/sdk-starter-kit/owner-management.ts diff --git a/packages/protocol-kit/src/Safe.ts b/packages/protocol-kit/src/Safe.ts index 52fe2d961..58ae0e6ee 100644 --- a/packages/protocol-kit/src/Safe.ts +++ b/packages/protocol-kit/src/Safe.ts @@ -275,6 +275,14 @@ class Safe { return this.#safeProvider } + /** + * Returns the OwnerManager + * @returns {OwnerManager} The current OwnerManager + */ + getOwnerManager(): OwnerManager { + return this.#ownerManager + } + /** * Returns the address of the MultiSend contract. * diff --git a/packages/sdk-starter-kit/src/BaseClient.ts b/packages/sdk-starter-kit/src/BaseClient.ts new file mode 100644 index 000000000..9331f1e7b --- /dev/null +++ b/packages/sdk-starter-kit/src/BaseClient.ts @@ -0,0 +1,262 @@ +import Safe from '@safe-global/protocol-kit' +import SafeApiKit, { SafeMultisigTransactionListResponse } from '@safe-global/api-kit' +import { + SafeTransaction, + TransactionOptions, + TransactionResult, + Transaction +} from '@safe-global/safe-core-sdk-types' + +import { + createSafeClientResult, + sendTransaction, + proposeTransaction, + waitSafeTxReceipt +} from '@safe-global/sdk-starter-kit/utils' +import { SafeClientTxStatus } from '@safe-global/sdk-starter-kit/constants' +import { + ConfirmTransactionProps, + SafeClientResult, + SendTransactionProps +} from '@safe-global/sdk-starter-kit/types' + +/** + * @class + * This class provides the core functionality to create, sign and execute transactions. + * It also provides the ability to be extended with features through the extend function. + * + * @example + * const safeClient = await createSafeClient({ ... }) + * + * const { transactions } = await safeClient.send(...) + * await safeClient.confirm(transactions?.safeTxHash) + */ +export class BaseClient { + protocolKit: Safe + apiKit: SafeApiKit + + constructor(protocolKit: Safe, apiKit: SafeApiKit) { + this.protocolKit = protocolKit + this.apiKit = apiKit + } + + /** + * 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 {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, + ...transactionOptions + }: SendTransactionProps): Promise { + const isSafeDeployed = await this.protocolKit.isSafeDeployed() + const isMultisigSafe = (await this.protocolKit.getThreshold()) > 1 + + const safeTransaction = await this.protocolKit.createTransaction({ transactions }) + + if (isSafeDeployed) { + if (isMultisigSafe) { + // If the threshold is greater than 1, we need to propose the transaction first + return this.#proposeTransaction({ safeTransaction }) + } else { + // If the threshold is 1, we can execute the transaction + 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, ...transactionOptions }) + } else { + // If the threshold is 1, we can deploy the Safe account and execute the transaction in one step + return this.#deployAndExecuteTransaction({ safeTransaction, ...transactionOptions }) + } + } + } + + /** + * Confirms a transaction by its safe transaction hash. + * + * @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 }: ConfirmTransactionProps): Promise { + let transactionResponse = await this.apiKit.getTransaction(safeTxHash) + const safeAddress = await this.protocolKit.getAddress() + const signedTransaction = await this.protocolKit.signTransaction(transactionResponse) + + await this.apiKit.confirmTransaction(safeTxHash, signedTransaction.encodedSignatures()) + + transactionResponse = await this.apiKit.getTransaction(safeTxHash) + + if ( + transactionResponse.confirmations && + transactionResponse.confirmationsRequired === transactionResponse.confirmations.length + ) { + const executedTransactionResponse: TransactionResult = + await this.protocolKit.executeTransaction(transactionResponse) + + await waitSafeTxReceipt(executedTransactionResponse) + + return createSafeClientResult({ + status: SafeClientTxStatus.EXECUTED, + safeAddress, + txHash: executedTransactionResponse.hash, + safeTxHash + }) + } + + return createSafeClientResult({ + status: SafeClientTxStatus.PENDING_SIGNATURES, + safeAddress, + safeTxHash + }) + } + + /** + * Retrieves the pending transactions for the current safe address. + * + * @async + * @returns {Promise} A promise that resolves to an array of pending transactions. + * @throws {Error} If there is an issue retrieving the safe address or pending transactions. + */ + async getPendingTransactions(): Promise { + const safeAddress = await this.protocolKit.getAddress() + + return this.apiKit.getPendingTransactions(safeAddress) + } + + /** + * Deploys and executes a transaction in one step. + * + * @param {SafeTransaction} safeTransaction The safe transaction to be executed + * @param {TransactionOptions} options Optional transaction options + * @returns A promise that resolves to the result of the transaction + */ + async #deployAndExecuteTransaction({ + safeTransaction, + ...transactionOptions + }: { safeTransaction: SafeTransaction } & TransactionOptions): Promise { + safeTransaction = await this.protocolKit.signTransaction(safeTransaction) + + const transactionBatchWithDeployment = + await this.protocolKit.wrapSafeTransactionIntoDeploymentBatch( + safeTransaction, + transactionOptions + ) + const hash = await sendTransaction({ + transaction: transactionBatchWithDeployment, + protocolKit: this.protocolKit + }) + + await this.#reconnectSafe() + + return createSafeClientResult({ + safeAddress: await this.protocolKit.getAddress(), + status: SafeClientTxStatus.DEPLOYED_AND_EXECUTED, + deploymentTxHash: hash, + txHash: hash + }) + } + + /** + * Deploys and proposes a transaction in one step. + * + * @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, + ...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, + protocolKit: this.protocolKit, + apiKit: this.apiKit + }) + + return createSafeClientResult({ + safeAddress: await this.protocolKit.getAddress(), + status: SafeClientTxStatus.DEPLOYED_AND_PENDING_SIGNATURES, + deploymentTxHash: hash, + safeTxHash + }) + } + + /** + * Executes a transaction. + * + * @param {SafeTransaction} safeTransaction The safe transaction to be executed + * @param {TransactionOptions} transactionOptions Optional transaction options + * @returns A promise that resolves to the result of the transaction + */ + async #executeTransaction({ + safeTransaction, + ...transactionOptions + }: { safeTransaction: SafeTransaction } & TransactionOptions): Promise { + safeTransaction = await this.protocolKit.signTransaction(safeTransaction) + + const { hash } = await this.protocolKit.executeTransaction(safeTransaction, transactionOptions) + + return createSafeClientResult({ + safeAddress: await this.protocolKit.getAddress(), + status: SafeClientTxStatus.EXECUTED, + txHash: hash + }) + } + + /** + * 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(), + status: SafeClientTxStatus.PENDING_SIGNATURES, + safeTxHash + }) + } + + async #reconnectSafe(): Promise { + this.protocolKit = await this.protocolKit.connect({ + provider: this.protocolKit.getSafeProvider().provider, + signer: this.protocolKit.getSafeProvider().signer, + safeAddress: await this.protocolKit.getAddress() + }) + } +} diff --git a/packages/sdk-starter-kit/src/SafeClient.ts b/packages/sdk-starter-kit/src/SafeClient.ts index 83a8fc6d4..7459fad37 100644 --- a/packages/sdk-starter-kit/src/SafeClient.ts +++ b/packages/sdk-starter-kit/src/SafeClient.ts @@ -1,283 +1,157 @@ -import Safe from '@safe-global/protocol-kit' -import SafeApiKit, { SafeMultisigTransactionListResponse } from '@safe-global/api-kit' -import { - SafeTransaction, - TransactionOptions, - TransactionResult, - Transaction -} from '@safe-global/safe-core-sdk-types' - -import { - createSafeClientResult, - sendTransaction, - proposeTransaction, - waitSafeTxReceipt -} from '@safe-global/sdk-starter-kit/utils' -import { SafeClientTxStatus } from '@safe-global/sdk-starter-kit/constants' -import { - ConfirmTransactionProps, - SafeClientResult, - SendTransactionProps -} from '@safe-global/sdk-starter-kit/types' - -/** - * @class - * This class provides the core functionality to create, sign and execute transactions. - * It also provides the ability to be extended with features through the extend function. - * - * @example - * const safeClient = await createSafeClient({ ... }) - * - * const { transactions } = await safeClient.send(...) - * await safeClient.confirm(transactions?.safeTxHash) - */ -export class SafeClient { - protocolKit: Safe - apiKit: SafeApiKit - +import Safe, { + AddOwnerTxParams, + RemoveOwnerTxParams, + SwapOwnerTxParams +} from '@safe-global/protocol-kit' +import { BaseClient } from './BaseClient' +import SafeApiKit from 'packages/api-kit/dist/src' +import { ChangeThresholdTxParams } from './types' +import { TransactionBase } from 'packages/safe-core-sdk-types/dist/src' + +export class SafeClient extends BaseClient { constructor(protocolKit: Safe, apiKit: SafeApiKit) { - this.protocolKit = protocolKit - this.apiKit = apiKit + super(protocolKit, apiKit) } /** - * Sends transactions through the Safe protocol. - * You can send an array to transactions { to, value, data} that we will convert to a transaction batch + * Checks if a specific address is an owner of the current Safe. * - * @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. + * @param {string} ownerAddress - The account address + * @returns {boolean} TRUE if the account is an owner */ - async send({ - transactions, - ...transactionOptions - }: SendTransactionProps): Promise { - const isSafeDeployed = await this.protocolKit.isSafeDeployed() - const isMultisigSafe = (await this.protocolKit.getThreshold()) > 1 - - const safeTransaction = await this.protocolKit.createTransaction({ transactions }) - - if (isSafeDeployed) { - if (isMultisigSafe) { - // If the threshold is greater than 1, we need to propose the transaction first - return this.#proposeTransaction({ safeTransaction }) - } else { - // If the threshold is 1, we can execute the transaction - 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, ...transactionOptions }) - } else { - // If the threshold is 1, we can deploy the Safe account and execute the transaction in one step - return this.#deployAndExecuteTransaction({ safeTransaction, ...transactionOptions }) - } - } + async isOwner(ownerAddress: string): Promise { + return this.protocolKit.isOwner(ownerAddress) } /** - * Confirms a transaction by its safe transaction hash. + * Returns the list of Safe owner accounts. * - * @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. + * @returns The list of owners */ - 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) - - await this.apiKit.confirmTransaction(safeTxHash, signedTransaction.encodedSignatures()) - - transactionResponse = await this.apiKit.getTransaction(safeTxHash) - - if ( - transactionResponse.confirmations && - transactionResponse.confirmationsRequired === transactionResponse.confirmations.length - ) { - const executedTransactionResponse: TransactionResult = - await this.protocolKit.executeTransaction(transactionResponse) - - await waitSafeTxReceipt(executedTransactionResponse) - - return createSafeClientResult({ - status: SafeClientTxStatus.EXECUTED, - safeAddress, - txHash: executedTransactionResponse.hash, - safeTxHash - }) - } - - return createSafeClientResult({ - status: SafeClientTxStatus.PENDING_SIGNATURES, - safeAddress, - safeTxHash - }) + async getOwners(): Promise { + return this.protocolKit.getOwners() } /** - * Retrieves the pending transactions for the current safe address. + * Returns the Safe threshold. * - * @async - * @returns {Promise} A promise that resolves to an array of pending transactions. - * @throws {Error} If there is an issue retrieving the safe address or pending transactions. + * @returns {number} The Safe threshold */ - async getPendingTransactions(): Promise { - const safeAddress = await this.protocolKit.getAddress() - - return this.apiKit.getPendingTransactions(safeAddress) + async getThreshold(): Promise { + return this.protocolKit.getThreshold() } /** - * Extend the SafeClient with additional functionality. + * Returns the Safe nonce. * - * @param extendFunc - * @returns + * @returns {number} The Safe nonce */ - extend(extendFunc: (client: SafeClient) => Promise): Promise - extend(extendFunc: (client: SafeClient) => T): SafeClient & T - - extend( - extendFunc: (client: SafeClient) => T | Promise - ): (SafeClient & T) | Promise { - const result = extendFunc(this) - - if (result instanceof Promise) { - return result.then((extensions) => Object.assign(this, extensions) as SafeClient & T) - } else { - return Object.assign(this, result) as SafeClient & T - } + async getNonce(): Promise { + return this.protocolKit.getNonce() } /** - * Deploys and executes a transaction in one step. + * Returns a list of owners who have approved a specific Safe transaction. * - * @param {SafeTransaction} safeTransaction The safe transaction to be executed - * @param {TransactionOptions} options Optional transaction options - * @returns A promise that resolves to the result of the transaction + * @param {string} txHash - The Safe transaction hash + * @returns {string[]} The list of owners */ - async #deployAndExecuteTransaction({ - safeTransaction, - ...transactionOptions - }: { safeTransaction: SafeTransaction } & TransactionOptions): Promise { - safeTransaction = await this.protocolKit.signTransaction(safeTransaction) - - const transactionBatchWithDeployment = - await this.protocolKit.wrapSafeTransactionIntoDeploymentBatch( - safeTransaction, - transactionOptions - ) - const hash = await sendTransaction({ - transaction: transactionBatchWithDeployment, - protocolKit: this.protocolKit - }) - - await this.#reconnectSafe() - - return createSafeClientResult({ - safeAddress: await this.protocolKit.getAddress(), - status: SafeClientTxStatus.DEPLOYED_AND_EXECUTED, - deploymentTxHash: hash, - txHash: hash - }) + async getOwnersWhoApprovedTx(txHash: string): Promise { + return this.protocolKit.getOwnersWhoApprovedTx(txHash) } /** - * Deploys and proposes a transaction in one step. + * Encodes the data for adding a new owner to the Safe. * - * @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 + * @param {AddOwnerTxParams} params - The parameters for adding a new owner + * @param {string} params.ownerAddress - The address of the owner to add + * @param {number} params.threshold - The threshold of the Safe + * @returns {TransactionBase} The encoded data */ - 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() + async createAddOwnerTx({ ownerAddress, threshold }: AddOwnerTxParams): Promise { + const ownerManager = this.protocolKit.getOwnerManager() - safeTransaction = await this.protocolKit.signTransaction(safeTransaction) - const safeTxHash = await proposeTransaction({ - safeTransaction, - protocolKit: this.protocolKit, - apiKit: this.apiKit - }) + return { + to: await this.protocolKit.getAddress(), + value: '0', + data: await ownerManager.encodeAddOwnerWithThresholdData(ownerAddress, threshold) + } + } - return createSafeClientResult({ - safeAddress: await this.protocolKit.getAddress(), - status: SafeClientTxStatus.DEPLOYED_AND_PENDING_SIGNATURES, - deploymentTxHash: hash, - safeTxHash - }) + /** + * Encodes the data for removing an owner from the Safe. + * @param {RemoveOwnerTxParams} params - The parameters for removing an owner + * @param {string} params.ownerAddress - The address of the owner to remove + * @param {number} params.threshold - The threshold of the Safe + * @returns {TransactionBase} The encoded data + */ + async createRemoveOwnerTx({ + ownerAddress, + threshold + }: RemoveOwnerTxParams): Promise { + const ownerManager = this.protocolKit.getOwnerManager() + return { + to: await this.protocolKit.getAddress(), + value: '0', + data: await ownerManager.encodeRemoveOwnerData(ownerAddress, threshold) + } } /** - * Executes a transaction. + * Encodes the data for swapping an owner in the Safe. * - * @param {SafeTransaction} safeTransaction The safe transaction to be executed - * @param {TransactionOptions} transactionOptions Optional transaction options - * @returns A promise that resolves to the result of the transaction + * @param {SwapOwnerTxParams} params - The parameters for swapping an owner + * @param {string} params.oldOwnerAddress - The address of the old owner + * @param {string} params.newOwnerAddress - The address of the new owner + * @returns {TransactionBase} The encoded data */ - async #executeTransaction({ - safeTransaction, - ...transactionOptions - }: { safeTransaction: SafeTransaction } & TransactionOptions): Promise { - safeTransaction = await this.protocolKit.signTransaction(safeTransaction) + async createSwapOwnerTx({ + oldOwnerAddress, + newOwnerAddress + }: SwapOwnerTxParams): Promise { + const ownerManager = this.protocolKit.getOwnerManager() + + return { + to: await this.protocolKit.getAddress(), + value: '0', + data: await ownerManager.encodeSwapOwnerData(oldOwnerAddress, newOwnerAddress) + } + } - const { hash } = await this.protocolKit.executeTransaction(safeTransaction, transactionOptions) + /** + * + * @param {ChangeThresholdTxParams} params - The parameters for changing the Safe threshold + * @param {number} params.threshold - The new threshold + * @returns {TransactionBase} The encoded data + */ + async createChangeThresholdTx({ threshold }: ChangeThresholdTxParams): Promise { + const ownerManager = this.protocolKit.getOwnerManager() - return createSafeClientResult({ - safeAddress: await this.protocolKit.getAddress(), - status: SafeClientTxStatus.EXECUTED, - txHash: hash - }) + return { + to: await this.protocolKit.getAddress(), + value: '0', + data: await ownerManager.encodeChangeThresholdData(threshold) + } } /** - * Proposes a transaction to the Safe. - * @param { SafeTransaction } safeTransaction The safe transaction to propose - * @returns The SafeClientResult + * Extend the SafeClient with additional functionality. + * + * @param extendFunc + * @returns */ - async #proposeTransaction({ safeTransaction }: { safeTransaction: SafeTransaction }) { - const safeTxHash = await proposeTransaction({ - safeTransaction, - protocolKit: this.protocolKit, - apiKit: this.apiKit - }) + extend(extendFunc: (client: SafeClient) => Promise): Promise + extend(extendFunc: (client: SafeClient) => T): BaseClient & T - return createSafeClientResult({ - safeAddress: await this.protocolKit.getAddress(), - status: SafeClientTxStatus.PENDING_SIGNATURES, - safeTxHash - }) - } + extend( + extendFunc: (client: SafeClient) => T | Promise + ): (SafeClient & T) | Promise { + const result = extendFunc(this) - async #reconnectSafe(): Promise { - this.protocolKit = await this.protocolKit.connect({ - provider: this.protocolKit.getSafeProvider().provider, - signer: this.protocolKit.getSafeProvider().signer, - safeAddress: await this.protocolKit.getAddress() - }) + if (result instanceof Promise) { + return result.then((extensions) => Object.assign(this, extensions) as SafeClient & T) + } else { + return Object.assign(this, result) as SafeClient & T + } } } diff --git a/packages/sdk-starter-kit/src/types.ts b/packages/sdk-starter-kit/src/types.ts index f3ceab375..5021ed852 100644 --- a/packages/sdk-starter-kit/src/types.ts +++ b/packages/sdk-starter-kit/src/types.ts @@ -83,3 +83,7 @@ export type SafeClientResult = { ethereumTxHash?: string } } + +export type ChangeThresholdTxParams = { + threshold: number +} diff --git a/playground/config/run.ts b/playground/config/run.ts index 5b8517ba6..749a724b1 100644 --- a/playground/config/run.ts +++ b/playground/config/run.ts @@ -32,7 +32,8 @@ const playgroundStarterKitPaths = { 'send-transactions': 'sdk-starter-kit/send-transactions', 'send-on-chain-message': 'sdk-starter-kit/send-on-chain-message', 'send-off-chain-message': 'sdk-starter-kit/send-off-chain-message', - 'send-safe-operation': 'sdk-starter-kit/send-safe-operation' + 'send-safe-operation': 'sdk-starter-kit/send-safe-operation', + 'owner-management': 'sdk-starter-kit/owner-management' } const path = diff --git a/playground/sdk-starter-kit/owner-management.ts b/playground/sdk-starter-kit/owner-management.ts new file mode 100644 index 000000000..73c0e77b3 --- /dev/null +++ b/playground/sdk-starter-kit/owner-management.ts @@ -0,0 +1,76 @@ +import { createSafeClient } from '@safe-global/sdk-starter-kit' + +const OWNER_1_PRIVATE_KEY = '' +const OWNER_2_PRIVATE_KEY = '' +const OWNER_2_ADDRESS = '' + +const RPC_URL = 'https://sepolia.gateway.tenderly.co' +const SAFE_ADDRESS = '' + +async function addOwner() { + const safeClient = await createSafeClient({ + provider: RPC_URL, + signer: OWNER_1_PRIVATE_KEY, + safeAddress: SAFE_ADDRESS + }) + + const transaction = await safeClient.createAddOwnerTx({ + ownerAddress: OWNER_2_ADDRESS, + threshold: 2 + }) + + const transactionResult = await safeClient.send({ transactions: [transaction] }) + + console.log('Add Owner Transaction Result', transactionResult) +} + +async function removeOwner() { + const safeClient1 = await createSafeClient({ + provider: RPC_URL, + signer: OWNER_1_PRIVATE_KEY, + safeAddress: SAFE_ADDRESS + }) + + const safeClient2 = await createSafeClient({ + provider: RPC_URL, + signer: OWNER_2_PRIVATE_KEY, + safeAddress: SAFE_ADDRESS + }) + + const transaction = await safeClient1.createRemoveOwnerTx({ + ownerAddress: OWNER_2_ADDRESS, + threshold: 1 + }) + const sendResult = await safeClient1.send({ transactions: [transaction] }) + + const transactionResult = await safeClient2.confirm({ + safeTxHash: sendResult.transactions?.safeTxHash || '' + }) + console.log('Remove Owner Transaction Result', transactionResult) +} + +async function safeInfo() { + const safeClient = await createSafeClient({ + provider: RPC_URL, + signer: OWNER_1_PRIVATE_KEY, + safeAddress: SAFE_ADDRESS + }) + + console.log('Safe Address', await safeClient.protocolKit.getAddress()) + console.log('Owners', await safeClient.getOwners()) + console.log('Threshold', await safeClient.getThreshold()) + console.log('Nonce', await safeClient.getNonce()) +} + +async function main() { + await safeInfo() + await addOwner() + + console.log('Waiting for transaction to be indexed ...') + setTimeout(async () => { + await safeInfo() + await removeOwner() + }, 10000) +} + +main() From ed10d7c09d89f646c2dd71c9ba49b78230b40593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Tue, 20 Aug 2024 16:13:05 +0200 Subject: [PATCH 2/3] Swap BaseClient and SafeClient methods --- packages/sdk-starter-kit/src/BaseClient.ts | 317 ++++++----------- packages/sdk-starter-kit/src/SafeClient.ts | 334 ++++++++++++------ .../sdk-starter-kit/owner-management.ts | 4 +- 3 files changed, 338 insertions(+), 317 deletions(-) diff --git a/packages/sdk-starter-kit/src/BaseClient.ts b/packages/sdk-starter-kit/src/BaseClient.ts index 9331f1e7b..55448de74 100644 --- a/packages/sdk-starter-kit/src/BaseClient.ts +++ b/packages/sdk-starter-kit/src/BaseClient.ts @@ -1,36 +1,13 @@ -import Safe from '@safe-global/protocol-kit' -import SafeApiKit, { SafeMultisigTransactionListResponse } from '@safe-global/api-kit' -import { - SafeTransaction, - TransactionOptions, - TransactionResult, - Transaction -} from '@safe-global/safe-core-sdk-types' +import Safe, { + AddOwnerTxParams, + RemoveOwnerTxParams, + SwapOwnerTxParams +} from '@safe-global/protocol-kit' +import SafeApiKit from '@safe-global/api-kit' +import { TransactionBase } from '@safe-global/safe-core-sdk-types' -import { - createSafeClientResult, - sendTransaction, - proposeTransaction, - waitSafeTxReceipt -} from '@safe-global/sdk-starter-kit/utils' -import { SafeClientTxStatus } from '@safe-global/sdk-starter-kit/constants' -import { - ConfirmTransactionProps, - SafeClientResult, - SendTransactionProps -} from '@safe-global/sdk-starter-kit/types' +import { ChangeThresholdTxParams } from './types' -/** - * @class - * This class provides the core functionality to create, sign and execute transactions. - * It also provides the ability to be extended with features through the extend function. - * - * @example - * const safeClient = await createSafeClient({ ... }) - * - * const { transactions } = await safeClient.send(...) - * await safeClient.confirm(transactions?.safeTxHash) - */ export class BaseClient { protocolKit: Safe apiKit: SafeApiKit @@ -41,222 +18,142 @@ export class BaseClient { } /** - * 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 {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. + * Returns the Safe address. + * @returns {string} The Safe address */ - async send({ - transactions, - ...transactionOptions - }: SendTransactionProps): Promise { - const isSafeDeployed = await this.protocolKit.isSafeDeployed() - const isMultisigSafe = (await this.protocolKit.getThreshold()) > 1 - - const safeTransaction = await this.protocolKit.createTransaction({ transactions }) - - if (isSafeDeployed) { - if (isMultisigSafe) { - // If the threshold is greater than 1, we need to propose the transaction first - return this.#proposeTransaction({ safeTransaction }) - } else { - // If the threshold is 1, we can execute the transaction - 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, ...transactionOptions }) - } else { - // If the threshold is 1, we can deploy the Safe account and execute the transaction in one step - return this.#deployAndExecuteTransaction({ safeTransaction, ...transactionOptions }) - } - } + async getAddress(): Promise { + return this.protocolKit.getAddress() } /** - * Confirms a transaction by its safe transaction hash. + * Checks if the current Safe is deployed. * - * @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. + * @returns {boolean} if the Safe contract is deployed */ - 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) - - await this.apiKit.confirmTransaction(safeTxHash, signedTransaction.encodedSignatures()) - - transactionResponse = await this.apiKit.getTransaction(safeTxHash) - - if ( - transactionResponse.confirmations && - transactionResponse.confirmationsRequired === transactionResponse.confirmations.length - ) { - const executedTransactionResponse: TransactionResult = - await this.protocolKit.executeTransaction(transactionResponse) - - await waitSafeTxReceipt(executedTransactionResponse) - - return createSafeClientResult({ - status: SafeClientTxStatus.EXECUTED, - safeAddress, - txHash: executedTransactionResponse.hash, - safeTxHash - }) - } - - return createSafeClientResult({ - status: SafeClientTxStatus.PENDING_SIGNATURES, - safeAddress, - safeTxHash - }) + async isDeployed(): Promise { + return this.protocolKit.isSafeDeployed() } /** - * Retrieves the pending transactions for the current safe address. + * Checks if a specific address is an owner of the current Safe. * - * @async - * @returns {Promise} A promise that resolves to an array of pending transactions. - * @throws {Error} If there is an issue retrieving the safe address or pending transactions. + * @param {string} ownerAddress - The account address + * @returns {boolean} TRUE if the account is an owner */ - async getPendingTransactions(): Promise { - const safeAddress = await this.protocolKit.getAddress() - - return this.apiKit.getPendingTransactions(safeAddress) + async isOwner(ownerAddress: string): Promise { + return this.protocolKit.isOwner(ownerAddress) } /** - * Deploys and executes a transaction in one step. + * Returns the list of Safe owner accounts. * - * @param {SafeTransaction} safeTransaction The safe transaction to be executed - * @param {TransactionOptions} options Optional transaction options - * @returns A promise that resolves to the result of the transaction + * @returns The list of owners */ - async #deployAndExecuteTransaction({ - safeTransaction, - ...transactionOptions - }: { safeTransaction: SafeTransaction } & TransactionOptions): Promise { - safeTransaction = await this.protocolKit.signTransaction(safeTransaction) - - const transactionBatchWithDeployment = - await this.protocolKit.wrapSafeTransactionIntoDeploymentBatch( - safeTransaction, - transactionOptions - ) - const hash = await sendTransaction({ - transaction: transactionBatchWithDeployment, - protocolKit: this.protocolKit - }) + async getOwners(): Promise { + return this.protocolKit.getOwners() + } - await this.#reconnectSafe() + /** + * Returns the Safe threshold. + * + * @returns {number} The Safe threshold + */ + async getThreshold(): Promise { + return this.protocolKit.getThreshold() + } - return createSafeClientResult({ - safeAddress: await this.protocolKit.getAddress(), - status: SafeClientTxStatus.DEPLOYED_AND_EXECUTED, - deploymentTxHash: hash, - txHash: hash - }) + /** + * Returns the Safe nonce. + * + * @returns {number} The Safe nonce + */ + async getNonce(): Promise { + return this.protocolKit.getNonce() } /** - * Deploys and proposes a transaction in one step. + * Returns a list of owners who have approved a specific Safe transaction. * - * @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 + * @param {string} txHash - The Safe transaction hash + * @returns {string[]} The list of owners */ - 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 - }) + async getOwnersWhoApprovedTx(txHash: string): Promise { + return this.protocolKit.getOwnersWhoApprovedTx(txHash) + } - await this.#reconnectSafe() + /** + * Encodes the data for adding a new owner to the Safe. + * + * @param {AddOwnerTxParams} params - The parameters for adding a new owner + * @param {string} params.ownerAddress - The address of the owner to add + * @param {number} params.threshold - The threshold of the Safe + * @returns {TransactionBase} The encoded data + */ + async createAddOwnerTransaction({ + ownerAddress, + threshold + }: AddOwnerTxParams): Promise { + const ownerManager = this.protocolKit.getOwnerManager() + + return this.#buildTransaction( + await ownerManager.encodeAddOwnerWithThresholdData(ownerAddress, threshold) + ) + } - safeTransaction = await this.protocolKit.signTransaction(safeTransaction) - const safeTxHash = await proposeTransaction({ - safeTransaction, - protocolKit: this.protocolKit, - apiKit: this.apiKit - }) + /** + * Encodes the data for removing an owner from the Safe. + * @param {RemoveOwnerTxParams} params - The parameters for removing an owner + * @param {string} params.ownerAddress - The address of the owner to remove + * @param {number} params.threshold - The threshold of the Safe + * @returns {TransactionBase} The encoded data + */ + async createRemoveOwnerTransaction({ + ownerAddress, + threshold + }: RemoveOwnerTxParams): Promise { + const ownerManager = this.protocolKit.getOwnerManager() - return createSafeClientResult({ - safeAddress: await this.protocolKit.getAddress(), - status: SafeClientTxStatus.DEPLOYED_AND_PENDING_SIGNATURES, - deploymentTxHash: hash, - safeTxHash - }) + return this.#buildTransaction(await ownerManager.encodeRemoveOwnerData(ownerAddress, threshold)) } /** - * Executes a transaction. + * Encodes the data for swapping an owner in the Safe. * - * @param {SafeTransaction} safeTransaction The safe transaction to be executed - * @param {TransactionOptions} transactionOptions Optional transaction options - * @returns A promise that resolves to the result of the transaction + * @param {SwapOwnerTxParams} params - The parameters for swapping an owner + * @param {string} params.oldOwnerAddress - The address of the old owner + * @param {string} params.newOwnerAddress - The address of the new owner + * @returns {TransactionBase} The encoded data */ - async #executeTransaction({ - safeTransaction, - ...transactionOptions - }: { safeTransaction: SafeTransaction } & TransactionOptions): Promise { - safeTransaction = await this.protocolKit.signTransaction(safeTransaction) - - const { hash } = await this.protocolKit.executeTransaction(safeTransaction, transactionOptions) - - return createSafeClientResult({ - safeAddress: await this.protocolKit.getAddress(), - status: SafeClientTxStatus.EXECUTED, - txHash: hash - }) + async createSwapOwnerTransaction({ + oldOwnerAddress, + newOwnerAddress + }: SwapOwnerTxParams): Promise { + const ownerManager = this.protocolKit.getOwnerManager() + + return this.#buildTransaction( + await ownerManager.encodeSwapOwnerData(oldOwnerAddress, newOwnerAddress) + ) } /** - * Proposes a transaction to the Safe. - * @param { SafeTransaction } safeTransaction The safe transaction to propose - * @returns The SafeClientResult + * + * @param {ChangeThresholdTxParams} params - The parameters for changing the Safe threshold + * @param {number} params.threshold - The new threshold + * @returns {TransactionBase} The encoded data */ - async #proposeTransaction({ safeTransaction }: { safeTransaction: SafeTransaction }) { - const safeTxHash = await proposeTransaction({ - safeTransaction, - protocolKit: this.protocolKit, - apiKit: this.apiKit - }) + async createChangeThresholdTransaction({ + threshold + }: ChangeThresholdTxParams): Promise { + const ownerManager = this.protocolKit.getOwnerManager() - return createSafeClientResult({ - safeAddress: await this.protocolKit.getAddress(), - status: SafeClientTxStatus.PENDING_SIGNATURES, - safeTxHash - }) + return this.#buildTransaction(await ownerManager.encodeChangeThresholdData(threshold)) } - async #reconnectSafe(): Promise { - this.protocolKit = await this.protocolKit.connect({ - provider: this.protocolKit.getSafeProvider().provider, - signer: this.protocolKit.getSafeProvider().signer, - safeAddress: await this.protocolKit.getAddress() - }) + async #buildTransaction(encodedData: string): Promise { + return { + to: await this.protocolKit.getAddress(), + value: '0', + data: encodedData + } } } diff --git a/packages/sdk-starter-kit/src/SafeClient.ts b/packages/sdk-starter-kit/src/SafeClient.ts index 7459fad37..60bb66155 100644 --- a/packages/sdk-starter-kit/src/SafeClient.ts +++ b/packages/sdk-starter-kit/src/SafeClient.ts @@ -1,157 +1,281 @@ -import Safe, { - AddOwnerTxParams, - RemoveOwnerTxParams, - SwapOwnerTxParams -} from '@safe-global/protocol-kit' +import Safe from '@safe-global/protocol-kit' +import SafeApiKit, { SafeMultisigTransactionListResponse } from '@safe-global/api-kit' +import { + SafeTransaction, + TransactionOptions, + TransactionResult, + Transaction +} from '@safe-global/safe-core-sdk-types' + +import { + createSafeClientResult, + sendTransaction, + proposeTransaction, + waitSafeTxReceipt +} from '@safe-global/sdk-starter-kit/utils' +import { SafeClientTxStatus } from '@safe-global/sdk-starter-kit/constants' +import { + ConfirmTransactionProps, + SafeClientResult, + SendTransactionProps +} from '@safe-global/sdk-starter-kit/types' + import { BaseClient } from './BaseClient' -import SafeApiKit from 'packages/api-kit/dist/src' -import { ChangeThresholdTxParams } from './types' -import { TransactionBase } from 'packages/safe-core-sdk-types/dist/src' +/** + * @class + * This class provides the core functionality to create, sign and execute transactions. + * It also provides the ability to be extended with features through the extend function. + * + * @example + * const safeClient = await createSafeClient({ ... }) + * + * const { transactions } = await safeClient.send(...) + * await safeClient.confirm(transactions?.safeTxHash) + */ export class SafeClient extends BaseClient { constructor(protocolKit: Safe, apiKit: SafeApiKit) { super(protocolKit, apiKit) } /** - * Checks if a specific address is an owner of the current Safe. + * 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 {string} ownerAddress - The account address - * @returns {boolean} TRUE if the account is an owner + * @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 isOwner(ownerAddress: string): Promise { - return this.protocolKit.isOwner(ownerAddress) - } + async send({ + transactions, + ...transactionOptions + }: SendTransactionProps): Promise { + const isSafeDeployed = await this.protocolKit.isSafeDeployed() + const isMultisigSafe = (await this.protocolKit.getThreshold()) > 1 - /** - * Returns the list of Safe owner accounts. - * - * @returns The list of owners - */ - async getOwners(): Promise { - return this.protocolKit.getOwners() - } + const safeTransaction = await this.protocolKit.createTransaction({ transactions }) - /** - * Returns the Safe threshold. - * - * @returns {number} The Safe threshold - */ - async getThreshold(): Promise { - return this.protocolKit.getThreshold() + if (isSafeDeployed) { + if (isMultisigSafe) { + // If the threshold is greater than 1, we need to propose the transaction first + return this.#proposeTransaction({ safeTransaction }) + } else { + // If the threshold is 1, we can execute the transaction + 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, ...transactionOptions }) + } else { + // If the threshold is 1, we can deploy the Safe account and execute the transaction in one step + return this.#deployAndExecuteTransaction({ safeTransaction, ...transactionOptions }) + } + } } /** - * Returns the Safe nonce. + * Confirms a transaction by its safe transaction hash. * - * @returns {number} The Safe nonce + * @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 getNonce(): Promise { - return this.protocolKit.getNonce() + 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) + + await this.apiKit.confirmTransaction(safeTxHash, signedTransaction.encodedSignatures()) + + transactionResponse = await this.apiKit.getTransaction(safeTxHash) + + if ( + transactionResponse.confirmations && + transactionResponse.confirmationsRequired === transactionResponse.confirmations.length + ) { + const executedTransactionResponse: TransactionResult = + await this.protocolKit.executeTransaction(transactionResponse) + + await waitSafeTxReceipt(executedTransactionResponse) + + return createSafeClientResult({ + status: SafeClientTxStatus.EXECUTED, + safeAddress, + txHash: executedTransactionResponse.hash, + safeTxHash + }) + } + + return createSafeClientResult({ + status: SafeClientTxStatus.PENDING_SIGNATURES, + safeAddress, + safeTxHash + }) } /** - * Returns a list of owners who have approved a specific Safe transaction. + * Retrieves the pending transactions for the current safe address. * - * @param {string} txHash - The Safe transaction hash - * @returns {string[]} The list of owners + * @async + * @returns {Promise} A promise that resolves to an array of pending transactions. + * @throws {Error} If there is an issue retrieving the safe address or pending transactions. */ - async getOwnersWhoApprovedTx(txHash: string): Promise { - return this.protocolKit.getOwnersWhoApprovedTx(txHash) + async getPendingTransactions(): Promise { + const safeAddress = await this.protocolKit.getAddress() + + return this.apiKit.getPendingTransactions(safeAddress) } /** - * Encodes the data for adding a new owner to the Safe. + * Extend the SafeClient with additional functionality. * - * @param {AddOwnerTxParams} params - The parameters for adding a new owner - * @param {string} params.ownerAddress - The address of the owner to add - * @param {number} params.threshold - The threshold of the Safe - * @returns {TransactionBase} The encoded data + * @param extendFunc + * @returns */ - async createAddOwnerTx({ ownerAddress, threshold }: AddOwnerTxParams): Promise { - const ownerManager = this.protocolKit.getOwnerManager() + extend(extendFunc: (client: SafeClient) => Promise): Promise + extend(extendFunc: (client: SafeClient) => T): SafeClient & T - return { - to: await this.protocolKit.getAddress(), - value: '0', - data: await ownerManager.encodeAddOwnerWithThresholdData(ownerAddress, threshold) + extend( + extendFunc: (client: SafeClient) => T | Promise + ): (SafeClient & T) | Promise { + const result = extendFunc(this) + + if (result instanceof Promise) { + return result.then((extensions) => Object.assign(this, extensions) as SafeClient & T) + } else { + return Object.assign(this, result) as SafeClient & T } } /** - * Encodes the data for removing an owner from the Safe. - * @param {RemoveOwnerTxParams} params - The parameters for removing an owner - * @param {string} params.ownerAddress - The address of the owner to remove - * @param {number} params.threshold - The threshold of the Safe - * @returns {TransactionBase} The encoded data + * Deploys and executes a transaction in one step. + * + * @param {SafeTransaction} safeTransaction The safe transaction to be executed + * @param {TransactionOptions} options Optional transaction options + * @returns A promise that resolves to the result of the transaction */ - async createRemoveOwnerTx({ - ownerAddress, - threshold - }: RemoveOwnerTxParams): Promise { - const ownerManager = this.protocolKit.getOwnerManager() - return { - to: await this.protocolKit.getAddress(), - value: '0', - data: await ownerManager.encodeRemoveOwnerData(ownerAddress, threshold) - } + async #deployAndExecuteTransaction({ + safeTransaction, + ...transactionOptions + }: { safeTransaction: SafeTransaction } & TransactionOptions): Promise { + safeTransaction = await this.protocolKit.signTransaction(safeTransaction) + + const transactionBatchWithDeployment = + await this.protocolKit.wrapSafeTransactionIntoDeploymentBatch( + safeTransaction, + transactionOptions + ) + const hash = await sendTransaction({ + transaction: transactionBatchWithDeployment, + protocolKit: this.protocolKit + }) + + await this.#reconnectSafe() + + return createSafeClientResult({ + safeAddress: await this.protocolKit.getAddress(), + status: SafeClientTxStatus.DEPLOYED_AND_EXECUTED, + deploymentTxHash: hash, + txHash: hash + }) } /** - * Encodes the data for swapping an owner in the Safe. + * Deploys and proposes a transaction in one step. * - * @param {SwapOwnerTxParams} params - The parameters for swapping an owner - * @param {string} params.oldOwnerAddress - The address of the old owner - * @param {string} params.newOwnerAddress - The address of the new owner - * @returns {TransactionBase} The encoded data + * @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 createSwapOwnerTx({ - oldOwnerAddress, - newOwnerAddress - }: SwapOwnerTxParams): Promise { - const ownerManager = this.protocolKit.getOwnerManager() - - return { - to: await this.protocolKit.getAddress(), - value: '0', - data: await ownerManager.encodeSwapOwnerData(oldOwnerAddress, newOwnerAddress) - } + 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, + protocolKit: this.protocolKit, + apiKit: this.apiKit + }) + + return createSafeClientResult({ + safeAddress: await this.protocolKit.getAddress(), + status: SafeClientTxStatus.DEPLOYED_AND_PENDING_SIGNATURES, + deploymentTxHash: hash, + safeTxHash + }) } /** + * Executes a transaction. * - * @param {ChangeThresholdTxParams} params - The parameters for changing the Safe threshold - * @param {number} params.threshold - The new threshold - * @returns {TransactionBase} The encoded data + * @param {SafeTransaction} safeTransaction The safe transaction to be executed + * @param {TransactionOptions} transactionOptions Optional transaction options + * @returns A promise that resolves to the result of the transaction */ - async createChangeThresholdTx({ threshold }: ChangeThresholdTxParams): Promise { - const ownerManager = this.protocolKit.getOwnerManager() + async #executeTransaction({ + safeTransaction, + ...transactionOptions + }: { safeTransaction: SafeTransaction } & TransactionOptions): Promise { + safeTransaction = await this.protocolKit.signTransaction(safeTransaction) - return { - to: await this.protocolKit.getAddress(), - value: '0', - data: await ownerManager.encodeChangeThresholdData(threshold) - } + const { hash } = await this.protocolKit.executeTransaction(safeTransaction, transactionOptions) + + return createSafeClientResult({ + safeAddress: await this.protocolKit.getAddress(), + status: SafeClientTxStatus.EXECUTED, + txHash: hash + }) } /** - * Extend the SafeClient with additional functionality. - * - * @param extendFunc - * @returns + * Proposes a transaction to the Safe. + * @param { SafeTransaction } safeTransaction The safe transaction to propose + * @returns The SafeClientResult */ - extend(extendFunc: (client: SafeClient) => Promise): Promise - extend(extendFunc: (client: SafeClient) => T): BaseClient & T + async #proposeTransaction({ safeTransaction }: { safeTransaction: SafeTransaction }) { + const safeTxHash = await proposeTransaction({ + safeTransaction, + protocolKit: this.protocolKit, + apiKit: this.apiKit + }) - extend( - extendFunc: (client: SafeClient) => T | Promise - ): (SafeClient & T) | Promise { - const result = extendFunc(this) + return createSafeClientResult({ + safeAddress: await this.protocolKit.getAddress(), + status: SafeClientTxStatus.PENDING_SIGNATURES, + safeTxHash + }) + } - if (result instanceof Promise) { - return result.then((extensions) => Object.assign(this, extensions) as SafeClient & T) - } else { - return Object.assign(this, result) as SafeClient & T - } + async #reconnectSafe(): Promise { + this.protocolKit = await this.protocolKit.connect({ + provider: this.protocolKit.getSafeProvider().provider, + signer: this.protocolKit.getSafeProvider().signer, + safeAddress: await this.protocolKit.getAddress() + }) } } diff --git a/playground/sdk-starter-kit/owner-management.ts b/playground/sdk-starter-kit/owner-management.ts index 73c0e77b3..cd234a819 100644 --- a/playground/sdk-starter-kit/owner-management.ts +++ b/playground/sdk-starter-kit/owner-management.ts @@ -14,7 +14,7 @@ async function addOwner() { safeAddress: SAFE_ADDRESS }) - const transaction = await safeClient.createAddOwnerTx({ + const transaction = await safeClient.createAddOwnerTransaction({ ownerAddress: OWNER_2_ADDRESS, threshold: 2 }) @@ -37,7 +37,7 @@ async function removeOwner() { safeAddress: SAFE_ADDRESS }) - const transaction = await safeClient1.createRemoveOwnerTx({ + const transaction = await safeClient1.createRemoveOwnerTransaction({ ownerAddress: OWNER_2_ADDRESS, threshold: 1 }) From 9bb508e90258509c0dba10c06f617d2713589408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yago=20P=C3=A9rez=20V=C3=A1zquez?= Date: Tue, 20 Aug 2024 16:24:06 +0200 Subject: [PATCH 3/3] Add missing comments --- packages/sdk-starter-kit/src/BaseClient.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/sdk-starter-kit/src/BaseClient.ts b/packages/sdk-starter-kit/src/BaseClient.ts index 55448de74..ebdb2475a 100644 --- a/packages/sdk-starter-kit/src/BaseClient.ts +++ b/packages/sdk-starter-kit/src/BaseClient.ts @@ -19,6 +19,7 @@ export class BaseClient { /** * Returns the Safe address. + * * @returns {string} The Safe address */ async getAddress(): Promise { @@ -102,6 +103,7 @@ export class BaseClient { /** * Encodes the data for removing an owner from the Safe. + * * @param {RemoveOwnerTxParams} params - The parameters for removing an owner * @param {string} params.ownerAddress - The address of the owner to remove * @param {number} params.threshold - The threshold of the Safe @@ -136,6 +138,7 @@ export class BaseClient { } /** + * Encodes the data for changing the Safe threshold. * * @param {ChangeThresholdTxParams} params - The parameters for changing the Safe threshold * @param {number} params.threshold - The new threshold