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()