diff --git a/guides/integrating-the-safe-core-sdk.md b/guides/integrating-the-safe-core-sdk.md index 2a648e302..03927ebe5 100644 --- a/guides/integrating-the-safe-core-sdk.md +++ b/guides/integrating-the-safe-core-sdk.md @@ -55,9 +55,7 @@ const apiKit = new SafeApiKit({ ### Initialize the Protocol Kit ```js -import Safe, { SafeFactory } from '@safe-global/protocol-kit' - -const safeFactory = await SafeFactory.init({ provider, signer }) +import Safe from '@safe-global/protocol-kit' const protocolKit = await Safe.init({ provider, signer, safeAddress }) ``` @@ -67,7 +65,6 @@ There are two versions of the Safe contracts: [Safe.sol](https://github.com/safe By default `Safe.sol` will be only used on Ethereum Mainnet. For the rest of the networks where the Safe contracts are already deployed, the `SafeL2.sol` contract will be used unless you add the property `isL1SafeSingleton` to force the use of the `Safe.sol` contract. ```js -const safeFactory = await SafeFactory.init({ provider, signer, isL1SafeSingleton: true }) const protocolKit = await Safe.init({ provider, signer, safeAddress, isL1SafeSingleton: true }) ``` @@ -102,36 +99,74 @@ const contractNetworks: ContractNetworksConfig = { } } -const safeFactory = await SafeFactory.init({ provider, signer, contractNetworks }) - const protocolKit = await Safe.init({ provider, signer, safeAddress, contractNetworks }) ``` -The `SafeFactory` constructor also accepts the property `safeVersion` to specify the Safe contract version that will be deployed. This string can take the values `1.0.0`, `1.1.1`, `1.2.0`, `1.3.0` or `1.4.1`. If not specified, the `DEFAULT_SAFE_VERSION` value will be used. - -```js -const safeVersion = 'X.Y.Z' -const safeFactory = await SafeFactory.init({ provider, signer, safeVersion }) -``` - ## 3. Deploy a new Safe -The Protocol Kit library allows the deployment of new Safes using the `safeFactory` instance we just created. +The Protocol Kit library now simplifies the creation of new Safes by providing the `createSafeDeploymentTransaction` method. This method returns an Ethereum transaction object ready for execution, which includes the deployment of a Safe. -Here, for example, we can create a new Safe account with 3 owners and 2 required signatures. +Here is an example of how to create a new Safe account with 3 owners and 2 required signatures: ```js import { SafeAccountConfig } from '@safe-global/protocol-kit' const safeAccountConfig: SafeAccountConfig = { - owners: ['0x...', '0x...', '0x...'] - threshold: 2, - // ... (optional params) + owners: ['0x...', '0x...', '0x...'], + threshold: 2 + // Additional optional parameters can be included here } -const protocolKit = await safeFactory.deploySafe({ safeAccountConfig }) + +const predictSafe = { + safeAccountConfig, + safeDeploymentConfig: { + saltNonce, // optional parameter + safeVersion // optional parameter + } +} + +const protocolKit = await Safe.init({ provider, signer, predictSafe }) + +const deploymentTransaction = await protocolKit.createSafeDeploymentTransaction() + +// Execute this transaction using the Ethereum client of your choice +const txHash = await client.sendTransaction({ + to: deploymentTransaction.to, + value: BigInt(deploymentTransaction.value), + data: `0x${deploymentTransaction.data}` +}) + ``` -Calling the method `deploySafe` will deploy the desired Safe and return a Protocol Kit initialized instance ready to be used. Check the [API Reference](https://github.com/safe-global/safe-core-sdk/tree/main/packages/protocol-kit#deploysafe) for more details on additional configuration parameters and callbacks. +Once you obtain the `deploymentTransaction` object, you will have an Ethereum transaction object containing the `to`, `value`, and `data` fields. You can execute this transaction using the Ethereum client of your choice. Check the [API Reference](https://github.com/safe-global/safe-core-sdk/tree/main/packages/protocol-kit#deploysafe) for more details on additional configuration parameters. + +After successfully executing the transaction and confirming that the Safe has been deployed, you will need to reconnect to the new Safe address. Use the `connect` method to reinitialize the protocol-kit instance with the deployed Safe address: + +```js +// Execute this transaction using the Ethereum client of your choice +const txHash = await client.sendTransaction({ + to: deploymentTransaction.to, + value: BigInt(deploymentTransaction.value), + data: `0x${deploymentTransaction.data}` +}) + +console.log('Transaction hash:', txHash) + +const txReceipt = await waitForTransactionReceipt(client, { hash: txHash }) + +// Extract the Safe address from the deployment transaction receipt +const safeAddress = getSafeAddressFromDeploymentTx(txReceipt, safeVersion) + +console.log('safeAddress:', safeAddress) + +// Reinitialize the instance of protocol-kit using the obtained Safe address +protocolKit.connect({ safeAddress }) + +console.log('is Safe deployed:', await protocolKit.isSafeDeployed()) +console.log('Safe Address:', await protocolKit.getAddress()) +console.log('Safe Owners:', await protocolKit.getOwners()) +console.log('Safe Threshold:', await protocolKit.getThreshold()) +``` ## 4. Create a transaction diff --git a/packages/protocol-kit/src/Safe.ts b/packages/protocol-kit/src/Safe.ts index 3c5bcca8b..26517716b 100644 --- a/packages/protocol-kit/src/Safe.ts +++ b/packages/protocol-kit/src/Safe.ts @@ -19,7 +19,9 @@ import { encodeSetupCallData, getChainSpecificDefaultSaltNonce, getPredictedSafeAddressInitCode, - predictSafeAddress + predictSafeAddress, + validateSafeAccountConfig, + validateSafeDeploymentConfig } from './contracts/utils' import { ContractInfo, DEFAULT_SAFE_VERSION, getContractInfo } from './contracts/config' import ContractManager from './managers/contractManager' @@ -877,7 +879,7 @@ class Safe { */ async getOwnersWhoApprovedTx(txHash: string): Promise { if (!this.#contractManager.safeContract) { - throw new Error('Safe is not deployed') + return [] } const owners = await this.getOwners() @@ -908,6 +910,13 @@ class Safe { fallbackHandlerAddress: string, options?: SafeTransactionOptionalProps ): Promise { + const safeVersion = await this.getContractVersion() + if (this.#predictedSafe && !hasSafeFeature(SAFE_FEATURES.ACCOUNT_ABSTRACTION, safeVersion)) { + throw new Error( + 'Account Abstraction functionality is not available for Safes with version lower than v1.3.0' + ) + } + const safeTransactionData = { to: await this.getAddress(), value: '0', @@ -933,6 +942,13 @@ class Safe { async createDisableFallbackHandlerTx( options?: SafeTransactionOptionalProps ): Promise { + const safeVersion = await this.getContractVersion() + if (this.#predictedSafe && !hasSafeFeature(SAFE_FEATURES.ACCOUNT_ABSTRACTION, safeVersion)) { + throw new Error( + 'Account Abstraction functionality is not available for Safes with version lower than v1.3.0' + ) + } + const safeTransactionData = { to: await this.getAddress(), value: '0', @@ -1307,31 +1323,9 @@ class Safe { ? await this.toSafeTransactionType(safeTransaction) : safeTransaction - const signedSafeTransaction = await this.copyTransaction(transaction) + const signedSafeTransaction = await this.#addPreValidatedSignature(transaction) - const txHash = await this.getTransactionHash(signedSafeTransaction) - const ownersWhoApprovedTx = await this.getOwnersWhoApprovedTx(txHash) - for (const owner of ownersWhoApprovedTx) { - signedSafeTransaction.addSignature(generatePreValidatedSignature(owner)) - } - const threshold = await this.getThreshold() - const signerAddress = await this.#safeProvider.getSignerAddress() - if (!signerAddress) { - throw new Error('The protocol-kit requires a signer to use this method') - } - const addressIsOwner = await this.isOwner(signerAddress) - if (threshold > signedSafeTransaction.signatures.size && addressIsOwner) { - signedSafeTransaction.addSignature(generatePreValidatedSignature(signerAddress)) - } - - if (threshold > signedSafeTransaction.signatures.size) { - const signaturesMissing = threshold - signedSafeTransaction.signatures.size - throw new Error( - `There ${signaturesMissing > 1 ? 'are' : 'is'} ${signaturesMissing} signature${ - signaturesMissing > 1 ? 's' : '' - } missing` - ) - } + await this.#isReadyToExecute(signedSafeTransaction) const value = BigInt(signedSafeTransaction.data.value) if (value !== 0n) { @@ -1341,6 +1335,8 @@ class Safe { } } + const signerAddress = await this.#safeProvider.getSignerAddress() + const txResponse = await this.#contractManager.safeContract.execTransaction( signedSafeTransaction, { @@ -1351,6 +1347,58 @@ class Safe { return txResponse } + /** + * Adds a PreValidatedSignature to the transaction if the threshold is not reached. + * + * @async + * @param {SafeTransaction} transaction - The transaction to add a signature to. + * @returns {Promise} A promise that resolves to the signed transaction. + */ + async #addPreValidatedSignature(transaction: SafeTransaction): Promise { + const signedSafeTransaction = await this.copyTransaction(transaction) + + const txHash = await this.getTransactionHash(signedSafeTransaction) + const ownersWhoApprovedTx = await this.getOwnersWhoApprovedTx(txHash) + + for (const owner of ownersWhoApprovedTx) { + signedSafeTransaction.addSignature(generatePreValidatedSignature(owner)) + } + + const owners = await this.getOwners() + const threshold = await this.getThreshold() + const signerAddress = await this.#safeProvider.getSignerAddress() + + if ( + threshold > signedSafeTransaction.signatures.size && + signerAddress && + owners.includes(signerAddress) + ) { + signedSafeTransaction.addSignature(generatePreValidatedSignature(signerAddress)) + } + + return signedSafeTransaction + } + + /** + * Checks if the transaction has enough signatures to be executed. + * + * @async + * @param {SafeTransaction} transaction - The Safe transaction to check. + * @throws Will throw an error if the required number of signatures is not met. + */ + async #isReadyToExecute(transaction: SafeTransaction) { + const threshold = await this.getThreshold() + + if (threshold > transaction.signatures.size) { + const signaturesMissing = threshold - transaction.signatures.size + throw new Error( + `There ${signaturesMissing > 1 ? 'are' : 'is'} ${signaturesMissing} signature${ + signaturesMissing > 1 ? 's' : '' + } missing` + ) + } + } + /** * Returns the Safe Transaction encoded * @@ -1397,15 +1445,13 @@ class Safe { * @async * @param {SafeTransaction} safeTransaction - The Safe transaction to be wrapped into the deployment batch. * @param {TransactionOptions} [transactionOptions] - Optional. Options for the transaction, such as from, gas price, gas limit, etc. - * @param {string} [customSaltNonce] - Optional. a Custom salt nonce to be used for the deployment of the Safe. If not provided, a default value is used. * @returns {Promise} A promise that resolves to a Transaction object representing the prepared batch of transactions. * @throws Will throw an error if the safe is already deployed. * */ async wrapSafeTransactionIntoDeploymentBatch( safeTransaction: SafeTransaction, - transactionOptions?: TransactionOptions, - customSaltNonce?: string + transactionOptions?: TransactionOptions ): Promise { const isSafeDeployed = await this.isSafeDeployed() @@ -1415,7 +1461,7 @@ class Safe { } // we create the deployment transaction - const safeDeploymentTransaction = await this.createSafeDeploymentTransaction(customSaltNonce) + const safeDeploymentTransaction = await this.createSafeDeploymentTransaction() // First transaction of the batch: The Safe deployment Transaction const safeDeploymentBatchTransaction = { @@ -1443,31 +1489,35 @@ class Safe { } /** - * Creates a Safe deployment transaction. - * - * This function prepares a transaction for the deployment of a Safe. - * Both the saltNonce and options parameters are optional, and if not - * provided, default values will be used. - * - * @async - * @param {string} [customSaltNonce] - Optional. a Custom salt nonce to be used for the deployment of the Safe. If not provided, a default value is used. - * @param {TransactionOptions} [options] - Optional. Options for the transaction, such as gas price, gas limit, etc. - * @returns {Promise} A promise that resolves to a Transaction object representing the prepared Safe deployment transaction. + * Creates a transaction to deploy a Safe Account. * + * @returns {Promise} Returns a promise that resolves to an Ethereum transaction with the fields `to`, `value`, and `data`, which can be used to deploy the Safe Account. */ - async createSafeDeploymentTransaction( - customSaltNonce?: string, - transactionOptions?: TransactionOptions - ): Promise { + async createSafeDeploymentTransaction(): Promise { if (!this.#predictedSafe) { - throw new Error('Predict Safe should be present') + throw new Error('Predict Safe should be present to build the Safe deployement transaction') } - const { safeAccountConfig, safeDeploymentConfig } = this.#predictedSafe + const { safeAccountConfig, safeDeploymentConfig = {} } = this.#predictedSafe + + validateSafeAccountConfig(safeAccountConfig) + validateSafeDeploymentConfig(safeDeploymentConfig) - const safeVersion = this.getContractVersion() const safeProvider = this.#safeProvider const chainId = await safeProvider.getChainId() + const safeVersion = safeDeploymentConfig?.safeVersion || DEFAULT_SAFE_VERSION + const saltNonce = safeDeploymentConfig?.saltNonce || getChainSpecificDefaultSaltNonce(chainId) + + // we only check if the safe is deployed if safeVersion >= 1.3.0 + if (hasSafeFeature(SAFE_FEATURES.ACCOUNT_ABSTRACTION, safeVersion)) { + const isSafeDeployed = await this.isSafeDeployed() + + // if the safe is already deployed throws an error + if (isSafeDeployed) { + throw new Error('Safe already deployed') + } + } + const isL1SafeSingleton = this.#contractManager.isL1SafeSingleton const customContracts = this.#contractManager.contractNetworks?.[chainId.toString()] @@ -1493,13 +1543,7 @@ class Safe { customContracts }) - const saltNonce = - customSaltNonce || - safeDeploymentConfig?.saltNonce || - getChainSpecificDefaultSaltNonce(chainId) - const safeDeployTransactionData = { - ...transactionOptions, // optional transaction options like from, gasLimit, gasPrice... to: safeProxyFactoryContract.getAddress(), value: '0', // we use the createProxyWithNonce method to create the Safe in a deterministic address, see: https://github.com/safe-global/safe-contracts/blob/main/contracts/proxies/SafeProxyFactory.sol#L52 diff --git a/packages/protocol-kit/src/SafeFactory.ts b/packages/protocol-kit/src/SafeFactory.ts deleted file mode 100644 index a6dc93c2a..000000000 --- a/packages/protocol-kit/src/SafeFactory.ts +++ /dev/null @@ -1,176 +0,0 @@ -import Safe from '@safe-global/protocol-kit/Safe' -import { DEFAULT_SAFE_VERSION } from '@safe-global/protocol-kit/contracts/config' -import { - getSafeProxyFactoryContract, - getSafeContract -} from '@safe-global/protocol-kit/contracts/safeDeploymentContracts' -import { - encodeSetupCallData, - getChainSpecificDefaultSaltNonce, - predictSafeAddress, - validateSafeAccountConfig, - validateSafeDeploymentConfig -} from '@safe-global/protocol-kit/contracts/utils' -import { - ContractNetworksConfig, - SafeAccountConfig, - SafeContractImplementationType, - SafeDeploymentConfig, - SafeProxyFactoryContractImplementationType, - SafeProviderConfig, - SafeFactoryConfig, - SafeFactoryInitConfig, - DeploySafeProps -} from '@safe-global/protocol-kit/types' -import { SafeVersion } from '@safe-global/types-kit' -import SafeProvider from '@safe-global/protocol-kit/SafeProvider' - -class SafeFactory { - #contractNetworks?: ContractNetworksConfig - #isL1SafeSingleton?: boolean - #safeVersion!: SafeVersion - #safeProxyFactoryContract!: SafeProxyFactoryContractImplementationType - #safeContract!: SafeContractImplementationType - #provider!: SafeProviderConfig['provider'] - #signer?: SafeFactoryConfig['signer'] - #safeProvider!: SafeProvider - - static async init({ - provider, - signer, - safeVersion = DEFAULT_SAFE_VERSION, - isL1SafeSingleton = false, - contractNetworks - }: SafeFactoryConfig): Promise { - const safeFactorySdk = new SafeFactory() - await safeFactorySdk.#initializeSafeFactory({ - provider, - signer, - safeVersion, - isL1SafeSingleton, - contractNetworks - }) - return safeFactorySdk - } - - async #initializeSafeFactory({ - provider, - signer, - safeVersion, - isL1SafeSingleton, - contractNetworks - }: SafeFactoryInitConfig) { - this.#provider = provider - this.#signer = signer - this.#safeProvider = await SafeProvider.init({ - provider, - signer, - safeVersion, - contractNetworks - }) - this.#safeVersion = safeVersion - this.#isL1SafeSingleton = isL1SafeSingleton - this.#contractNetworks = contractNetworks - const chainId = await this.#safeProvider.getChainId() - const customContracts = contractNetworks?.[chainId.toString()] - this.#safeProxyFactoryContract = await getSafeProxyFactoryContract({ - safeProvider: this.#safeProvider, - safeVersion, - customContracts - }) - this.#safeContract = await getSafeContract({ - safeProvider: this.#safeProvider, - safeVersion, - isL1SafeSingleton, - customContracts - }) - } - - getSafeProvider(): SafeProvider { - return this.#safeProvider - } - - getSafeVersion(): SafeVersion { - return this.#safeVersion - } - - getAddress(): string { - return this.#safeProxyFactoryContract.getAddress() - } - - async getChainId(): Promise { - return this.#safeProvider.getChainId() - } - - async predictSafeAddress( - safeAccountConfig: SafeAccountConfig, - saltNonce?: string - ): Promise { - const chainId = await this.#safeProvider.getChainId() - const customContracts = this.#contractNetworks?.[chainId.toString()] - const safeVersion = this.#safeVersion - - const safeDeploymentConfig: SafeDeploymentConfig = { - saltNonce: saltNonce || getChainSpecificDefaultSaltNonce(chainId), - safeVersion - } - - return predictSafeAddress({ - safeProvider: this.#safeProvider, - chainId, - safeAccountConfig, - safeDeploymentConfig, - isL1SafeSingleton: this.#isL1SafeSingleton, - customContracts - }) - } - - async deploySafe({ - safeAccountConfig, - saltNonce, - options, - callback - }: DeploySafeProps): Promise { - validateSafeAccountConfig(safeAccountConfig) - validateSafeDeploymentConfig({ saltNonce }) - - const signerAddress = await this.#safeProvider.getSignerAddress() - if (!signerAddress) { - throw new Error('SafeProvider must be initialized with a signer to use this method') - } - - const chainId = await this.getChainId() - const customContracts = this.#contractNetworks?.[chainId.toString()] - const initializer = await encodeSetupCallData({ - safeProvider: this.#safeProvider, - safeAccountConfig, - safeContract: this.#safeContract, - customContracts - }) - - const safeAddress = await this.#safeProxyFactoryContract.createProxyWithOptions({ - safeSingletonAddress: this.#safeContract.getAddress(), - initializer, - saltNonce: saltNonce || getChainSpecificDefaultSaltNonce(chainId), - options: { - from: signerAddress, - ...options - }, - callback - }) - const isContractDeployed = await this.#safeProvider.isContractDeployed(safeAddress) - if (!isContractDeployed) { - throw new Error('SafeProxy contract is not deployed on the current network') - } - const safe = await Safe.init({ - provider: this.#provider, - signer: this.#signer, - safeAddress, - isL1SafeSingleton: this.#isL1SafeSingleton, - contractNetworks: this.#contractNetworks - }) - return safe - } -} - -export default SafeFactory diff --git a/packages/protocol-kit/src/SafeProvider.ts b/packages/protocol-kit/src/SafeProvider.ts index 84b727822..5ab9ec71b 100644 --- a/packages/protocol-kit/src/SafeProvider.ts +++ b/packages/protocol-kit/src/SafeProvider.ts @@ -5,7 +5,7 @@ import { hasSafeFeature, validateEip3770Address, toEstimateGasParameters, - toCallGasParameters, + toTransactionRequest, sameString } from '@safe-global/protocol-kit/utils' import { isTypedDataSigner } from '@safe-global/protocol-kit/contracts/utils' @@ -346,7 +346,7 @@ class SafeProvider { } async call(transaction: SafeProviderTransaction, blockTag?: string | number): Promise { - const converted = toCallGasParameters(transaction) + const converted = toTransactionRequest(transaction) const { data } = await call(this.#externalProvider, { ...converted, ...asBlockId(blockTag) diff --git a/packages/protocol-kit/src/contracts/SafeProxyFactory/SafeProxyFactoryBaseContract.ts b/packages/protocol-kit/src/contracts/SafeProxyFactory/SafeProxyFactoryBaseContract.ts index d95988ef8..17213408f 100644 --- a/packages/protocol-kit/src/contracts/SafeProxyFactory/SafeProxyFactoryBaseContract.ts +++ b/packages/protocol-kit/src/contracts/SafeProxyFactory/SafeProxyFactoryBaseContract.ts @@ -1,18 +1,10 @@ import { Abi } from 'abitype' import SafeProvider from '@safe-global/protocol-kit/SafeProvider' import BaseContract from '@safe-global/protocol-kit/contracts/BaseContract' +import { SafeVersion } from '@safe-global/types-kit' import { DeploymentType } from '@safe-global/protocol-kit/types' -import { - SafeVersion, - TransactionOptions, - CreateProxyProps as CreateProxyPropsGeneral -} from '@safe-global/types-kit' import { contractName } from '@safe-global/protocol-kit/contracts/config' -export interface CreateProxyProps extends CreateProxyPropsGeneral { - options?: TransactionOptions -} - /** * Abstract class SafeProxyFactoryBaseContract extends BaseContract to specifically integrate with the SafeProxyFactory contract. * It is designed to be instantiated for different versions of the Safe contract. diff --git a/packages/protocol-kit/src/contracts/SafeProxyFactory/v1.0.0/SafeProxyFactoryContract_v1_0_0.ts b/packages/protocol-kit/src/contracts/SafeProxyFactory/v1.0.0/SafeProxyFactoryContract_v1_0_0.ts index f0b349f84..5761fe014 100644 --- a/packages/protocol-kit/src/contracts/SafeProxyFactory/v1.0.0/SafeProxyFactoryContract_v1_0_0.ts +++ b/packages/protocol-kit/src/contracts/SafeProxyFactory/v1.0.0/SafeProxyFactoryContract_v1_0_0.ts @@ -1,7 +1,4 @@ -import { parseEventLogs } from 'viem' -import SafeProxyFactoryBaseContract, { - CreateProxyProps -} from '@safe-global/protocol-kit/contracts/SafeProxyFactory/SafeProxyFactoryBaseContract' +import SafeProxyFactoryBaseContract from '@safe-global/protocol-kit/contracts/SafeProxyFactory/SafeProxyFactoryBaseContract' import SafeProvider from '@safe-global/protocol-kit/SafeProvider' import { DeploymentType } from '@safe-global/protocol-kit/types' import { @@ -10,8 +7,6 @@ import { SafeProxyFactoryContract_v1_0_0_Function, safeProxyFactory_1_0_0_ContractArtifacts } from '@safe-global/types-kit' -import { waitForTransactionReceipt } from '@safe-global/protocol-kit/utils' -import { asHex } from '@safe-global/protocol-kit/utils/types' /** * SafeProxyFactoryContract_v1_0_0 is the implementation specific to the Safe Proxy Factory contract version 1.0.0. @@ -90,57 +85,6 @@ class SafeProxyFactoryContract_v1_0_0 ) => { return [await this.write('createProxyWithNonce', args)] } - - /** - * Allows to create new proxy contract and execute a message call to the new proxy within one transaction. - * @param {CreateProxyProps} props - Properties for the new proxy contract. - * @returns The address of the new proxy contract. - */ - async createProxyWithOptions({ - safeSingletonAddress, - initializer, - saltNonce, - options, - callback - }: CreateProxyProps): Promise { - const saltNonceBigInt = BigInt(saltNonce) - - if (saltNonceBigInt < 0) throw new Error('saltNonce must be greater than or equal to 0') - - if (options && !options.gasLimit) { - options.gasLimit = ( - await this.estimateGas( - 'createProxyWithNonce', - [safeSingletonAddress, asHex(initializer), saltNonceBigInt], - { ...options } - ) - ).toString() - } - - const coverted = this.convertOptions(options) - const proxyAddress = await this.getWallet() - .writeContract({ - address: this.contractAddress, - abi: this.contractAbi, - functionName: 'createProxyWithNonce', - args: [safeSingletonAddress, asHex(initializer), saltNonceBigInt], - ...coverted - }) - .then(async (hash) => { - if (callback) { - callback(hash) - } - const { logs } = await waitForTransactionReceipt(this.runner, hash) - const events = parseEventLogs({ logs, abi: this.contractAbi }) - const proxyCreationEvent = events.find((event) => event?.eventName === 'ProxyCreation') - if (!proxyCreationEvent || !proxyCreationEvent.args) { - throw new Error('SafeProxy was not deployed correctly') - } - return proxyCreationEvent.args.proxy - }) - - return proxyAddress - } } export default SafeProxyFactoryContract_v1_0_0 diff --git a/packages/protocol-kit/src/contracts/SafeProxyFactory/v1.1.1/SafeProxyFactoryContract_v1_1_1.ts b/packages/protocol-kit/src/contracts/SafeProxyFactory/v1.1.1/SafeProxyFactoryContract_v1_1_1.ts index 7e948b397..f7ace326b 100644 --- a/packages/protocol-kit/src/contracts/SafeProxyFactory/v1.1.1/SafeProxyFactoryContract_v1_1_1.ts +++ b/packages/protocol-kit/src/contracts/SafeProxyFactory/v1.1.1/SafeProxyFactoryContract_v1_1_1.ts @@ -1,7 +1,4 @@ -import { parseEventLogs } from 'viem' -import SafeProxyFactoryBaseContract, { - CreateProxyProps -} from '@safe-global/protocol-kit/contracts/SafeProxyFactory/SafeProxyFactoryBaseContract' +import SafeProxyFactoryBaseContract from '@safe-global/protocol-kit/contracts/SafeProxyFactory/SafeProxyFactoryBaseContract' import SafeProvider from '@safe-global/protocol-kit/SafeProvider' import { DeploymentType } from '@safe-global/protocol-kit/types' import { @@ -10,8 +7,6 @@ import { SafeProxyFactoryContract_v1_1_1_Function, safeProxyFactory_1_1_1_ContractArtifacts } from '@safe-global/types-kit' -import { waitForTransactionReceipt } from '@safe-global/protocol-kit/utils' -import { asHex } from '@safe-global/protocol-kit/utils/types' /** * SafeProxyFactoryContract_v1_1_1 is the implementation specific to the Safe Proxy Factory contract version 1.1.1. @@ -110,57 +105,6 @@ class SafeProxyFactoryContract_v1_1_1 ) => { return [await this.write('createProxyWithNonce', args)] } - - /** - * Allows to create new proxy contract and execute a message call to the new proxy within one transaction. - * @param {CreateProxyProps} props - Properties for the new proxy contract. - * @returns The address of the new proxy contract. - */ - async createProxyWithOptions({ - safeSingletonAddress, - initializer, - saltNonce, - options, - callback - }: CreateProxyProps): Promise { - const saltNonceBigInt = BigInt(saltNonce) - - if (saltNonceBigInt < 0) throw new Error('saltNonce must be greater than or equal to 0') - - if (options && !options.gasLimit) { - options.gasLimit = ( - await this.estimateGas( - 'createProxyWithNonce', - [safeSingletonAddress, asHex(initializer), saltNonceBigInt], - { ...options } - ) - ).toString() - } - - const coverted = this.convertOptions(options) - const proxyAddress = await this.getWallet() - .writeContract({ - address: this.contractAddress, - abi: this.contractAbi, - functionName: 'createProxyWithNonce', - args: [safeSingletonAddress, asHex(initializer), saltNonceBigInt], - ...coverted - }) - .then(async (hash) => { - if (callback) { - callback(hash) - } - const { logs } = await waitForTransactionReceipt(this.runner, hash) - const events = parseEventLogs({ logs, abi: this.contractAbi }) - const proxyCreationEvent = events.find((event) => event?.eventName === 'ProxyCreation') - if (!proxyCreationEvent || !proxyCreationEvent.args) { - throw new Error('SafeProxy was not deployed correctly') - } - return proxyCreationEvent.args.proxy - }) - - return proxyAddress - } } export default SafeProxyFactoryContract_v1_1_1 diff --git a/packages/protocol-kit/src/contracts/SafeProxyFactory/v1.3.0/SafeProxyFactoryContract_v1_3_0.ts b/packages/protocol-kit/src/contracts/SafeProxyFactory/v1.3.0/SafeProxyFactoryContract_v1_3_0.ts index 59d8128f4..e99a1f267 100644 --- a/packages/protocol-kit/src/contracts/SafeProxyFactory/v1.3.0/SafeProxyFactoryContract_v1_3_0.ts +++ b/packages/protocol-kit/src/contracts/SafeProxyFactory/v1.3.0/SafeProxyFactoryContract_v1_3_0.ts @@ -1,7 +1,4 @@ -import { parseEventLogs } from 'viem' -import SafeProxyFactoryBaseContract, { - CreateProxyProps -} from '@safe-global/protocol-kit/contracts/SafeProxyFactory/SafeProxyFactoryBaseContract' +import SafeProxyFactoryBaseContract from '@safe-global/protocol-kit/contracts/SafeProxyFactory/SafeProxyFactoryBaseContract' import SafeProvider from '@safe-global/protocol-kit/SafeProvider' import { DeploymentType } from '@safe-global/protocol-kit/types' import { @@ -10,8 +7,6 @@ import { SafeProxyFactoryContract_v1_3_0_Function, safeProxyFactory_1_3_0_ContractArtifacts } from '@safe-global/types-kit' -import { waitForTransactionReceipt } from '@safe-global/protocol-kit/utils' -import { asHex } from '@safe-global/protocol-kit/utils/types' /** * SafeProxyFactoryContract_v1_3_0 is the implementation specific to the Safe Proxy Factory contract version 1.3.0. @@ -110,57 +105,6 @@ class SafeProxyFactoryContract_v1_3_0 ) => { return [await this.write('createProxyWithNonce', args)] } - - /** - * Allows to create new proxy contract and execute a message call to the new proxy within one transaction. - * @param {CreateProxyProps} props - Properties for the new proxy contract. - * @returns The address of the new proxy contract. - */ - async createProxyWithOptions({ - safeSingletonAddress, - initializer, - saltNonce, - options, - callback - }: CreateProxyProps): Promise { - const saltNonceBigInt = BigInt(saltNonce) - - if (saltNonceBigInt < 0) throw new Error('saltNonce must be greater than or equal to 0') - - if (options && !options.gasLimit) { - options.gasLimit = ( - await this.estimateGas( - 'createProxyWithNonce', - [safeSingletonAddress, asHex(initializer), saltNonceBigInt], - { ...options } - ) - ).toString() - } - - const coverted = this.convertOptions(options) - const proxyAddress = await this.getWallet() - .writeContract({ - address: this.contractAddress, - abi: this.contractAbi, - functionName: 'createProxyWithNonce', - args: [safeSingletonAddress, asHex(initializer), saltNonceBigInt], - ...coverted - }) - .then(async (hash) => { - if (callback) { - callback(hash) - } - const { logs } = await waitForTransactionReceipt(this.runner, hash) - const events = parseEventLogs({ logs, abi: this.contractAbi }) - const proxyCreationEvent = events.find((event) => event?.eventName === 'ProxyCreation') - if (!proxyCreationEvent || !proxyCreationEvent.args) { - throw new Error('SafeProxy was not deployed correctly') - } - return proxyCreationEvent.args.proxy - }) - - return proxyAddress - } } export default SafeProxyFactoryContract_v1_3_0 diff --git a/packages/protocol-kit/src/contracts/SafeProxyFactory/v1.4.1/SafeProxyFactoryContract_v1_4_1.ts b/packages/protocol-kit/src/contracts/SafeProxyFactory/v1.4.1/SafeProxyFactoryContract_v1_4_1.ts index 336502a60..07e808c92 100644 --- a/packages/protocol-kit/src/contracts/SafeProxyFactory/v1.4.1/SafeProxyFactoryContract_v1_4_1.ts +++ b/packages/protocol-kit/src/contracts/SafeProxyFactory/v1.4.1/SafeProxyFactoryContract_v1_4_1.ts @@ -1,7 +1,4 @@ -import { parseEventLogs } from 'viem' -import SafeProxyFactoryBaseContract, { - CreateProxyProps -} from '@safe-global/protocol-kit/contracts/SafeProxyFactory/SafeProxyFactoryBaseContract' +import SafeProxyFactoryBaseContract from '@safe-global/protocol-kit/contracts/SafeProxyFactory/SafeProxyFactoryBaseContract' import { DeploymentType } from '@safe-global/protocol-kit/types' import { SafeProxyFactoryContract_v1_4_1_Abi, @@ -10,8 +7,6 @@ import { safeProxyFactory_1_4_1_ContractArtifacts } from '@safe-global/types-kit' import SafeProvider from '@safe-global/protocol-kit/SafeProvider' -import { waitForTransactionReceipt } from '@safe-global/protocol-kit/utils' -import { asHex } from '@safe-global/protocol-kit/utils/types' /** * SafeProxyFactoryContract_v1_4_1 is the implementation specific to the Safe Proxy Factory contract version 1.4.1. @@ -102,57 +97,6 @@ class SafeProxyFactoryContract_v1_4_1 ) => { return [await this.write('createProxyWithNonce', args)] } - - /** - * Allows to create new proxy contract and execute a message call to the new proxy within one transaction. - * @param {CreateProxyProps} props - Properties for the new proxy contract. - * @returns The address of the new proxy contract. - */ - async createProxyWithOptions({ - safeSingletonAddress, - initializer, - saltNonce, - options, - callback - }: CreateProxyProps): Promise { - const saltNonceBigInt = BigInt(saltNonce) - - if (saltNonceBigInt < 0) throw new Error('saltNonce must be greater than or equal to 0') - - if (options && !options.gasLimit) { - options.gasLimit = ( - await this.estimateGas( - 'createProxyWithNonce', - [safeSingletonAddress, asHex(initializer), saltNonceBigInt], - { ...options } - ) - ).toString() - } - - const coverted = this.convertOptions(options) - const proxyAddress = await this.getWallet() - .writeContract({ - address: this.contractAddress, - abi: this.contractAbi, - functionName: 'createProxyWithNonce', - args: [safeSingletonAddress, asHex(initializer), saltNonceBigInt], - ...coverted - }) - .then(async (hash) => { - if (callback) { - callback(hash) - } - const { logs } = await waitForTransactionReceipt(this.runner, hash) - const events = parseEventLogs({ logs, abi: this.contractAbi }) - const proxyCreationEvent = events.find((event) => event?.eventName === 'ProxyCreation') - if (!proxyCreationEvent || !proxyCreationEvent.args) { - throw new Error('SafeProxy was not deployed correctly') - } - return proxyCreationEvent.args.proxy - }) - - return proxyAddress - } } export default SafeProxyFactoryContract_v1_4_1 diff --git a/packages/protocol-kit/src/contracts/SafeWebAuthnSharedSigner/SafeWebAuthnSharedSignerBaseContract.ts b/packages/protocol-kit/src/contracts/SafeWebAuthnSharedSigner/SafeWebAuthnSharedSignerBaseContract.ts index b9c138e2d..dea48e4ba 100644 --- a/packages/protocol-kit/src/contracts/SafeWebAuthnSharedSigner/SafeWebAuthnSharedSignerBaseContract.ts +++ b/packages/protocol-kit/src/contracts/SafeWebAuthnSharedSigner/SafeWebAuthnSharedSignerBaseContract.ts @@ -2,17 +2,9 @@ import { Abi } from 'abitype' import SafeProvider from '@safe-global/protocol-kit/SafeProvider' import { DeploymentType } from '@safe-global/protocol-kit/types' import BaseContract from '@safe-global/protocol-kit/contracts/BaseContract' -import { - SafeVersion, - TransactionOptions, - CreateProxyProps as CreateProxyPropsGeneral -} from '@safe-global/types-kit' +import { SafeVersion } from '@safe-global/types-kit' import { contractName } from '@safe-global/protocol-kit/contracts/config' -export interface CreateProxyProps extends CreateProxyPropsGeneral { - options?: TransactionOptions -} - /** * Abstract class SafeWebAuthnSharedSignerBaseContract extends BaseContract to specifically integrate with the SafeWebAuthnSharedSigner contract. * It is designed to be instantiated for different versions of the Safe contract. diff --git a/packages/protocol-kit/src/contracts/SafeWebAuthnSignerFactory/SafeWebAuthnSignerFactoryBaseContract.ts b/packages/protocol-kit/src/contracts/SafeWebAuthnSignerFactory/SafeWebAuthnSignerFactoryBaseContract.ts index b5c63f4fb..c599cb7c9 100644 --- a/packages/protocol-kit/src/contracts/SafeWebAuthnSignerFactory/SafeWebAuthnSignerFactoryBaseContract.ts +++ b/packages/protocol-kit/src/contracts/SafeWebAuthnSignerFactory/SafeWebAuthnSignerFactoryBaseContract.ts @@ -2,17 +2,9 @@ import { Abi } from 'abitype' import SafeProvider from '@safe-global/protocol-kit/SafeProvider' import { DeploymentType } from '@safe-global/protocol-kit/types' import BaseContract from '@safe-global/protocol-kit/contracts/BaseContract' -import { - SafeVersion, - TransactionOptions, - CreateProxyProps as CreateProxyPropsGeneral -} from '@safe-global/types-kit' +import { SafeVersion } from '@safe-global/types-kit' import { contractName } from '@safe-global/protocol-kit/contracts/config' -export interface CreateProxyProps extends CreateProxyPropsGeneral { - options?: TransactionOptions -} - /** * Abstract class SafeWebAuthnSignerFactoryBaseContract extends BaseContract to specifically integrate with the SafeWebAuthnSignerFactory contract. * It is designed to be instantiated for different versions of the Safe contract. diff --git a/packages/protocol-kit/src/contracts/utils.ts b/packages/protocol-kit/src/contracts/utils.ts index 12e30903d..6250eebda 100644 --- a/packages/protocol-kit/src/contracts/utils.ts +++ b/packages/protocol-kit/src/contracts/utils.ts @@ -9,7 +9,10 @@ import { parseAbi, toHex, Client, - WalletClient + WalletClient, + toEventHash, + FormattedTransactionReceipt, + decodeEventLog } from 'viem' import { waitForTransactionReceipt } from 'viem/actions' import { DEFAULT_SAFE_VERSION } from '@safe-global/protocol-kit/contracts/config' @@ -398,6 +401,68 @@ export const validateSafeDeploymentConfig = ({ saltNonce }: SafeDeploymentConfig throw new Error('saltNonce must be greater than or equal to 0') } +/** + * Returns the ProxyCreation Event based on the Safe version + * + * based on the Safe Version, we have different proxyCreation events + * + * @param {safeVersion} safeVersion - The Safe Version. + * @returns {string} - The ProxyCreation event. + */ + +function getProxyCreationEvent(safeVersion: SafeVersion): string { + // Events inputs here are left unnamed to deal with the decoding as a list: https://github.com/wevm/viem/blob/632d4b9fa074f4da722e26b28607947d2c14ad2d/src/utils/abi/decodeEventLog.ts#L128 + const isLegacyProxyCreationEvent = semverSatisfies(safeVersion, '<1.3.0') + + if (isLegacyProxyCreationEvent) { + return 'event ProxyCreation(address)' // v1.0.0, 1.1.1 & v1.2.0 + } + + if (semverSatisfies(safeVersion, '=1.3.0')) { + return 'event ProxyCreation(address, address)' // v1.3.0 + } + + return 'event ProxyCreation(address indexed, address)' // >= v1.4.1 +} + +/** + * Returns the address of a SafeProxy Address from the transaction receipt. + * + * This function looks for a ProxyCreation event in the transaction receipt logs to get address of the deployed SafeProxy. + * + * @param {FormattedTransactionReceipt} txReceipt - The transaction receipt containing logs. + * @param {safeVersion} safeVersion - The Safe Version. + * @returns {string} - The address of the deployed SafeProxy. + * @throws {Error} - Throws an error if the SafeProxy was not deployed correctly. + */ + +export function getSafeAddressFromDeploymentTx( + txReceipt: FormattedTransactionReceipt, + safeVersion: SafeVersion +): string { + const eventHash = toEventHash(getProxyCreationEvent(safeVersion)) + const proxyCreationEvent = txReceipt?.logs.find((event) => event.topics[0] === eventHash) + + if (!proxyCreationEvent) { + throw new Error('SafeProxy was not deployed correctly') + } + + const { data, topics } = proxyCreationEvent + + const { args } = decodeEventLog({ + abi: parseAbi([getProxyCreationEvent(safeVersion)]), + eventName: 'ProxyCreation', + data, + topics + }) + + if (!args || !args.length) { + throw new Error('SafeProxy was not deployed correctly') + } + + return args[0] as string +} + /** * Generates a zkSync Era address. zkSync Era uses a distinct address derivation method compared to Ethereum * see: https://docs.zksync.io/build/developer-reference/ethereum-differences/evm-instructions/#address-derivation diff --git a/packages/protocol-kit/src/index.ts b/packages/protocol-kit/src/index.ts index 2b845d325..cd12b85cf 100644 --- a/packages/protocol-kit/src/index.ts +++ b/packages/protocol-kit/src/index.ts @@ -25,10 +25,10 @@ import { encodeCreateProxyWithNonce, encodeSetupCallData, predictSafeAddress, - getPredictedSafeAddressInitCode + getPredictedSafeAddressInitCode, + getSafeAddressFromDeploymentTx } from './contracts/utils' import ContractManager from './managers/contractManager' -import SafeFactory from './SafeFactory' import { EthSafeSignature, estimateTxBaseGas, @@ -84,7 +84,6 @@ export { MultiSendBaseContract, PREDETERMINED_SALT_NONCE, SafeBaseContract, - SafeFactory, SafeProxyFactoryBaseContract, SafeTransactionOptionalProps, SignMessageLibBaseContract, @@ -115,6 +114,7 @@ export { preimageSafeMessageHash, getEip712TxTypes, getEip712MessageTypes, + getSafeAddressFromDeploymentTx, hashSafeMessage, generateTypedData, SafeProvider, diff --git a/packages/protocol-kit/src/types/index.ts b/packages/protocol-kit/src/types/index.ts index 54c7b9fbe..67ce5f8a8 100644 --- a/packages/protocol-kit/src/types/index.ts +++ b/packages/protocol-kit/src/types/index.ts @@ -1,6 +1,5 @@ export * from './contracts' export * from './safeConfig' -export * from './safeFactory' export * from './safeProvider' export * from './signing' export * from './transactions' diff --git a/packages/protocol-kit/src/types/safeFactory.ts b/packages/protocol-kit/src/types/safeFactory.ts deleted file mode 100644 index 5cacaf46b..000000000 --- a/packages/protocol-kit/src/types/safeFactory.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { SafeVersion, TransactionOptions } from '@safe-global/types-kit' - -import { SafeProviderConfig } from './safeProvider' -import { SafeAccountConfig } from './safeConfig' -import { ContractNetworksConfig } from './contracts' - -export type DeploySafeProps = { - safeAccountConfig: SafeAccountConfig - saltNonce?: string - options?: TransactionOptions - callback?: (txHash: string) => void -} - -export type SafeFactoryConfig = { - provider: SafeProviderConfig['provider'] - signer?: SafeProviderConfig['signer'] - /** safeVersion - Versions of the Safe deployed by this Factory contract */ - safeVersion?: SafeVersion - /** isL1SafeSingleton - Forces to use the Safe L1 version of the contract instead of the L2 version */ - isL1SafeSingleton?: boolean - /** contractNetworks - Contract network configuration */ - contractNetworks?: ContractNetworksConfig -} - -export type SafeFactoryInitConfig = { - provider: SafeProviderConfig['provider'] - signer?: SafeProviderConfig['signer'] - privateKeyOrMnemonic?: string - /** safeVersion - Versions of the Safe deployed by this Factory contract */ - safeVersion: SafeVersion - /** isL1SafeSingleton - Forces to use the Safe L1 version of the contract instead of the L2 version */ - isL1SafeSingleton?: boolean - /** contractNetworks - Contract network configuration */ - contractNetworks?: ContractNetworksConfig -} diff --git a/packages/protocol-kit/src/utils/transactions/utils.ts b/packages/protocol-kit/src/utils/transactions/utils.ts index 35ac3f6b2..05767898f 100644 --- a/packages/protocol-kit/src/utils/transactions/utils.ts +++ b/packages/protocol-kit/src/utils/transactions/utils.ts @@ -22,7 +22,8 @@ import { SafeTransactionData, SafeTransactionDataPartial, SafeVersion, - TransactionOptions + TransactionOptions, + Transaction } from '@safe-global/types-kit' import semverSatisfies from 'semver/functions/satisfies' import { estimateGas, estimateTxGas } from './gas' @@ -240,8 +241,8 @@ export function toEstimateGasParameters(tx: SafeProviderTransaction): EstimateGa return params } -export function toCallGasParameters( - tx: SafeProviderTransaction +export function toTransactionRequest( + tx: SafeProviderTransaction | Transaction ): UnionOmit { const params: UnionOmit = isLegacyTransaction(tx) ? createLegacyTxOptions(tx) diff --git a/packages/protocol-kit/tests/e2e/core.test.ts b/packages/protocol-kit/tests/e2e/core.test.ts index ec63fff7d..811ed2a09 100644 --- a/packages/protocol-kit/tests/e2e/core.test.ts +++ b/packages/protocol-kit/tests/e2e/core.test.ts @@ -6,7 +6,7 @@ import { getSafeWithOwners, waitTransactionReceipt } from '@safe-global/testing-kit' -import Safe, { PredictedSafeProps, SafeFactory } from '@safe-global/protocol-kit/index' +import Safe, { PredictedSafeProps } from '@safe-global/protocol-kit/index' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import { getEip1193Provider } from './utils/setupProvider' @@ -167,7 +167,7 @@ describe('Safe Info', () => { itif(safeVersionDeployed >= '1.3.0')( 'should return the address of a Safe >=v1.3.0 that is not deployed', async () => { - const { predictedSafe, contractNetworks } = await setupTests() + const { predictedSafe, contractNetworks, accounts } = await setupTests() const safeSdk = await Safe.init({ provider, predictedSafe, @@ -175,15 +175,18 @@ describe('Safe Info', () => { }) const safeAddress = await safeSdk.getAddress() - const safeFactory = await SafeFactory.init({ - provider, - safeVersion: safeVersionDeployed, - contractNetworks - }) - const deployedSdk = await safeFactory.deploySafe(predictedSafe) - const expectedSafeAddress = await deployedSdk.getAddress() + chai.expect(await safeSdk.isSafeDeployed()).to.be.false + + const deploymentTransaction = await safeSdk.createSafeDeploymentTransaction() + + const signer = accounts[0].signer + await signer.sendTransaction(deploymentTransaction) + + const expectedSafeAddress = await safeSdk.getAddress() chai.expect(safeAddress).to.be.eq(expectedSafeAddress) + + chai.expect(await safeSdk.isSafeDeployed()).to.be.true } ) diff --git a/packages/protocol-kit/tests/e2e/createSafeDeploymentTransaction.test.ts b/packages/protocol-kit/tests/e2e/createSafeDeploymentTransaction.test.ts index 978e0dcbb..2985706f5 100644 --- a/packages/protocol-kit/tests/e2e/createSafeDeploymentTransaction.test.ts +++ b/packages/protocol-kit/tests/e2e/createSafeDeploymentTransaction.test.ts @@ -179,26 +179,6 @@ describe('createSafeDeploymentTransaction', () => { .to.contains(predeterminedSaltNonceEncoded.replace('0x', '')) }) - it('should include the custom salt nonce in the Safe deployment data', async () => { - const { contractNetworks, predictedSafe } = await setupTests() - - const safeProvider = new SafeProvider({ provider }) - const safeSdk = await Safe.init({ - provider, - predictedSafe, - contractNetworks - }) - - const customSaltNonce = '123456789' - - const customSaltNonceEncoded = safeProvider.encodeParameters('uint256', [customSaltNonce]) - - const deploymentTransaction = await safeSdk.createSafeDeploymentTransaction(customSaltNonce) - - // custom salt nonce included in the deployment data - chai.expect(deploymentTransaction.data).to.contains(customSaltNonceEncoded.replace('0x', '')) - }) - it('should include the salt nonce included in the safeDeploymentConfig in the Safe deployment data', async () => { const { contractNetworks, predictedSafe } = await setupTests() @@ -220,7 +200,7 @@ describe('createSafeDeploymentTransaction', () => { .getSafeProvider() .encodeParameters('uint256', [customSaltNonce]) - const deploymentTransaction = await safeSdk.createSafeDeploymentTransaction(customSaltNonce) + const deploymentTransaction = await safeSdk.createSafeDeploymentTransaction() // custom salt nonce included in the deployment data chai.expect(deploymentTransaction.data).to.contains(saltNonceEncoded.replace('0x', '')) diff --git a/packages/protocol-kit/tests/e2e/safeDeployment.test.ts b/packages/protocol-kit/tests/e2e/safeDeployment.test.ts new file mode 100644 index 000000000..7c5c1878e --- /dev/null +++ b/packages/protocol-kit/tests/e2e/safeDeployment.test.ts @@ -0,0 +1,899 @@ +import { DEFAULT_SAFE_VERSION } from '@safe-global/protocol-kit/contracts/config' +import { + getCompatibilityFallbackHandler, + getDefaultCallbackHandler, + getFactory, + itif, + safeVersionDeployed, + setupTests, + waitTransactionReceipt +} from '@safe-global/testing-kit' +import Safe, { + getSafeAddressFromDeploymentTx, + PredictedSafeProps, + SafeAccountConfig +} from '@safe-global/protocol-kit/index' +import { ZERO_ADDRESS } from '@safe-global/protocol-kit/utils/constants' +import chai from 'chai' +import chaiAsPromised from 'chai-as-promised' +import { getEip1193Provider } from './utils/setupProvider' +// import { getAccounts } from './utils/setupTestNetwork' + +chai.use(chaiAsPromised) + +describe('Safe Deployment', () => { + const provider = getEip1193Provider() + + describe('init', async () => { + it('should fail if the SafeProxyFactory contract provided is not deployed', async () => { + const { chainId, accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 2 + const safeAccountConfig: SafeAccountConfig = { owners, threshold } + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed + } + } + + const contractNetworksWithoutSafeProxyFactoryContract = { + [chainId.toString()]: { + ...contractNetworks[chainId.toString()], + safeProxyFactoryAddress: ZERO_ADDRESS, + safeProxyFactoryAbi: (await getFactory()).abi + } + } + + const safeSDK = await Safe.init({ + provider, + contractNetworks: contractNetworksWithoutSafeProxyFactoryContract, + predictedSafe + }) + + await chai + .expect(safeSDK.createSafeDeploymentTransaction()) + .rejectedWith('SafeProxyFactory contract is not deployed on the current network') + }) + }) + + describe('predictSafeAddress', async () => { + it('should fail if there are no owners', async () => { + const { contractNetworks } = await setupTests() + const owners: string[] = [] + const threshold = 2 + const safeAccountConfig: SafeAccountConfig = { owners, threshold } + const saltNonce = '1' + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed, + saltNonce + } + } + + const safeSDK = await Safe.init({ provider, contractNetworks, predictedSafe }) + + await chai + .expect(safeSDK.createSafeDeploymentTransaction()) + .rejectedWith('Owner list must have at least one owner') + }) + + it('should fail if the threshold is lower than 0', async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const invalidThreshold = 0 + const saltNonce = '1' + const safeAccountConfig: SafeAccountConfig = { owners, threshold: invalidThreshold } + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed, + saltNonce + } + } + + const safeSDK = await Safe.init({ provider, contractNetworks, predictedSafe }) + + await chai + .expect(safeSDK.createSafeDeploymentTransaction()) + .rejectedWith('Threshold must be greater than or equal to 1') + }) + + it('should fail if the threshold is higher than the number of owners', async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const invalidThreshold = 3 + const safeAccountConfig: SafeAccountConfig = { owners, threshold: invalidThreshold } + const saltNonce = '1' + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed, + saltNonce + } + } + + const safeSDK = await Safe.init({ provider, contractNetworks, predictedSafe }) + + await chai + .expect(safeSDK.createSafeDeploymentTransaction()) + .rejectedWith('Threshold must be lower than or equal to owners length') + }) + + it('should fail if the saltNonce is lower than 0', async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 2 + const safeAccountConfig: SafeAccountConfig = { owners, threshold } + const invalidSaltNonce = '-1' + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed, + saltNonce: invalidSaltNonce + } + } + + const safeSDK = await Safe.init({ provider, contractNetworks, predictedSafe }) + + await chai + .expect(safeSDK.createSafeDeploymentTransaction()) + .rejectedWith('saltNonce must be greater than or equal to 0') + }) + + itif(safeVersionDeployed < '1.3.0')( + 'should fail if the safe Version is lower than 1.3.0', + async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 2 + const safeAccountConfig: SafeAccountConfig = { owners, threshold } + const saltNonce = '12345' + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed, + saltNonce + } + } + + const safeSDK = await Safe.init({ provider, contractNetworks, predictedSafe }) + + await chai + .expect(safeSDK.getAddress()) + .rejectedWith( + 'Account Abstraction functionality is not available for Safes with version lower than v1.3.0' + ) + } + ) + + itif(safeVersionDeployed >= '1.3.0')('should predict a new Safe with saltNonce', async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 2 + const safeAccountConfig: SafeAccountConfig = { owners, threshold } + const saltNonce = '12345' + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed, + saltNonce + } + } + + const safeSDK = await Safe.init({ provider, contractNetworks, predictedSafe }) + + const counterfactualSafeAddress = await safeSDK.getAddress() + const deploymentTransaction = await safeSDK.createSafeDeploymentTransaction() + + const signer = account1.signer + await signer.sendTransaction(deploymentTransaction) + + chai.expect(counterfactualSafeAddress).to.be.eq(await safeSDK.getAddress()) + chai.expect(threshold).to.be.eq(await safeSDK.getThreshold()) + const deployedSafeOwners = await safeSDK.getOwners() + chai.expect(deployedSafeOwners.toString()).to.be.eq(owners.toString()) + chai.expect(safeSDK.getContractVersion()).to.be.eq(safeVersionDeployed) + }) + + itif(safeVersionDeployed > '1.0.0')( + 'should predict a new Safe with the default CompatibilityFallbackHandler', + async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 2 + const safeAccountConfig: SafeAccountConfig = { owners, threshold } + const saltNonce = '12345' + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed, + saltNonce + } + } + + const safeSDK = await Safe.init({ provider, contractNetworks, predictedSafe }) + + const deploymentTransaction = await safeSDK.createSafeDeploymentTransaction() + + const signer = accounts[0].signer + + const txHash = await signer.sendTransaction(deploymentTransaction) + + const txReceipt = await waitTransactionReceipt(txHash) + + const safeAddress = getSafeAddressFromDeploymentTx(txReceipt, safeVersionDeployed) + + const safeSDKDeployed = await Safe.init({ + provider, + contractNetworks, + safeAddress + }) + + const compatibilityFallbackHandler = (await getCompatibilityFallbackHandler()).contract + .address + chai + .expect(compatibilityFallbackHandler) + .to.be.eq(await safeSDKDeployed.getFallbackHandler()) + chai.expect(safeSDKDeployed.getContractVersion()).to.be.eq(safeVersionDeployed) + } + ) + + itif(safeVersionDeployed > '1.3.0')( + 'should predict a new Safe with a custom fallback handler', + async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 2 + const defaultCallbackHandler = await getDefaultCallbackHandler() + const safeAccountConfig: SafeAccountConfig = { + owners, + threshold, + fallbackHandler: defaultCallbackHandler.address + } + const saltNonce = '12345' + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed, + saltNonce + } + } + + const safeSDK = await Safe.init({ provider, contractNetworks, predictedSafe }) + + const deploymentTransaction = await safeSDK.createSafeDeploymentTransaction() + + const signer = accounts[0].signer + + const txHash = await signer.sendTransaction(deploymentTransaction) + + const txReceipt = await waitTransactionReceipt(txHash) + + const safeAddress = getSafeAddressFromDeploymentTx(txReceipt, safeVersionDeployed) + + const safeSDKDeployed = await Safe.init({ + provider, + contractNetworks, + safeAddress + }) + + chai + .expect(defaultCallbackHandler.address) + .to.be.eq(await safeSDKDeployed.getFallbackHandler()) + } + ) + }) + + describe('deploySafe', async () => { + itif(safeVersionDeployed >= '1.3.0')('should fail if the Safe is deployed', async () => { + const { contractNetworks, accounts } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 2 + const safeAccountConfig: SafeAccountConfig = { owners, threshold } + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed + } + } + + const safeSDK = await Safe.init({ + provider, + contractNetworks, + predictedSafe + }) + + chai.expect(await safeSDK.isSafeDeployed()).to.be.false + + const deploymentTransaction = await safeSDK.createSafeDeploymentTransaction() + + const signer = accounts[0].signer + await signer.sendTransaction(deploymentTransaction) + + await chai + .expect(safeSDK.createSafeDeploymentTransaction()) + .rejectedWith('Safe already deployed') + + chai.expect(await safeSDK.isSafeDeployed()).to.be.true + }) + + it('should fail if there are no owners', async () => { + const { contractNetworks } = await setupTests() + const owners: string[] = [] + const threshold = 2 + const safeAccountConfig: SafeAccountConfig = { owners, threshold } + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed + } + } + + const safeSDK = await Safe.init({ + provider, + contractNetworks, + predictedSafe + }) + + await chai + .expect(safeSDK.createSafeDeploymentTransaction()) + .rejectedWith('Owner list must have at least one owner') + }) + + it('should fail if the threshold is lower than 0', async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 0 + const safeAccountConfig: SafeAccountConfig = { owners, threshold } + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed + } + } + + const safeSDK = await Safe.init({ + provider, + contractNetworks, + predictedSafe + }) + + await chai + .expect(safeSDK.createSafeDeploymentTransaction()) + .rejectedWith('Threshold must be greater than or equal to 1') + }) + + it('should fail if the threshold is higher than the number of owners', async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 3 + const safeAccountConfig: SafeAccountConfig = { owners, threshold } + + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed + } + } + + const safeSDK = await Safe.init({ + provider, + contractNetworks, + predictedSafe + }) + + await chai + .expect(safeSDK.createSafeDeploymentTransaction()) + .rejectedWith('Threshold must be lower than or equal to owners length') + }) + + it('should fail if the saltNonce is lower than 0', async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 2 + const safeAccountConfig: SafeAccountConfig = { owners, threshold } + const invalidSaltNonce = '-1' + + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed, + saltNonce: invalidSaltNonce + } + } + + const safeSDK = await Safe.init({ + provider, + contractNetworks, + predictedSafe + }) + + await chai + .expect(safeSDK.createSafeDeploymentTransaction()) + .rejectedWith('saltNonce must be greater than or equal to 0') + }) + + itif(safeVersionDeployed > '1.0.0')( + 'should deploy a new Safe with custom fallback handler', + async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 2 + const customFallbackHandler = accounts[3].address + const safeAccountConfig: SafeAccountConfig = { + owners, + threshold, + fallbackHandler: customFallbackHandler + } + + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed + } + } + + const safeSDK = await Safe.init({ + provider, + contractNetworks, + predictedSafe + }) + + if (safeVersionDeployed >= '1.3.0') { + chai.expect(await safeSDK.isSafeDeployed()).to.be.false + } + + const deploymentTransaction = await safeSDK.createSafeDeploymentTransaction() + + const signer = accounts[0].signer + const txHash = await signer.sendTransaction(deploymentTransaction) + + const txReceipt = await waitTransactionReceipt(txHash) + + const safeAddress = getSafeAddressFromDeploymentTx(txReceipt, safeVersionDeployed) + + const safeSDKDeployed = await Safe.init({ + provider, + contractNetworks, + safeAddress + }) + + const deployedSafeOwners = await safeSDKDeployed.getOwners() + const deployedSafeThreshold = await safeSDKDeployed.getThreshold() + const fallbackHandler = await safeSDKDeployed.getFallbackHandler() + + chai.expect(deployedSafeOwners.toString()).to.be.eq(owners.toString()) + chai.expect(deployedSafeThreshold).to.be.eq(threshold) + chai.expect(customFallbackHandler).to.be.eq(fallbackHandler) + chai.expect(await safeSDKDeployed.isSafeDeployed()).to.be.true + chai.expect(await safeSDKDeployed.getContractVersion()).to.be.eq(safeVersionDeployed) + chai.expect(await safeSDKDeployed.getNonce()).to.be.eq(0) + } + ) + + itif(safeVersionDeployed > '1.0.0')( + 'should deploy a new Safe with the default CompatibilityFallbackHandler', + async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 2 + const safeAccountConfig: SafeAccountConfig = { + owners, + threshold + } + + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed + } + } + + const safeSDK = await Safe.init({ + provider, + contractNetworks, + predictedSafe + }) + + if (safeVersionDeployed >= '1.3.0') { + chai.expect(await safeSDK.isSafeDeployed()).to.be.false + } + + const deploymentTransaction = await safeSDK.createSafeDeploymentTransaction() + + const signer = accounts[0].signer + const txHash = await signer.sendTransaction(deploymentTransaction) + + const txReceipt = await waitTransactionReceipt(txHash) + + const safeAddress = getSafeAddressFromDeploymentTx(txReceipt, safeVersionDeployed) + + const safeSDKDeployed = await Safe.init({ + provider, + contractNetworks, + safeAddress + }) + + const defaultCompatibilityFallbackHandler = (await getCompatibilityFallbackHandler()) + .contract.address + + chai + .expect(defaultCompatibilityFallbackHandler) + .to.be.eq(await safeSDKDeployed.getFallbackHandler()) + + chai.expect(await safeSDKDeployed.isSafeDeployed()).to.be.true + chai.expect(await safeSDKDeployed.getContractVersion()).to.be.eq(safeVersionDeployed) + chai.expect(await safeSDKDeployed.getNonce()).to.be.eq(0) + } + ) + + it('should deploy a new Safe without saltNonce', async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 2 + const safeAccountConfig: SafeAccountConfig = { owners, threshold } + + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed + } + } + + const safeSDK = await Safe.init({ + provider, + contractNetworks, + predictedSafe + }) + + if (safeVersionDeployed >= '1.3.0') { + chai.expect(await safeSDK.isSafeDeployed()).to.be.false + } + + const deploymentTransaction = await safeSDK.createSafeDeploymentTransaction() + + const signer = accounts[0].signer + const txHash = await signer.sendTransaction(deploymentTransaction) + + const txReceipt = await waitTransactionReceipt(txHash) + + const safeAddress = getSafeAddressFromDeploymentTx(txReceipt, safeVersionDeployed) + + const safeSDKDeployed = await Safe.init({ + provider, + contractNetworks, + safeAddress + }) + + const deployedSafeOwners = await safeSDKDeployed.getOwners() + const deployedSafeThreshold = await safeSDKDeployed.getThreshold() + + chai.expect(deployedSafeOwners.toString()).to.be.eq(owners.toString()) + chai.expect(deployedSafeThreshold).to.be.eq(threshold) + chai.expect(await safeSDKDeployed.isSafeDeployed()).to.be.true + chai.expect(await safeSDKDeployed.getContractVersion()).to.be.eq(safeVersionDeployed) + chai.expect(await safeSDKDeployed.getNonce()).to.be.eq(0) + }) + + it('should deploy a new Safe with saltNonce', async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 2 + const safeAccountConfig: SafeAccountConfig = { owners, threshold } + const saltNonce = '1' + + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed, + saltNonce + } + } + + const safeSDK = await Safe.init({ + provider, + contractNetworks, + predictedSafe + }) + + if (safeVersionDeployed >= '1.3.0') { + chai.expect(await safeSDK.isSafeDeployed()).to.be.false + } + + const deploymentTransaction = await safeSDK.createSafeDeploymentTransaction() + + const signer = accounts[0].signer + const txHash = await signer.sendTransaction(deploymentTransaction) + + const txReceipt = await waitTransactionReceipt(txHash) + + const safeAddress = getSafeAddressFromDeploymentTx(txReceipt, safeVersionDeployed) + + const safeSDKDeployed = await Safe.init({ + provider, + contractNetworks, + safeAddress + }) + + const deployedSafeOwners = await safeSDKDeployed.getOwners() + const deployedSafeThreshold = await safeSDKDeployed.getThreshold() + + chai.expect(deployedSafeOwners.toString()).to.be.eq(owners.toString()) + chai.expect(deployedSafeThreshold).to.be.eq(threshold) + chai.expect(await safeSDKDeployed.isSafeDeployed()).to.be.true + chai.expect(safeSDKDeployed.getContractVersion()).to.be.eq(safeVersionDeployed) + chai.expect(await safeSDKDeployed.getNonce()).to.be.eq(0) + }) + + itif(safeVersionDeployed == '1.3.0')( + 'should deploy the v1.3.0 Safe version by default', + async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 2 + const safeAccountConfig: SafeAccountConfig = { owners, threshold } + + const predictedSafe: PredictedSafeProps = { + safeAccountConfig + } + + const safeSDK = await Safe.init({ + provider, + contractNetworks, + predictedSafe + }) + + chai.expect(await safeSDK.isSafeDeployed()).to.be.false + + const deploymentTransaction = await safeSDK.createSafeDeploymentTransaction() + + const signer = accounts[0].signer + const txHash = await signer.sendTransaction(deploymentTransaction) + + const txReceipt = await waitTransactionReceipt(txHash) + + const safeAddress = getSafeAddressFromDeploymentTx(txReceipt, DEFAULT_SAFE_VERSION) + + const safeSDKDeployed = await Safe.init({ + provider, + contractNetworks, + safeAddress + }) + + chai.expect(await safeSDKDeployed.isSafeDeployed()).to.be.true + chai.expect(safeSDKDeployed.getContractVersion()).to.be.eq(DEFAULT_SAFE_VERSION) + chai.expect(await safeSDKDeployed.getNonce()).to.be.eq(0) + } + ) + + it('should deploy a specific Safe version', async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 2 + const safeAccountConfig: SafeAccountConfig = { owners, threshold } + + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed + } + } + + const safeSDK = await Safe.init({ + provider, + contractNetworks, + predictedSafe + }) + + if (safeVersionDeployed >= '1.3.0') { + chai.expect(await safeSDK.isSafeDeployed()).to.be.false + } + + const deploymentTransaction = await safeSDK.createSafeDeploymentTransaction() + + const signer = accounts[0].signer + const txHash = await signer.sendTransaction(deploymentTransaction) + + const txReceipt = await waitTransactionReceipt(txHash) + + const safeAddress = getSafeAddressFromDeploymentTx(txReceipt, safeVersionDeployed) + + const safeSDKDeployed = await Safe.init({ + provider, + contractNetworks, + safeAddress + }) + + chai.expect(await safeSDKDeployed.isSafeDeployed()).to.be.true + chai.expect(safeSDKDeployed.getContractVersion()).to.be.eq(safeVersionDeployed) + chai.expect(await safeSDKDeployed.getNonce()).to.be.eq(0) + }) + + describe('counterfactual deployment via wrapSafeTransactionIntoDeploymentBatch', () => { + itif(safeVersionDeployed >= '1.3.0')( + 'should deploy the Safe Account and execute one transaction', + async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 1 + const safeAccountConfig: SafeAccountConfig = { owners, threshold } + + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed + } + } + + const safeSDK = await Safe.init({ + provider, + contractNetworks, + predictedSafe + }) + + const transaction = { + to: account2.address, + value: '0', + data: '0x' + } + + const safeTransaction = await safeSDK.createTransaction({ transactions: [transaction] }) + + const signedSafeTransaction = await safeSDK.signTransaction(safeTransaction) + + const deploymentTransaction = + await safeSDK.wrapSafeTransactionIntoDeploymentBatch(signedSafeTransaction) + + chai.expect(await safeSDK.isSafeDeployed()).to.be.false + + const signer = accounts[0].signer + const txHash = await signer.sendTransaction(deploymentTransaction) + + const txReceipt = await waitTransactionReceipt(txHash) + + const safeAddress = getSafeAddressFromDeploymentTx(txReceipt, safeVersionDeployed) + + const safeSDKDeployed = await Safe.init({ + provider, + contractNetworks, + safeAddress + }) + + chai.expect(await safeSDKDeployed.isSafeDeployed()).to.be.true + chai.expect(safeSDKDeployed.getContractVersion()).to.be.eq(safeVersionDeployed) + chai.expect(await safeSDKDeployed.getNonce()).to.be.eq(1) + } + ) + itif(safeVersionDeployed >= '1.3.0')( + 'should deploy the Safe Account and execute a batch of transactions', + async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 1 + const safeAccountConfig: SafeAccountConfig = { owners, threshold } + + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed + } + } + + const safeSDK = await Safe.init({ + provider, + contractNetworks, + predictedSafe + }) + + const firstTransaction = { + to: account1.address, + value: '0', + data: '0x' + } + + const secondTransaction = { + to: account2.address, + value: '0', + data: '0x' + } + + // batch to execute after the deployment + const transactions = [firstTransaction, secondTransaction] + + const safeTransaction = await safeSDK.createTransaction({ transactions }) + + const signedSafeTransaction = await safeSDK.signTransaction(safeTransaction) + + chai.expect(await safeSDK.isSafeDeployed()).to.be.false + + const deploymentTransaction = + await safeSDK.wrapSafeTransactionIntoDeploymentBatch(signedSafeTransaction) + + const signer = accounts[0].signer + const txHash = await signer.sendTransaction(deploymentTransaction) + + const txReceipt = await waitTransactionReceipt(txHash) + + const safeAddress = getSafeAddressFromDeploymentTx(txReceipt, safeVersionDeployed) + + const safeSDKDeployed = await Safe.init({ + provider, + contractNetworks, + safeAddress + }) + + chai.expect(await safeSDKDeployed.isSafeDeployed()).to.be.true + chai.expect(safeSDKDeployed.getContractVersion()).to.be.eq(safeVersionDeployed) + chai.expect(await safeSDKDeployed.getNonce()).to.be.eq(1) + } + ) + + itif(safeVersionDeployed < '1.3.0')( + 'Account Abstraction functionality is not available for Safes with version lower than v1.3.0', + async () => { + const { accounts, contractNetworks } = await setupTests() + const [account1, account2] = accounts + const owners = [account1.address, account2.address] + const threshold = 1 + const safeAccountConfig: SafeAccountConfig = { owners, threshold } + + const predictedSafe: PredictedSafeProps = { + safeAccountConfig, + safeDeploymentConfig: { + safeVersion: safeVersionDeployed + } + } + + const safeSDK = await Safe.init({ + provider, + contractNetworks, + predictedSafe + }) + + const firstTransaction = { + to: account1.address, + value: '0', + data: '0x' + } + + const secondTransaction = { + to: account2.address, + value: '0', + data: '0x' + } + + // batch to execute after the deployment + const transactions = [firstTransaction, secondTransaction] + + await chai + .expect(safeSDK.createTransaction({ transactions })) + .rejectedWith( + 'Account Abstraction functionality is not available for Safes with version lower than v1.3.0' + ) + } + ) + }) + }) +}) diff --git a/packages/protocol-kit/tests/e2e/safeFactory.test.ts b/packages/protocol-kit/tests/e2e/safeFactory.test.ts deleted file mode 100644 index a4db69dbb..000000000 --- a/packages/protocol-kit/tests/e2e/safeFactory.test.ts +++ /dev/null @@ -1,440 +0,0 @@ -import { DEFAULT_SAFE_VERSION } from '@safe-global/protocol-kit/contracts/config' -import { - safeVersionDeployed, - setupTests, - itif, - getCompatibilityFallbackHandler, - getCreateCall, - getDefaultCallbackHandler, - getFactory, - getMultiSend, - getMultiSendCallOnly, - getSafeSingleton, - getSignMessageLib, - getSimulateTxAccessor -} from '@safe-global/testing-kit' -import { - ContractNetworksConfig, - DeploySafeProps, - SafeAccountConfig, - SafeFactory, - SafeProvider -} from '@safe-global/protocol-kit/index' -import { ZERO_ADDRESS } from '@safe-global/protocol-kit/utils/constants' -import chai from 'chai' -import chaiAsPromised from 'chai-as-promised' -import { getEip1193Provider } from './utils/setupProvider' - -chai.use(chaiAsPromised) - -describe('SafeProxyFactory', () => { - const provider = getEip1193Provider() - - describe('create', async () => { - it('should fail if the current network is not a default network and no contractNetworks is provided', async () => { - chai.expect(SafeFactory.init({ provider })).rejectedWith('Invalid SafeProxyFactory contract') - }) - - it('should fail if the contractNetworks provided are not deployed', async () => { - const { chainId } = await setupTests() - const contractNetworks: ContractNetworksConfig = { - [chainId.toString()]: { - safeSingletonAddress: ZERO_ADDRESS, - safeSingletonAbi: (await getSafeSingleton()).abi, - safeProxyFactoryAddress: ZERO_ADDRESS, - safeProxyFactoryAbi: (await getFactory()).abi, - multiSendAddress: ZERO_ADDRESS, - multiSendAbi: (await getMultiSend()).abi, - multiSendCallOnlyAddress: ZERO_ADDRESS, - multiSendCallOnlyAbi: (await getMultiSendCallOnly()).abi, - fallbackHandlerAddress: ZERO_ADDRESS, - fallbackHandlerAbi: (await getCompatibilityFallbackHandler()).abi, - signMessageLibAddress: ZERO_ADDRESS, - signMessageLibAbi: (await getSignMessageLib()).abi, - createCallAddress: ZERO_ADDRESS, - createCallAbi: (await getCreateCall()).abi, - simulateTxAccessorAddress: ZERO_ADDRESS, - simulateTxAccessorAbi: (await getSimulateTxAccessor()).abi - } - } - chai - .expect(SafeFactory.init({ provider, contractNetworks })) - .rejectedWith('SafeProxyFactory contract is not deployed on the current network') - }) - - it('should instantiate the SafeProxyFactory', async () => { - const { contractNetworks } = await setupTests() - const safeProvider = new SafeProvider({ provider }) - const safeFactory = await SafeFactory.init({ provider, contractNetworks }) - const networkId = await safeProvider.getChainId() - chai - .expect(safeFactory.getAddress()) - .to.be.eq(contractNetworks[networkId.toString()].safeProxyFactoryAddress) - }) - }) - - describe('getEip1193Provider', async () => { - it('should return the connected SafeProvider', async () => { - const { accounts, contractNetworks } = await setupTests() - const [account1] = accounts - const safeFactory = await SafeFactory.init({ provider, contractNetworks }) - chai.expect(await safeFactory.getSafeProvider().getSignerAddress()).to.be.eq(account1.address) - }) - }) - - describe('getChainId', async () => { - it('should return the chainId of the current network', async () => { - const { chainId, contractNetworks } = await setupTests() - const safeFactory = await SafeFactory.init({ provider, contractNetworks }) - chai.expect(await safeFactory.getChainId()).to.be.eq(chainId) - }) - }) - - describe('predictSafeAddress', async () => { - it('should fail if there are no owners', async () => { - const { contractNetworks } = await setupTests() - const safeFactory = await SafeFactory.init({ provider, contractNetworks }) - const owners: string[] = [] - const threshold = 2 - const safeAccountConfig: SafeAccountConfig = { owners, threshold } - const saltNonce = '1' - - await chai - .expect(safeFactory.predictSafeAddress(safeAccountConfig, saltNonce)) - .rejectedWith('Owner list must have at least one owner') - }) - - it('should fail if the threshold is lower than 0', async () => { - const { accounts, contractNetworks } = await setupTests() - const [account1, account2] = accounts - const safeFactory = await SafeFactory.init({ provider, contractNetworks }) - const owners = [account1.address, account2.address] - const invalidThreshold = 0 - const safeAccountConfig: SafeAccountConfig = { owners, threshold: invalidThreshold } - const saltNonce = '1' - - await chai - .expect(safeFactory.predictSafeAddress(safeAccountConfig, saltNonce)) - .rejectedWith('Threshold must be greater than or equal to 1') - }) - - it('should fail if the threshold is higher than the threshold', async () => { - const { accounts, contractNetworks } = await setupTests() - const [account1, account2] = accounts - const safeFactory = await SafeFactory.init({ provider, contractNetworks }) - const owners = [account1.address, account2.address] - const invalidThreshold = 3 - const safeAccountConfig: SafeAccountConfig = { owners, threshold: invalidThreshold } - const saltNonce = '1' - - await chai - .expect(safeFactory.predictSafeAddress(safeAccountConfig, saltNonce)) - .rejectedWith('Threshold must be lower than or equal to owners length') - }) - - it('should fail if the saltNonce is lower than 0', async () => { - const { accounts, contractNetworks } = await setupTests() - const [account1, account2] = accounts - const safeFactory = await SafeFactory.init({ - provider, - safeVersion: safeVersionDeployed, - contractNetworks - }) - const owners = [account1.address, account2.address] - const threshold = 2 - const safeAccountConfig: SafeAccountConfig = { owners, threshold } - const invalidSaltNonce = '-1' - - await chai - .expect(safeFactory.predictSafeAddress(safeAccountConfig, invalidSaltNonce)) - .rejectedWith('saltNonce must be greater than or equal to 0') - }) - - it('should predict a new Safe with saltNonce', async () => { - const { accounts, contractNetworks } = await setupTests() - const [account1, account2] = accounts - const safeFactory = await SafeFactory.init({ - provider, - safeVersion: safeVersionDeployed, - contractNetworks - }) - const owners = [account1.address, account2.address] - const threshold = 2 - const safeAccountConfig: SafeAccountConfig = { owners, threshold } - const saltNonce = '12345' - const counterfactualSafeAddress = await safeFactory.predictSafeAddress( - safeAccountConfig, - saltNonce - ) - const deploySafeProps: DeploySafeProps = { safeAccountConfig, saltNonce } - const safe = await safeFactory.deploySafe(deploySafeProps) - chai.expect(counterfactualSafeAddress).to.be.eq(await safe.getAddress()) - chai.expect(threshold).to.be.eq(await safe.getThreshold()) - const deployedSafeOwners = await safe.getOwners() - chai.expect(deployedSafeOwners.toString()).to.be.eq(owners.toString()) - }) - - itif(safeVersionDeployed > '1.0.0')( - 'should predict a new Safe with the default CompatibilityFallbackHandler', - async () => { - const { accounts, contractNetworks } = await setupTests() - const [account1, account2] = accounts - const safeFactory = await SafeFactory.init({ - provider, - safeVersion: safeVersionDeployed, - contractNetworks - }) - const owners = [account1.address, account2.address] - const threshold = 2 - const safeAccountConfig: SafeAccountConfig = { owners, threshold } - const saltNonce = '12345' - const counterfactualSafeAddress = await safeFactory.predictSafeAddress( - safeAccountConfig, - saltNonce - ) - const deploySafeProps: DeploySafeProps = { safeAccountConfig, saltNonce } - const safe = await safeFactory.deploySafe(deploySafeProps) - chai.expect(counterfactualSafeAddress).to.be.eq(await safe.getAddress()) - const compatibilityFallbackHandler = await ( - await getCompatibilityFallbackHandler() - ).contract.address - chai.expect(compatibilityFallbackHandler).to.be.eq(await safe.getFallbackHandler()) - } - ) - - itif(safeVersionDeployed > '1.0.0')( - 'should predict a new Safe with a custom fallback handler', - async () => { - const { accounts, contractNetworks } = await setupTests() - const defaultCallbackHandler = await getDefaultCallbackHandler() - const [account1, account2] = accounts - const safeFactory = await SafeFactory.init({ - provider, - safeVersion: safeVersionDeployed, - contractNetworks - }) - const owners = [account1.address, account2.address] - const threshold = 2 - const safeAccountConfig: SafeAccountConfig = { - owners, - threshold, - fallbackHandler: defaultCallbackHandler.address - } - const saltNonce = '12345' - const counterfactualSafeAddress = await safeFactory.predictSafeAddress( - safeAccountConfig, - saltNonce - ) - const deploySafeProps: DeploySafeProps = { safeAccountConfig, saltNonce } - const safe = await safeFactory.deploySafe(deploySafeProps) - chai.expect(counterfactualSafeAddress).to.be.eq(await safe.getAddress()) - chai.expect(defaultCallbackHandler.address).to.be.eq(await safe.getFallbackHandler()) - } - ) - }) - - describe('deploySafe', async () => { - it('should fail if there are no owners', async () => { - const { contractNetworks } = await setupTests() - const safeFactory = await SafeFactory.init({ provider, contractNetworks }) - const owners: string[] = [] - const threshold = 2 - const safeAccountConfig: SafeAccountConfig = { owners, threshold } - const safeDeployProps: DeploySafeProps = { safeAccountConfig } - await chai - .expect(safeFactory.deploySafe(safeDeployProps)) - .rejectedWith('Owner list must have at least one owner') - }) - - it('should fail if the threshold is lower than 0', async () => { - const { accounts, contractNetworks } = await setupTests() - const [account1, account2] = accounts - const safeFactory = await SafeFactory.init({ provider, contractNetworks }) - const owners = [account1.address, account2.address] - const threshold = 0 - const safeAccountConfig: SafeAccountConfig = { owners, threshold } - const safeDeployProps: DeploySafeProps = { safeAccountConfig } - await chai - .expect(safeFactory.deploySafe(safeDeployProps)) - .rejectedWith('Threshold must be greater than or equal to 1') - }) - - it('should fail if the threshold is higher than the threshold', async () => { - const { accounts, contractNetworks } = await setupTests() - const [account1, account2] = accounts - const safeFactory = await SafeFactory.init({ provider, contractNetworks }) - const owners = [account1.address, account2.address] - const threshold = 3 - const safeAccountConfig: SafeAccountConfig = { owners, threshold } - const deploySafeProps: DeploySafeProps = { safeAccountConfig } - await chai - .expect(safeFactory.deploySafe(deploySafeProps)) - .rejectedWith('Threshold must be lower than or equal to owners length') - }) - - it('should fail if the saltNonce is lower than 0', async () => { - const { accounts, contractNetworks } = await setupTests() - const [account1, account2] = accounts - const safeFactory = await SafeFactory.init({ - provider, - safeVersion: safeVersionDeployed, - contractNetworks - }) - const owners = [account1.address, account2.address] - const threshold = 2 - const safeAccountConfig: SafeAccountConfig = { owners, threshold } - const invalidSaltNonce = '-1' - const safeDeployProps: DeploySafeProps = { safeAccountConfig, saltNonce: invalidSaltNonce } - await chai - .expect(safeFactory.deploySafe(safeDeployProps)) - .rejectedWith('saltNonce must be greater than or equal to 0') - }) - - itif(safeVersionDeployed > '1.0.0')( - 'should deploy a new Safe with custom fallback handler', - async () => { - const { accounts, contractNetworks } = await setupTests() - const defaultCallbackHandler = await getDefaultCallbackHandler() - const [account1, account2] = accounts - const safeFactory = await SafeFactory.init({ - provider, - safeVersion: safeVersionDeployed, - contractNetworks - }) - const owners = [account1.address, account2.address] - const threshold = 2 - const safeAccountConfig: SafeAccountConfig = { - owners, - threshold, - fallbackHandler: defaultCallbackHandler.address - } - const deploySafeProps: DeploySafeProps = { safeAccountConfig } - const safe = await safeFactory.deploySafe(deploySafeProps) - const deployedSafeOwners = await safe.getOwners() - chai.expect(deployedSafeOwners.toString()).to.be.eq(owners.toString()) - const deployedSafeThreshold = await safe.getThreshold() - chai.expect(deployedSafeThreshold).to.be.eq(threshold) - const fallbackHandler = await safe.getFallbackHandler() - chai.expect(defaultCallbackHandler.address).to.be.eq(fallbackHandler) - } - ) - - itif(safeVersionDeployed > '1.0.0')( - 'should deploy a new Safe with the default CompatibilityFallbackHandler', - async () => { - const { accounts, contractNetworks } = await setupTests() - const [account1, account2] = accounts - const safeFactory = await SafeFactory.init({ - provider, - safeVersion: safeVersionDeployed, - contractNetworks - }) - const owners = [account1.address, account2.address] - const threshold = 2 - const safeAccountConfig: SafeAccountConfig = { owners, threshold } - const deploySafeProps: DeploySafeProps = { safeAccountConfig } - const safe = await safeFactory.deploySafe(deploySafeProps) - const fallbackHandler = await safe.getFallbackHandler() - const compatibilityFallbackHandler = await ( - await getCompatibilityFallbackHandler() - ).contract.address - chai.expect(compatibilityFallbackHandler).to.be.eq(fallbackHandler) - } - ) - - it('should deploy a new Safe without saltNonce', async () => { - const { accounts, contractNetworks } = await setupTests() - const [account1, account2] = accounts - const safeFactory = await SafeFactory.init({ - provider, - safeVersion: safeVersionDeployed, - contractNetworks - }) - const owners = [account1.address, account2.address] - const threshold = 2 - const safeAccountConfig: SafeAccountConfig = { owners, threshold } - const deploySafeProps: DeploySafeProps = { safeAccountConfig } - const safe = await safeFactory.deploySafe(deploySafeProps) - const deployedSafeOwners = await safe.getOwners() - chai.expect(deployedSafeOwners.toString()).to.be.eq(owners.toString()) - const deployedSafeThreshold = await safe.getThreshold() - chai.expect(deployedSafeThreshold).to.be.eq(threshold) - }) - - it('should deploy a new Safe with saltNonce', async () => { - const { accounts, contractNetworks } = await setupTests() - const [account1, account2] = accounts - const safeFactory = await SafeFactory.init({ - provider, - safeVersion: safeVersionDeployed, - contractNetworks - }) - const owners = [account1.address, account2.address] - const threshold = 2 - const safeAccountConfig: SafeAccountConfig = { owners, threshold } - const saltNonce = '1' - const deploySafeProps: DeploySafeProps = { safeAccountConfig, saltNonce } - const safe = await safeFactory.deploySafe(deploySafeProps) - const deployedSafeOwners = await safe.getOwners() - chai.expect(deployedSafeOwners.toString()).to.be.eq(owners.toString()) - const deployedSafeThreshold = await safe.getThreshold() - chai.expect(deployedSafeThreshold).to.be.eq(threshold) - }) - - it('should deploy a new Safe with callback', async () => { - const { accounts, contractNetworks } = await setupTests() - const [account1, account2] = accounts - let callbackResult = '' - const callback = (txHash: string) => { - callbackResult = txHash - } - const safeFactory = await SafeFactory.init({ - provider, - safeVersion: safeVersionDeployed, - contractNetworks - }) - const owners = [account1.address, account2.address] - const threshold = 2 - const safeAccountConfig: SafeAccountConfig = { owners, threshold } - const deploySafeProps: DeploySafeProps = { safeAccountConfig, callback } - chai.expect(callbackResult).to.be.empty - const safe = await safeFactory.deploySafe(deploySafeProps) - chai.expect(callbackResult).to.be.not.empty - const safeInstanceVersion = safe.getContractVersion() - chai.expect(safeInstanceVersion).to.be.eq(safeVersionDeployed) - }) - - itif(safeVersionDeployed === DEFAULT_SAFE_VERSION)( - 'should deploy last Safe version by default', - async () => { - const { accounts, contractNetworks } = await setupTests() - const [account1, account2] = accounts - const safeFactory = await SafeFactory.init({ provider, contractNetworks }) - const owners = [account1.address, account2.address] - const threshold = 2 - const safeAccountConfig: SafeAccountConfig = { owners, threshold } - const deploySafeProps: DeploySafeProps = { safeAccountConfig } - const safe = await safeFactory.deploySafe(deploySafeProps) - const safeInstanceVersion = safe.getContractVersion() - chai.expect(safeInstanceVersion).to.be.eq(safeVersionDeployed) - } - ) - - it('should deploy a specific Safe version', async () => { - const { accounts, contractNetworks } = await setupTests() - const [account1, account2] = accounts - const safeFactory = await SafeFactory.init({ - provider, - safeVersion: safeVersionDeployed, - contractNetworks - }) - const owners = [account1.address, account2.address] - const threshold = 2 - const safeAccountConfig: SafeAccountConfig = { owners, threshold } - const deploySafeProps: DeploySafeProps = { safeAccountConfig } - const safe = await safeFactory.deploySafe(deploySafeProps) - const safeInstanceVersion = safe.getContractVersion() - chai.expect(safeInstanceVersion).to.be.eq(safeVersionDeployed) - }) - }) -}) diff --git a/packages/protocol-kit/tests/e2e/utilsContracts.test.ts b/packages/protocol-kit/tests/e2e/utilsContracts.test.ts index 9e2b3d45b..422ba7e2c 100644 --- a/packages/protocol-kit/tests/e2e/utilsContracts.test.ts +++ b/packages/protocol-kit/tests/e2e/utilsContracts.test.ts @@ -2,34 +2,62 @@ import chai from 'chai' import { polygon, optimism, bsc, gnosis, base, avalanche } from 'viem/chains' import { getEip1193Provider, getSafeProviderFromNetwork } from './utils/setupProvider' import { + getSafeAddressFromDeploymentTx, PREDETERMINED_SALT_NONCE, predictSafeAddress } from '@safe-global/protocol-kit/contracts/utils' -import { safeVersionDeployed, setupTests, itif } from '@safe-global/testing-kit' +import { + safeVersionDeployed, + setupTests, + itif, + waitTransactionReceipt +} from '@safe-global/testing-kit' import { SafeDeploymentConfig, SafeAccountConfig, ContractNetworksConfig, - Eip1193Provider + Eip1193Provider, + PredictedSafeProps } from '@safe-global/protocol-kit/types' -import Safe, { SafeFactory, DeploySafeProps } from '@safe-global/protocol-kit/index' +import Safe from '@safe-global/protocol-kit/index' import SafeProvider from '@safe-global/protocol-kit/SafeProvider' // test util funcion to deploy a safe (needed to check the expected Safe Address) async function deploySafe( - deploySafeProps: DeploySafeProps, + predictedSafeProps: PredictedSafeProps, provider: Eip1193Provider, contractNetworks: ContractNetworksConfig, + signer, signerAddress?: string ): Promise { - const safeFactory = await SafeFactory.init({ + const safeSDK = await Safe.init({ provider, signer: signerAddress, - safeVersion: safeVersionDeployed, + predictedSafe: { + ...predictedSafeProps, + safeDeploymentConfig: { + saltNonce: predictedSafeProps.safeDeploymentConfig?.saltNonce, + safeVersion: safeVersionDeployed + } + }, contractNetworks }) - return await safeFactory.deploySafe(deploySafeProps) + const deploymentTransaction = await safeSDK.createSafeDeploymentTransaction() + + const txHash = await signer.sendTransaction(deploymentTransaction) + + const txReceipt = await waitTransactionReceipt(txHash) + + const safeAddress = getSafeAddressFromDeploymentTx(txReceipt, safeVersionDeployed) + + const safeSDKDeployed = await Safe.init({ + provider, + contractNetworks, + safeAddress + }) + + return safeSDKDeployed } describe('Contract utils', () => { @@ -42,6 +70,7 @@ describe('Contract utils', () => { // 1/1 Safe const [owner1] = accounts const owners = [owner1.address] + const signer = accounts[0].signer const threshold = 1 const safeVersion = safeVersionDeployed const safeProvider = new SafeProvider({ provider }) @@ -67,9 +96,10 @@ describe('Contract utils', () => { // we deploy the Safe with the given configuration and the deployed Safe address should be equal to the predicted one const deployedSafe = await deploySafe( - { safeAccountConfig, saltNonce: safeDeploymentConfig.saltNonce }, + { safeAccountConfig, safeDeploymentConfig: { saltNonce: safeDeploymentConfig.saltNonce } }, provider, contractNetworks, + signer, owner1.address ) @@ -87,6 +117,7 @@ describe('Contract utils', () => { // 1/2 Safe const [owner1, owner2] = accounts const owners = [owner1.address, owner2.address] + const signer = accounts[0].signer const threshold = 1 const safeVersion = safeVersionDeployed const safeProvider = new SafeProvider({ provider }) @@ -112,9 +143,10 @@ describe('Contract utils', () => { // we deploy the Safe with the given configuration and the deployed Safe address should be equal to the predicted one const deployedSafe = await deploySafe( - { safeAccountConfig, saltNonce: safeDeploymentConfig.saltNonce }, + { safeAccountConfig, safeDeploymentConfig: { saltNonce: safeDeploymentConfig.saltNonce } }, provider, contractNetworks, + signer, owner1.address ) @@ -132,6 +164,7 @@ describe('Contract utils', () => { // 2/2 Safe const [owner1, owner2] = accounts const owners = [owner1.address, owner2.address] + const signer = accounts[0].signer const threshold = 2 const safeVersion = safeVersionDeployed const safeProvider = new SafeProvider({ provider }) @@ -157,9 +190,10 @@ describe('Contract utils', () => { // we deploy the Safe with the given configuration and the deployed Safe address should be equal to the predicted one const deployedSafe = await deploySafe( - { safeAccountConfig, saltNonce: safeDeploymentConfig.saltNonce }, + { safeAccountConfig, safeDeploymentConfig: { saltNonce: safeDeploymentConfig.saltNonce } }, provider, contractNetworks, + signer, owner1.address ) @@ -312,6 +346,7 @@ describe('Contract utils', () => { // 1/2 Safe const [owner1, owner2] = accounts const owners = [owner1.address, owner2.address] + const signer = accounts[0].signer const threshold = 1 const safeVersion = safeVersionDeployed const safeProvider = new SafeProvider({ provider }) @@ -339,9 +374,10 @@ describe('Contract utils', () => { // we deploy the Safe with the given configuration and the deployed Safe address should be equal to the predicted one const firstDeployedSafe = await deploySafe( - { safeAccountConfig, saltNonce: firstSaltNonce }, + { safeAccountConfig, safeDeploymentConfig: { saltNonce: firstSaltNonce } }, provider, contractNetworks, + signer, owner1.address ) @@ -363,9 +399,10 @@ describe('Contract utils', () => { // we deploy the Safe with the given configuration and the deployed Safe address should be equal to the predicted one const secondDeployedSafe = await deploySafe( - { safeAccountConfig, saltNonce: secondSaltNonce }, + { safeAccountConfig, safeDeploymentConfig: { saltNonce: secondSaltNonce } }, provider, contractNetworks, + signer, owner1.address ) @@ -387,9 +424,10 @@ describe('Contract utils', () => { // we deploy the Safe with the given configuration and the deployed Safe address should be equal to the predicted one const thirdDeployedSafe = await deploySafe( - { safeAccountConfig, saltNonce: thirdSaltNonce }, + { safeAccountConfig, safeDeploymentConfig: { saltNonce: thirdSaltNonce } }, provider, contractNetworks, + signer, owner1.address ) @@ -405,6 +443,7 @@ describe('Contract utils', () => { // 2/2 Safe const [owner1, owner2] = accounts const owners = [owner1.address, owner2.address] + const signer = accounts[0].signer const threshold = 2 const safeVersion = safeVersionDeployed const safeProvider = new SafeProvider({ provider }) @@ -422,9 +461,10 @@ describe('Contract utils', () => { // we deploy the Safe with the given configuration and the deployed Safe address should be equal to the predicted one const deployedSafe = await deploySafe( - { safeAccountConfig, saltNonce: safeDeploymentConfig.saltNonce }, + { safeAccountConfig, safeDeploymentConfig: { saltNonce: safeDeploymentConfig.saltNonce } }, provider, contractNetworks, + signer, owner1.address ) // We ensure the Safe is deployed, as getAddress() function is able to return an address for a predictedSafe @@ -470,6 +510,7 @@ describe('Contract utils', () => { // 1/1 Safe const [owner1] = accounts const owners = [owner1.address] + const signer = accounts[0].signer const threshold = 1 const customContracts = contractNetworks[chainId.toString()] @@ -492,6 +533,7 @@ describe('Contract utils', () => { { safeAccountConfig }, provider, contractNetworks, + signer, owner1.address ) diff --git a/packages/protocol-kit/tests/e2e/wrapSafeTransactionIntoDeploymentBatch.test.ts b/packages/protocol-kit/tests/e2e/wrapSafeTransactionIntoDeploymentBatch.test.ts index e4f5721fd..682cfe483 100644 --- a/packages/protocol-kit/tests/e2e/wrapSafeTransactionIntoDeploymentBatch.test.ts +++ b/packages/protocol-kit/tests/e2e/wrapSafeTransactionIntoDeploymentBatch.test.ts @@ -119,9 +119,17 @@ describe('wrapSafeTransactionIntoDeploymentBatch', () => { const { accounts, contractNetworks, predictedSafe } = await setupTests() const [, account2] = accounts + const customSaltNonce = '123456789' + const safeSdk = await Safe.init({ provider, - predictedSafe, + predictedSafe: { + ...predictedSafe, + safeDeploymentConfig: { + ...predictedSafe.safeDeploymentConfig, + saltNonce: customSaltNonce + } + }, contractNetworks }) @@ -135,13 +143,7 @@ describe('wrapSafeTransactionIntoDeploymentBatch', () => { transactions: [safeTransactionData] }) - const customSaltNonce = '123456789' - - const batchTransaction = await safeSdk.wrapSafeTransactionIntoDeploymentBatch( - safeTransaction, - {}, // transaction options - customSaltNonce - ) + const batchTransaction = await safeSdk.wrapSafeTransactionIntoDeploymentBatch(safeTransaction) const customSaltNonceEncoded = safeSdk .getSafeProvider() diff --git a/packages/sdk-starter-kit/src/SafeClient.test.ts b/packages/sdk-starter-kit/src/SafeClient.test.ts index 98f393058..3b4cc87b0 100644 --- a/packages/sdk-starter-kit/src/SafeClient.test.ts +++ b/packages/sdk-starter-kit/src/SafeClient.test.ts @@ -131,7 +131,7 @@ describe('SafeClient', () => { const result = await safeClient.send({ transactions: TRANSACTION_BATCH }) - expect(protocolKit.createSafeDeploymentTransaction).toHaveBeenCalledWith(undefined, {}) + expect(protocolKit.createSafeDeploymentTransaction).toHaveBeenCalledWith() expect(utils.sendTransaction).toHaveBeenCalledWith({ transaction: DEPLOYMENT_TRANSACTION, protocolKit diff --git a/packages/sdk-starter-kit/src/SafeClient.ts b/packages/sdk-starter-kit/src/SafeClient.ts index 1520444d9..8ce983495 100644 --- a/packages/sdk-starter-kit/src/SafeClient.ts +++ b/packages/sdk-starter-kit/src/SafeClient.ts @@ -1,11 +1,6 @@ import Safe from '@safe-global/protocol-kit' import SafeApiKit, { SafeMultisigTransactionListResponse } from '@safe-global/api-kit' -import { - SafeTransaction, - TransactionOptions, - TransactionResult, - Transaction -} from '@safe-global/types-kit' +import { SafeTransaction, TransactionOptions, TransactionResult } from '@safe-global/types-kit' import { createSafeClientResult, @@ -204,10 +199,9 @@ export class SafeClient extends BaseClient { }: { safeTransaction: SafeTransaction } & TransactionOptions): Promise { - const safeDeploymentTransaction: Transaction = - await this.protocolKit.createSafeDeploymentTransaction(undefined, transactionOptions) + const safeDeploymentTransaction = await this.protocolKit.createSafeDeploymentTransaction() const hash = await sendTransaction({ - transaction: { ...safeDeploymentTransaction }, + transaction: { ...safeDeploymentTransaction, ...transactionOptions }, protocolKit: this.protocolKit }) diff --git a/packages/sdk-starter-kit/src/extensions/messages/SafeMessageClient.test.ts b/packages/sdk-starter-kit/src/extensions/messages/SafeMessageClient.test.ts index fbeff7ed0..cd405f55d 100644 --- a/packages/sdk-starter-kit/src/extensions/messages/SafeMessageClient.test.ts +++ b/packages/sdk-starter-kit/src/extensions/messages/SafeMessageClient.test.ts @@ -118,7 +118,7 @@ describe('SafeClient', () => { const result = await safeMessageClient.sendMessage({ message: MESSAGE }) - expect(protocolKit.createSafeDeploymentTransaction).toHaveBeenCalledWith(undefined) + expect(protocolKit.createSafeDeploymentTransaction).toHaveBeenCalledWith() expect(utils.sendTransaction).toHaveBeenCalledWith({ transaction: DEPLOYMENT_TRANSACTION, protocolKit diff --git a/packages/sdk-starter-kit/src/extensions/messages/SafeMessageClient.ts b/packages/sdk-starter-kit/src/extensions/messages/SafeMessageClient.ts index c5db11ad1..d340393b9 100644 --- a/packages/sdk-starter-kit/src/extensions/messages/SafeMessageClient.ts +++ b/packages/sdk-starter-kit/src/extensions/messages/SafeMessageClient.ts @@ -105,8 +105,7 @@ export class SafeMessageClient { }): Promise { let deploymentTxHash const threshold = await this.protocolKit.getThreshold() - const safeDeploymentTransaction = - await this.protocolKit.createSafeDeploymentTransaction(undefined) + const safeDeploymentTransaction = await this.protocolKit.createSafeDeploymentTransaction() try { deploymentTxHash = await sendTransaction({ diff --git a/packages/types-kit/src/types.ts b/packages/types-kit/src/types.ts index 7405d0ca7..839e9635a 100644 --- a/packages/types-kit/src/types.ts +++ b/packages/types-kit/src/types.ts @@ -7,14 +7,6 @@ export enum OperationType { DelegateCall // 1 } -export interface CreateProxyProps { - safeSingletonAddress: string - initializer: string - saltNonce: string - options?: TransactionOptions - callback?: (txHash: string) => void -} - export interface SafeSetupConfig { owners: string[] threshold: number diff --git a/playground/protocol-kit/deploy-safe.ts b/playground/protocol-kit/deploy-safe.ts index 5b168f4b4..eadbf568b 100644 --- a/playground/protocol-kit/deploy-safe.ts +++ b/playground/protocol-kit/deploy-safe.ts @@ -1,6 +1,12 @@ -import { SafeAccountConfig, SafeFactory } from '@safe-global/protocol-kit' +import Safe, { SafeAccountConfig, getSafeAddressFromDeploymentTx } from '@safe-global/protocol-kit' import { SafeVersion } from '@safe-global/types-kit' +import { createWalletClient, http } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { sepolia } from 'viem/chains' +import { waitForTransactionReceipt } from 'viem/actions' +import semverSatisfies from 'semver/functions/satisfies' + // This file can be used to play around with the Safe Core SDK interface Config { @@ -15,7 +21,7 @@ interface Config { } const config: Config = { - RPC_URL: 'https://sepolia.gateway.tenderly.co', + RPC_URL: sepolia.rpcUrls.default.http[0], DEPLOYER_ADDRESS_PRIVATE_KEY: '', DEPLOY_SAFE: { OWNERS: ['OWNER_ADDRESS'], @@ -26,44 +32,76 @@ const config: Config = { } async function main() { - const safeVersion = config.DEPLOY_SAFE.SAFE_VERSION as SafeVersion - - console.log('safe config: ', config.DEPLOY_SAFE) - - // Create SafeFactory instance - const safeFactory = await SafeFactory.init({ - provider: config.RPC_URL, - signer: config.DEPLOYER_ADDRESS_PRIVATE_KEY, - safeVersion - }) + console.log('Safe Account config: ', config.DEPLOY_SAFE) // Config of the deployed Safe const safeAccountConfig: SafeAccountConfig = { owners: config.DEPLOY_SAFE.OWNERS, threshold: config.DEPLOY_SAFE.THRESHOLD } + + const safeVersion = config.DEPLOY_SAFE.SAFE_VERSION as SafeVersion const saltNonce = config.DEPLOY_SAFE.SALT_NONCE - // Predict deployed address - const predictedDeploySafeAddress = await safeFactory.predictSafeAddress( - safeAccountConfig, - saltNonce - ) + // protocol-kit instance creation + const protocolKit = await Safe.init({ + provider: config.RPC_URL, + signer: config.DEPLOYER_ADDRESS_PRIVATE_KEY, + predictedSafe: { + safeAccountConfig, + safeDeploymentConfig: { + saltNonce, + safeVersion + } + } + }) - console.log('Predicted deployed Safe address:', predictedDeploySafeAddress) + // The Account Abstraction feature is only available for Safes version 1.3.0 and above. + if (semverSatisfies(safeVersion, '>=1.3.0')) { + // check if its deployed + console.log('Safe Account deployed: ', await protocolKit.isSafeDeployed()) - function callback(txHash: string) { - console.log('Transaction hash:', txHash) + // Predict deployed address + const predictedSafeAddress = await protocolKit.getAddress() + console.log('Predicted Safe address:', predictedSafeAddress) } - // Deploy Safe - const safe = await safeFactory.deploySafe({ - safeAccountConfig, - saltNonce, - callback + console.log('Deploying Safe Account...') + + // Deploy the Safe account + const deploymentTransaction = await protocolKit.createSafeDeploymentTransaction() + + console.log('deploymentTransaction: ', deploymentTransaction) + + const account = privateKeyToAccount(`0x${config.DEPLOYER_ADDRESS_PRIVATE_KEY}`) + + const client = createWalletClient({ + account, + chain: sepolia, + transport: http(config.RPC_URL) + }) + + const txHash = await client.sendTransaction({ + to: deploymentTransaction.to, + value: BigInt(deploymentTransaction.value), + data: deploymentTransaction.data as `0x${string}` }) - console.log('Deployed Safe:', await safe.getAddress()) + console.log('Transaction hash:', txHash) + + const txReceipt = await waitForTransactionReceipt(client, { hash: txHash }) + + const safeAddress = getSafeAddressFromDeploymentTx(txReceipt, safeVersion) + + console.log('safeAddress:', safeAddress) + + // now you can use the Safe address in the instance of the protocol-kit + protocolKit.connect({ safeAddress }) + + console.log('is Safe deployed:', await protocolKit.isSafeDeployed()) + console.log('Safe Address:', await protocolKit.getAddress()) + console.log('Safe Owners:', await protocolKit.getOwners()) + console.log('Safe Threshold:', await protocolKit.getThreshold()) } main()