dp-contract-proxy-kit
Version:
Enable batched transactions and contract account interactions using a unique deterministic Gnosis Safe.
497 lines (452 loc) • 14.7 kB
text/typescript
import BigNumber from 'bignumber.js'
import multiSendAbi from './abis/MultiSendAbi.json'
import {
defaultNetworks,
NetworksConfig,
NormalizedNetworksConfig,
normalizeNetworksConfig
} from './config/networks'
import ContractManager from './contractManager'
import EthLibAdapter from './ethLibAdapters/EthLibAdapter'
import SafeAppsSdkConnector from './safeAppsSdkConnector'
import CpkTransactionManager from './transactionManagers/CpkTransactionManager'
import TransactionManager from './transactionManagers/TransactionManager'
import { Address } from './utils/basicTypes'
import { checkConnectedToSafe } from './utils/checkConnectedToSafe'
import { predeterminedSaltNonce } from './utils/constants'
import { getHexDataLength, joinHexData } from './utils/hexData'
import { getNetworkIdFromName } from './utils/networks'
import {
ExecOptions,
normalizeGasLimit,
OperationType,
standardizeTransaction,
StandardTransaction,
Transaction,
TransactionResult
} from './utils/transactions'
export interface CPKConfig {
ethLibAdapter: EthLibAdapter
transactionManager?: TransactionManager
ownerAccount?: string
networks?: NetworksConfig
saltNonce?: string
isSafeApp?: boolean
}
class CPK {
static Call = OperationType.Call
static DelegateCall = OperationType.DelegateCall
#safeAppsSdkConnector?: SafeAppsSdkConnector
#ethLibAdapter?: EthLibAdapter
#transactionManager?: TransactionManager
#contractManager?: ContractManager
#networks: NormalizedNetworksConfig
#ownerAccount?: Address
#saltNonce = predeterminedSaltNonce
#isConnectedToSafe = false
/**
* Creates and initializes an instance of the CPK with the selected configuration parameters.
*
* @param opts - CPK configuration
* @returns The CPK instance
*/
static async create(opts?: CPKConfig): Promise<CPK> {
const cpk = new CPK(opts)
if (opts) {
await cpk.init()
}
return cpk
}
/**
* Creates a non-initialized instance of the CPK with the selected configuration parameters.
*
* @param opts - CPK configuration
* @returns The CPK instance
*/
constructor(opts?: CPKConfig) {
this.#networks = {
...defaultNetworks
}
if (!opts) {
return
}
const {
ethLibAdapter,
transactionManager,
ownerAccount,
networks,
saltNonce,
isSafeApp = true
} = opts
if (isSafeApp) {
this.#safeAppsSdkConnector = new SafeAppsSdkConnector()
}
if (!ethLibAdapter) {
throw new Error('ethLibAdapter property missing from options')
}
this.#ethLibAdapter = ethLibAdapter
this.#transactionManager = transactionManager ?? new CpkTransactionManager()
this.#ownerAccount = ownerAccount
this.#networks = normalizeNetworksConfig(defaultNetworks, networks)
if (saltNonce) {
this.#saltNonce = saltNonce
}
}
/**
* Initializes the CPK instance.
*/
async init(): Promise<void> {
if (!this.#ethLibAdapter) {
throw new Error('CPK uninitialized ethLibAdapter')
}
const networkId = await this.#ethLibAdapter.getNetworkId()
const network = this.#networks[networkId]
if (!network) {
throw new Error(`Unrecognized network ID ${networkId}`)
}
const ownerAccount = await this.getOwnerAccount()
this.#isConnectedToSafe =
(await checkConnectedToSafe(this.#ethLibAdapter.getProvider())) ||
this.#safeAppsSdkConnector?.isSafeApp === true
this.#contractManager = await ContractManager.create({
ethLibAdapter: this.#ethLibAdapter,
network,
ownerAccount,
saltNonce: this.#saltNonce,
isSafeApp: this.#safeAppsSdkConnector?.isSafeApp === true,
isConnectedToSafe: this.#isConnectedToSafe
})
}
/**
* Checks if the Proxy contract is deployed or not. The deployment of the Proxy contract happens automatically when the first transaction is submitted.
*
* @returns TRUE when the Proxy contract is deployed
*/
async isProxyDeployed(): Promise<boolean> {
const address = await this.address
if (!address) {
throw new Error('CPK address uninitialized')
}
if (!this.ethLibAdapter) {
throw new Error('CPK ethLibAdapter uninitialized')
}
const codeAtAddress = await this.ethLibAdapter.getCode(address)
const isDeployed = codeAtAddress !== '0x'
return isDeployed
}
/**
* Returns the address of the account connected to the CPK (Proxy contract owner). However, if the CPK is running as a Safe App or connected to a Safe, the Safe address will be returned.
*
* @returns The address of the account connected to the CPK
*/
async getOwnerAccount(): Promise<Address | undefined> {
if (this.#safeAppsSdkConnector?.isSafeApp) {
return (await this.#safeAppsSdkConnector.getSafeInfo()).safeAddress
}
if (this.#ownerAccount) {
return this.#ownerAccount
}
if (!this.#ethLibAdapter) {
throw new Error('CPK uninitialized ethLibAdapter')
}
return this.#ethLibAdapter?.getAccount()
}
/**
* Returns the ETH balance of the Proxy contract.
*
* @returns The ETH balance of the Proxy contract
*/
async getBalance(): Promise<BigNumber> {
const address = await this.address
if (!address) {
throw new Error('CPK address uninitialized')
}
if (!this.#ethLibAdapter) {
throw new Error('CPK ethLibAdapter uninitialized')
}
return this.#ethLibAdapter?.getBalance(address)
}
/**
* Returns the ID of the connected network.
*
* @returns The ID of the connected network
*/
async getNetworkId(): Promise<number | undefined> {
if (this.#safeAppsSdkConnector?.isSafeApp) {
const networkName = (await this.#safeAppsSdkConnector.getSafeInfo()).network
return getNetworkIdFromName(networkName)
}
if (!this.#ethLibAdapter) {
throw new Error('CPK ethLibAdapter uninitialized')
}
return this.#ethLibAdapter.getNetworkId()
}
/**
* Returns the safeAppsSdkConnector used by the CPK.
*
* @returns The safeAppsSdkConnector used by the CPK
*/
get safeAppsSdkConnector(): SafeAppsSdkConnector | undefined {
return this.#safeAppsSdkConnector
}
/**
* Returns the contractManager used by the CPK.
*
* @returns The contractManager used by the CPK
*/
get contractManager(): ContractManager | undefined {
return this.#contractManager
}
/**
* Returns the ethLibAdapter used by the CPK.
*
* @returns The ethLibAdapter used by the CPK
*/
get ethLibAdapter(): EthLibAdapter | undefined {
return this.#ethLibAdapter
}
/**
* Returns a list of the contract addresses which drive the CPK per network by network ID.
*
* @returns The list of the contract addresses which drive the CPK per network by network ID
*/
get networks(): NormalizedNetworksConfig {
return this.#networks
}
/**
* Checks if the CPK is connected to a Safe account or not.
*
* @returns TRUE if the CPK is connected to a Safe account
*/
get isConnectedToSafe(): boolean {
return this.#isConnectedToSafe
}
/**
* Returns the salt nonce used to deploy the Proxy Contract.
*
* @returns The salt nonce used to deploy the Proxy Contract
*/
get saltNonce(): string {
return this.#saltNonce
}
/**
* Returns the address of the Proxy contract.
*
* @returns The address of the Proxy contract
*/
get address(): Address | undefined {
if (this.#safeAppsSdkConnector?.safeAddress) {
return this.#safeAppsSdkConnector?.safeAddress
}
return this.#contractManager?.contract?.address
}
/**
* Sets the ethLibAdapter used by the CPK.
*/
setEthLibAdapter(ethLibAdapter: EthLibAdapter): void {
this.#ethLibAdapter = ethLibAdapter
}
/**
* Sets the transactionManager used by the CPK.
*/
setTransactionManager(transactionManager: TransactionManager): void {
if (this.#safeAppsSdkConnector?.isSafeApp) {
throw new Error('TransactionManagers are not allowed when the app is running as a Safe App')
}
this.#transactionManager = transactionManager
}
/**
* Sets the network configuration used by the CPK.
*/
setNetworks(networks: NetworksConfig): void {
this.#networks = normalizeNetworksConfig(defaultNetworks, networks)
}
/**
* Returns the encoding of a list of transactions.
*
* @param transactions - The transaction list
* @returns The encoding of a list of transactions
*/
encodeMultiSendCallData(transactions: Transaction[]): string {
if (!this.#ethLibAdapter) {
throw new Error('CPK ethLibAdapter uninitialized')
}
const multiSend =
this.#contractManager?.multiSend || this.#ethLibAdapter.getContract(multiSendAbi)
const standardizedTxs = transactions.map(standardizeTransaction)
const ethLibAdapter = this.#ethLibAdapter
return multiSend.encode('multiSend', [
joinHexData(
standardizedTxs.map((tx) =>
ethLibAdapter.abiEncodePacked(
{ type: 'uint8', value: tx.operation },
{ type: 'address', value: tx.to },
{ type: 'uint256', value: tx.value },
{ type: 'uint256', value: getHexDataLength(tx.data) },
{ type: 'bytes', value: tx.data }
)
)
)
])
}
/**
* Executes a list of transactions.
*
* @param transactions - The transaction list to execute
* @param options - Execution configuration options
* @returns The transaction response
*/
async execTransactions(
transactions: Transaction[],
options?: ExecOptions
): Promise<TransactionResult> {
const standardizedTxs = transactions.map(standardizeTransaction)
if (this.#safeAppsSdkConnector?.isSafeApp && transactions.length >= 1) {
return this.#safeAppsSdkConnector.sendTransactions(standardizedTxs, {
safeTxGas: options?.safeTxGas
})
}
const address = await this.address
if (!address) {
throw new Error('CPK address uninitialized')
}
if (!this.#contractManager) {
throw new Error('CPK contractManager uninitialized')
}
if (!this.#ethLibAdapter) {
throw new Error('CPK ethLibAdapter uninitialized')
}
if (!this.#transactionManager) {
throw new Error('CPK transactionManager uninitialized')
}
const ownerAccount = await this.getOwnerAccount()
if (!ownerAccount) {
throw new Error('CPK ownerAccount uninitialized')
}
const safeExecTxParams = this.getSafeExecTxParams(transactions)
const sendOptions = normalizeGasLimit({ ...options, from: ownerAccount })
const codeAtAddress = await this.#ethLibAdapter.getCode(address)
const isDeployed = codeAtAddress !== '0x'
const txManager = !isDeployed ? new CpkTransactionManager() : this.#transactionManager
return txManager.execTransactions({
ownerAccount,
safeExecTxParams,
transactions: standardizedTxs,
ethLibAdapter: this.#ethLibAdapter,
contractManager: this.#contractManager,
saltNonce: this.#saltNonce,
isDeployed,
isConnectedToSafe: this.#isConnectedToSafe,
sendOptions
})
}
/**
* Returns the Master Copy contract version.
*
* @returns The Master Copy contract version
*/
async getContractVersion(): Promise<string> {
const isProxyDeployed = await this.isProxyDeployed()
if (!isProxyDeployed) {
throw new Error('CPK Proxy contract is not deployed')
}
if (!this.#contractManager?.versionUtils) {
throw new Error('CPK contractManager uninitialized')
}
return await this.#contractManager.versionUtils.getContractVersion()
}
/**
* Returns the list of addresses of all the enabled Safe modules.
*
* @returns The list of addresses of all the enabled Safe modules
*/
async getModules(): Promise<Address[]> {
const isProxyDeployed = await this.isProxyDeployed()
if (!isProxyDeployed) {
throw new Error('CPK Proxy contract is not deployed')
}
if (!this.#contractManager?.versionUtils) {
throw new Error('CPK contractManager uninitialized')
}
return await this.#contractManager.versionUtils.getModules()
}
/**
* Checks if a specific Safe module is enabled or not.
*
* @param moduleAddress - The desired module address
* @returns TRUE if the module is enabled
*/
async isModuleEnabled(moduleAddress: Address): Promise<boolean> {
const isProxyDeployed = await this.isProxyDeployed()
if (!isProxyDeployed) {
throw new Error('CPK Proxy contract is not deployed')
}
if (!this.#contractManager?.versionUtils) {
throw new Error('CPK contractManager uninitialized')
}
return await this.#contractManager.versionUtils.isModuleEnabled(moduleAddress)
}
/**
* Enables a Safe module
*
* @param moduleAddress - The desired module address
* @returns The transaction response
*/
async enableModule(moduleAddress: Address): Promise<TransactionResult> {
if (!this.#contractManager?.versionUtils) {
throw new Error('CPK contractManager uninitialized')
}
const address = await this.address
if (!address) {
throw new Error('CPK address uninitialized')
}
return await this.execTransactions([
{
to: address,
data: await this.#contractManager.versionUtils.encodeEnableModule(moduleAddress),
operation: CPK.Call
}
])
}
/**
* Disables a Safe module
*
* @param moduleAddress - The desired module address
* @returns The transaction response
*/
async disableModule(moduleAddress: Address): Promise<TransactionResult> {
const isProxyDeployed = await this.isProxyDeployed()
if (!isProxyDeployed) {
throw new Error('CPK Proxy contract is not deployed')
}
if (!this.#contractManager?.versionUtils) {
throw new Error('CPK contractManager uninitialized')
}
const address = await this.address
if (!address) {
throw new Error('CPK address uninitialized')
}
return await this.execTransactions([
{
to: address,
data: await this.#contractManager.versionUtils.encodeDisableModule(moduleAddress),
operation: CPK.Call
}
])
}
private getSafeExecTxParams(transactions: Transaction[]): StandardTransaction {
if (transactions.length === 1) {
return standardizeTransaction(transactions[0])
}
if (!this.#contractManager?.multiSend) {
throw new Error('CPK MultiSend uninitialized')
}
return {
to: this.#contractManager?.multiSend.address,
value: '0',
data: this.encodeMultiSendCallData(transactions),
operation: CPK.DelegateCall
}
}
}
export default CPK