Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(protocol-kit): Migrate CreateCall contract to Abitype #742

Merged
merged 9 commits into from
Mar 27, 2024
6 changes: 2 additions & 4 deletions packages/protocol-kit/scripts/generateTypechainFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,10 @@ const outDirTests = 'typechain/tests/'
const safeContractsPath = '../../node_modules/@safe-global/safe-deployments/dist/assets'

const safeContracts_V1_4_1 = [
`${safeContractsPath}/v1.4.1/compatibility_fallback_handler.json`,
`${safeContractsPath}/v1.4.1/create_call.json`
`${safeContractsPath}/v1.4.1/compatibility_fallback_handler.json`
].join(' ')
const safeContracts_V1_3_0 = [
`${safeContractsPath}/v1.3.0/compatibility_fallback_handler.json`,
`${safeContractsPath}/v1.3.0/create_call.json`
`${safeContractsPath}/v1.3.0/compatibility_fallback_handler.json`
].join(' ')

// Won't be included in dist/ folder
Expand Down
61 changes: 61 additions & 0 deletions packages/protocol-kit/src/adapters/CreateCallBaseContract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { contractName, getContractDeployment } from '@safe-global/protocol-kit/contracts/config'
import { SafeVersion } from '@safe-global/safe-core-sdk-types'

/**
* Abstract class CreateCallBaseContract serves as a base for creating a CreateCallBaseContract contract for a specific adapter (Ethers.js, Web3.js, or viem.js)
* This class is designed to be extended by adapter-specific abstract classes, such as CreateCallBaseContractEthers, CreateCallBaseContractWeb3, and CreateCallBaseContractViem.
* It includes the core logic for selecting the appropriate ABI and the address from CreateCall deployments.
*
* @template CreateCallContractAbiType - The ABI associated with the CreateCall contract.
*
* Example subclasses extending this base class:
* - CreateCallBaseContractEthers<CreateCallContract_v1_3_0_Abi> extends CreateCallBaseContract<CreateCallContract_v1_3_0_Abi>
* - CreateCallBaseContractWeb3<CreateCallContract_v1_3_0_Abi> extends CreateCallBaseContract<CreateCallContract_v1_3_0_Abi>
* - CreateCallBaseContractViem<CreateCallContract_v1_3_0_Abi> extends CreateCallBaseContract<CreateCallContract_v1_3_0_Abi>
*/
abstract class CreateCallBaseContract<CreateCallContractAbiType> {
contractAbi: CreateCallContractAbiType
contractAddress: string

contractName: contractName
abstract safeVersion: SafeVersion

abstract contract: unknown // This needs to be implemented for each adapter.
abstract adapter: unknown // This needs to be implemented for each adapter.

/**
* Constructs a new CreateCallBaseContract instance.
*
* @param chainId - The chain ID of the contract.
* @param defaultAbi - The hardcoded ABI of the CreateCall contract.
* @param safeVersion - The version of the CreateCall contract.
* @param customContractAddress - Optional custom address for the contract.
* @param customContractAbi - Optional custom ABI for the contract.
* @throws Will throw an error if the contract address is invalid.
*/
constructor(
chainId: bigint,
defaultAbi: CreateCallContractAbiType,
safeVersion: SafeVersion,
customContractAddress?: string,
customContractAbi?: CreateCallContractAbiType
) {
this.contractName = 'createCallVersion'

const deployment = getContractDeployment(safeVersion, chainId, this.contractName)

const contractAddress = customContractAddress || deployment?.defaultAddress

if (!contractAddress) {
throw new Error('Invalid contract address')
}

this.contractAddress = contractAddress
this.contractAbi =
customContractAbi ||
(deployment?.abi as CreateCallContractAbiType) || // this cast is required because abi is set as any[] in safe-deployments
defaultAbi // if no customAbi and no abi is present in the safe-deployments we use our hardcoded abi
}
}

export default CreateCallBaseContract
10 changes: 5 additions & 5 deletions packages/protocol-kit/src/adapters/ethers/EthersAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { generateTypedData, validateEip3770Address } from '@safe-global/protocol-kit/utils'
import {
CreateCallContract,
EIP712TypedDataMessage,
EIP712TypedDataTx,
Eip3770Address,
Expand All @@ -12,7 +13,6 @@ import {
} from '@safe-global/safe-core-sdk-types'
import { ethers, TransactionResponse, AbstractSigner, Provider } from 'ethers'
import CompatibilityFallbackHandlerContractEthers from './contracts/CompatibilityFallbackHandler/CompatibilityFallbackHandlerEthersContract'
import CreateCallEthersContract from './contracts/CreateCall/CreateCallEthersContract'
import SafeContractEthers from './contracts/Safe/SafeContractEthers'
import {
getCompatibilityFallbackHandlerContractInstance,
Expand Down Expand Up @@ -220,16 +220,16 @@ class EthersAdapter implements EthAdapter {
async getCreateCallContract({
safeVersion,
singletonDeployment,
customContractAddress
}: GetContractProps): Promise<CreateCallEthersContract> {
customContractAddress,
customContractAbi
}: GetContractProps): Promise<CreateCallContract> {
const chainId = await this.getChainId()
const contractAddress =
customContractAddress ?? singletonDeployment?.networkAddresses[chainId.toString()]
if (!contractAddress) {
throw new Error('Invalid CreateCall contract address')
}
const signerOrProvider = this.#signer || this.#provider
return getCreateCallContractInstance(safeVersion, contractAddress, signerOrProvider)
return getCreateCallContractInstance(safeVersion, contractAddress, this, customContractAbi)
}

async getSimulateTxAccessorContract({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Contract, ContractRunner, InterfaceAbi } from 'ethers'

import EthersAdapter from '@safe-global/protocol-kit/adapters/ethers/EthersAdapter'
import CreateCallBaseContract from '@safe-global/protocol-kit/adapters/CreateCallBaseContract'
import { SafeVersion } from '@safe-global/safe-core-sdk-types'

/**
* Abstract class CreateCallBaseContractEthers extends CreateCallBaseContract to specifically integrate with the Ethers.js v6 library.
* It is designed to be instantiated for different versions of the Safe contract.
*
* This abstract class sets up the Ethers v6 Contract object that interacts with a CreateCall contract version.
*
* Subclasses of CreateCallBaseContractEthers are expected to represent specific versions of the contract.
*
* @template CreateCallContractAbiType - The ABI type specific to the version of the CreateCall contract, extending InterfaceAbi from Ethers.
* @extends CreateCallBaseContract<CreateCallContractAbiType> - Extends the generic CreateCallBaseContract with Ethers-specific implementation.
*
* Example subclasses:
* - CreateCallContract_v1_4_1_Ethers extends CreateCallBaseContractEthers<CreateCallContract_v1_4_1_Abi>
* - CreateCallContract_v1_3_0_Ethers extends CreateCallBaseContractEthers<CreateCallContract_v1_3_0_Abi>
*/
abstract class CreateCallBaseContractEthers<
CreateCallContractAbiType extends InterfaceAbi
> extends CreateCallBaseContract<CreateCallContractAbiType> {
contract: Contract
adapter: EthersAdapter

/**
* @constructor
* Constructs an instance of CreateCallBaseContractEthers.
*
* @param chainId - The chain ID of the contract.
* @param ethersAdapter - An instance of EthersAdapter.
* @param defaultAbi - The default ABI for the CreateCall contract. It should be compatible with the specific version of the contract.
* @param safeVersion - The version of the Safe contract.
* @param customContractAddress - Optional custom address for the contract. If not provided, the address is derived from the Safe deployments based on the chainId and safeVersion.
* @param customContractAbi - Optional custom ABI for the contract. If not provided, the ABI is derived from the Safe deployments or the defaultAbi is used.
*/
constructor(
chainId: bigint,
ethersAdapter: EthersAdapter,
defaultAbi: CreateCallContractAbiType,
safeVersion: SafeVersion,
customContractAddress?: string,
customContractAbi?: CreateCallContractAbiType,
runner?: ContractRunner | null
) {
super(chainId, defaultAbi, safeVersion, customContractAddress, customContractAbi)

this.adapter = ethersAdapter
this.contract = new Contract(
this.contractAddress,
this.contractAbi,
runner || this.adapter.getSigner()
)
}
}

export default CreateCallBaseContractEthers

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import CreateCallBaseContractEthers from '@safe-global/protocol-kit/adapters/ethers/contracts/CreateCall/CreateCallBaseContractEthers'
import EthersAdapter from '@safe-global/protocol-kit/adapters/ethers/EthersAdapter'
import {
EthersTransactionOptions,
EthersTransactionResult
} from '@safe-global/protocol-kit/adapters/ethers/types'
import CreateCallContract_v1_3_0_Contract, {
CreateCallContract_v1_3_0_Abi
} from '@safe-global/protocol-kit/contracts/AbiType/CreateCall/v1.3.0/CreateCallContract_v1_3_0'
import CreateCall_1_3_0_ContractArtifacts from '@safe-global/protocol-kit/contracts/AbiType/assets/CreateCall/v1.3.0/create_call'
import { SafeVersion } from '@safe-global/safe-core-sdk-types'
import {
EncodeCreateCallFunction,
EstimateGasCreateCallFunction,
GetAddressCreateCallFunction
} from '@safe-global/protocol-kit/contracts/AbiType/CreateCall/CreateCallBaseContract'
import { toTxResult } from '@safe-global/protocol-kit/adapters/ethers/utils'

/**
* CreateCallContract_V1_3_0_Ethers is the implementation specific to the CreateCall contract version 1.3.0.
*
* This class specializes in handling interactions with the CreateCall contract version 1.3.0 using Ethers.js v6.
*
* @extends CreateCallBaseContractEthers<CreateCallContract_v1_3_0_Abi> - Inherits from CreateCallBaseContractEthers with ABI specific to CreateCall contract version 1.3.0.
* @implements CreateCallContract_v1_3_0_Contract - Implements the interface specific to CreateCall contract version 1.3.0.
*/
class CreateCallContract_V1_3_0_Ethers
extends CreateCallBaseContractEthers<CreateCallContract_v1_3_0_Abi>
implements CreateCallContract_v1_3_0_Contract
{
safeVersion: SafeVersion

/**
* Constructs an instance of CreateCallContract_V1_3_0_Ethers
*
* @param chainId - The chain ID where the contract resides.
* @param ethersAdapter - An instance of EthersAdapter.
* @param customContractAddress - Optional custom address for the contract. If not provided, the address is derived from the CreateCall deployments based on the chainId and safeVersion.
* @param customContractAbi - Optional custom ABI for the contract. If not provided, the default ABI for version 1.3.0 is used.
*/
constructor(
chainId: bigint,
ethersAdapter: EthersAdapter,
customContractAddress?: string,
customContractAbi?: CreateCallContract_v1_3_0_Abi
) {
const safeVersion = '1.3.0'
const defaultAbi = CreateCall_1_3_0_ContractArtifacts.abi

super(chainId, ethersAdapter, defaultAbi, safeVersion, customContractAddress, customContractAbi)

this.safeVersion = safeVersion
}

getAddress: GetAddressCreateCallFunction = () => {
return this.contract.getAddress()
}

encode: EncodeCreateCallFunction<CreateCallContract_v1_3_0_Abi> = (functionToEncode, args) => {
return this.contract.interface.encodeFunctionData(functionToEncode, args)
}

estimateGas: EstimateGasCreateCallFunction<
CreateCallContract_v1_3_0_Abi,
EthersTransactionOptions
> = (functionToEstimate, args, options = {}) => {
const contractMethodToEstimate = this.contract.getFunction(functionToEstimate)

return contractMethodToEstimate.estimateGas(...args, options)
}

async performCreate(
args: readonly [value: bigint, deploymentData: string],
options?: EthersTransactionOptions
): Promise<EthersTransactionResult> {
if (options && !options.gasLimit) {
options.gasLimit = (await this.estimateGas('performCreate', args, { ...options })).toString()
}
const txResponse = await this.contract.performCreate(...args, { ...options })
return toTxResult(txResponse, options)
}

async performCreate2(
args: readonly [value: bigint, deploymentData: string, salt: string],
options?: EthersTransactionOptions
): Promise<EthersTransactionResult> {
if (options && !options.gasLimit) {
options.gasLimit = (await this.estimateGas('performCreate2', args, { ...options })).toString()
}
const txResponse = await this.contract.performCreate2(...args)
return toTxResult(txResponse, options)
}

// TODO: Remove this mapper after remove Typechain
mapToTypechainContract(): any {
return {
contract: this.contract,

getAddress: this.getAddress.bind(this),

encode: this.encode.bind(this),

estimateGas: async (...args: Parameters<typeof this.estimateGas>) =>
(await this.estimateGas(...args)).toString(),

performCreate: async (
value: string,
deploymentData: string,
options?: EthersTransactionOptions
) => this.performCreate([BigInt(value), deploymentData], options),

performCreate2: async (
value: string,
deploymentData: string,
salt: string,
options?: EthersTransactionOptions
) => this.performCreate2([BigInt(value), deploymentData, salt], options)
}
}
}

export default CreateCallContract_V1_3_0_Ethers

This file was deleted.

Loading
Loading