UNPKG

0xweb

Version:

Contract package manager and other web3 tools

677 lines (581 loc) 23.5 kB
import alot from 'alot'; import { Web3Client } from '@dequanto/clients/Web3Client'; import { ContractBase } from '@dequanto/contracts/ContractBase'; import { HardhatProvider } from '@dequanto/hardhat/HardhatProvider'; import { IAccount } from '@dequanto/models/TAccount'; import { TEth } from '@dequanto/models/TEth'; import { $abiUtils } from '@dequanto/utils/$abiUtils'; import { $contract } from '@dequanto/utils/$contract'; import { $require } from '@dequanto/utils/$require'; import { ParametersFromSecond } from '@dequanto/utils/types'; import { Constructor } from '@dequanto/utils/types'; import { BlockchainExplorerFactory } from '@dequanto/explorer/BlockchainExplorerFactory'; import { ContractVerifier } from '@dequanto/explorer/ContractVerifier'; import { HardhatWeb3Client } from '@dequanto/hardhat/HardhatWeb3Client'; import { LoggerService } from '@dequanto/loggers/LoggerService'; import { $is } from '@dequanto/utils/$is'; import { IBeacon, IBeaconProxy, IProxy, IProxyAdmin, ProxyDeployment } from './proxy/ProxyDeployment'; import { DeploymentsStorage, IDeployment } from './storage/DeploymentsStorage'; import { TAddress } from '@dequanto/models/TAddress'; import { $promise } from '@dequanto/utils/$promise'; import { l } from '@dequanto/utils/$logger'; import { $bytecode } from '@dequanto/evm/utils/$bytecode'; type TDeploymentOptions = { id?: string /** Will deploy the contract */ force?: boolean /** Will check if local bytecode has changed and will deploy */ latest?: boolean verification?: boolean | 'silent' // Will be used for verification process proxyFor?: TAddress } type TVerificationOptions = TDeploymentOptions & { // otherwise will be fetched from the deployment TX constructorParams?: any[] } export class Deployments { public store: DeploymentsStorage; private _logger = new LoggerService('deployments', { fs: false, std: true }); private _hh = new HardhatProvider(); private _proxyDeployment: ProxyDeployment; private _config: { TransparentProxy?: { Proxy?: Constructor<ContractBase>, ProxyAdmin?: Constructor<IProxyAdmin> }, Beacon?: { Beacon?: Constructor<IBeacon>, BeaconProxy?: Constructor<IBeaconProxy> } } = { TransparentProxy: {} } constructor(public client: Web3Client, public deployer: IAccount, public opts: { Proxy?: Constructor<ContractBase>, ProxyAdmin?: Constructor<IProxyAdmin> directory?: string // Will be part of the deployments filename name?: string // TPlatform of a forked network fork?: string Beacon?: { Beacon: Constructor<IBeacon> BeaconProxy: Constructor<IBeaconProxy> } verification?: boolean } = {}) { this._config.TransparentProxy.Proxy = opts?.Proxy; this._config.TransparentProxy.ProxyAdmin = opts?.ProxyAdmin; this._config.Beacon = opts?.Beacon; this.store = new DeploymentsStorage(client, deployer, opts); this._proxyDeployment = new ProxyDeployment(this.store, this._config); if (opts?.fork) { $require.eq(client.platform, 'hardhat', 'Only hardhat is supported for forked networks'); (client as HardhatWeb3Client).configureFork(opts.fork); } } async has<T extends ContractBase>(Ctor: Constructor<T>, opts?: { id?: string; params?: ConstructorParameters<Constructor<T>>; }): Promise<boolean> { let x = await this.getOrNull(Ctor, opts); return x != null; } async get<T extends ContractBase>(Ctor: Constructor<T>, opts?: { id?: string; address?: TAddress; version?: string }): Promise<T> { let contract = await this.getOrNull(Ctor, opts); if (contract == null) { throw new Error(`Deployment ${Ctor.name} ${opts?.id ?? ''} not found in ${this.client.platform} [${this.store.opts?.name ?? this.store.opts?.directory ?? ''}]`); } return contract; } async getIfExists<T extends ContractBase>(Ctor: Constructor<T>, opts?: { id?: string; address?: TAddress; }): Promise<T> { return this.getOrNull(Ctor, opts); } private async getOrNull<T extends ContractBase>(Ctor: Constructor<T>, opts: { id?: string; address?: TAddress; version?: string; }): Promise<T> { let deployment = await this.store.getDeploymentInfo(Ctor, opts); let address = deployment?.address; if (address == null) { return null; } return new Ctor(address, this.client); } /** * Get the deployment for Ctor, but returns the CtorWrapped instance. Useful for proxies, etc. * e.g. deployments.getAs(SomeProxy, SomeImplementation); */ async getAs<TDeployed extends ContractBase, TWrapped extends ContractBase>(Ctor: Constructor<TDeployed>, CtorWrapped: Constructor<TWrapped>, opts?: { id?: string; }): Promise<TWrapped> { let deployment = await this.store.getDeploymentInfo(Ctor, opts); return deployment == null ? null : new CtorWrapped(deployment.address, this.client); } async verify (params: { id: string address?: TAddress, Ctor: Constructor<ContractBase> constructorParams?: any[] }) { let deployment = await this.store.getDeploymentInfo('', { id: params.id }); $require.notNull(deployment, `Deployment for ${params.id} not found`); await this.ensureVerification(params.Ctor, deployment, { id: params.id, constructorParams: params.constructorParams, proxyFor: deployment.proxyFor, }); } async ensureContract<T extends TContract>(Ctor: Constructor<T>, opts?: TConstructorArgs<T> & { id?: string; force?: boolean; }): Promise<T> { let { contract } = await this.ensure(Ctor, opts); return contract; } async ensure<T extends TContract>(Ctor: Constructor<T>, opts?: TConstructorArgs<T> & TDeploymentOptions): Promise<{ contract: T receipt?: TEth.TxReceipt deployment: IDeployment }> { opts ??= {} as any; let currentDeployment = await this.store.getDeploymentInfo(Ctor, opts); let contract = await this.getOrNull(Ctor, opts); if (contract != null) { await this.ensureVerification(Ctor, currentDeployment, opts); let requireLatest = opts.latest == null ? (this.client.platform === 'hardhat') : (opts.latest ?? false); if (opts.force !== true && requireLatest !== true) { // return already deployed contract $contract.store.register(contract as any); return { contract, deployment: currentDeployment, }; } if (requireLatest === true && opts.force !== true) { // was already deployed. Check new bytecode hash let isSame = await this.isSameBytecode(Ctor, currentDeployment); if (isSame) { $contract.store.register(contract as any); return { contract, deployment: currentDeployment, }; } } } // Lets deploy the contract, new, forced, or latest let constructorArgs = opts.arguments ?? []; let id = opts.id ?? Ctor.name; let { receipt, contract: deployedContract, bytecode, deployedBytecode } = await this._hh.deployClass(Ctor, { arguments: constructorArgs, client: this.client, deployer: this.deployer as any, }); let deployment = await this.store.saveDeployment(deployedContract, { id, name: Ctor.name, bytecodeHash: this.getBytecodeHash(deployedBytecode) }, receipt); await this.ensureVerification(Ctor, deployment, { id: id, verification: opts?.verification, }); return { receipt, contract: deployedContract, deployment: deployment }; } async ensureWithProxy< T extends (TContract & { initialize?: TInit }), TInit extends TInitializer >( CtorImpl: Constructor<T>, opts?: TConstructorArgs<T> & TDeploymentOptions & { initialize?: ParametersFromSecond<T['initialize']> } ): Promise<{ // the Implementation Contract with the address set to Proxy contract: T contractReceipt?: TEth.TxReceipt contractProxy: IProxy, contractProxyAdmin: IProxyAdmin, contractImplementation: T deployment: IDeployment }> { let id = opts?.id ?? getImplementationId(CtorImpl); let proxyId = `${id}Proxy`; let { contract: contractImpl, receipt: contractReceipt, deployment: contractImplDeployment } = await this.ensure(CtorImpl, { arguments: opts?.arguments as any, id: id, force: opts?.force, latest: true, verification: opts?.verification, }); let data = serializeInitData(id, contractImpl, opts.initialize); let implementationAddress = contractImplDeployment.implementation ?? contractImplDeployment.address; let { contractProxy, contractProxyDeployment, contractProxyAdmin } = await this._proxyDeployment.ensureProxy({ ImplementationContract: CtorImpl, proxyId: proxyId, deployer: this.deployer, deployments: this, implementation: { address: implementationAddress, initData: data } }) if (contractImplDeployment.implementation == null) { // Set the Proxy contract as the main Address contractImplDeployment.implementation = contractImplDeployment.address; contractImplDeployment.address = contractProxy.address; contractProxyDeployment.proxyFor = contractImplDeployment.implementation; await this.store.updateDeployment(contractImplDeployment); await this.store.updateDeployment(contractProxyDeployment); } let contract = new CtorImpl(contractProxy.address, this.client); return { contract: contract, contractReceipt, contractProxy: contractProxy, contractProxyAdmin: contractProxyAdmin, contractImplementation: contractImpl, deployment: contractImplDeployment }; } /** * Deploys the Beacon contract. Implementation is the target contract (can be a proxy or normal contract) * https://docs.openzeppelin.com/contracts/5.x/api/proxy#beacon **/ async ensureWithBeacon< T extends (TContract & { initialize?: TInit }), TInit extends TInitializer >( CtorImpl: Constructor<T>, opts: TConstructorArgs<T> & TDeploymentOptions & { // Supports path with the first slug as the Implementation ID, e.g. `myImplementation/Foo` id: string initialize?: ParametersFromSecond<T['initialize']> } ): Promise<{ // the Implementation Contract with the address set to Beacon Proxy contract: T contractReceipt?: TEth.TxReceipt contractDeployment: IDeployment contractBeacon: IBeacon contractBeaconDeployment: IDeployment contractBeaconProxy: IBeaconProxy contractBeaconProxyDeployment: IDeployment }> { $require.notEmpty(opts?.id, `ID is required for Beacon deployment, as Implementation apparently will get multiple Beacons`); let beaconProxyId = opts.id; let implId = beaconProxyId.includes('/') ? beaconProxyId.substring(0, beaconProxyId.indexOf('/')) : getImplementationId(CtorImpl); let beaconId = `${implId}/beacon`; let { contract: contractImpl, receipt: contractReceipt, deployment: contractImplDeployment } = await this.ensure(CtorImpl, { arguments: opts?.arguments as any, id: implId, force: opts?.force, latest: true, verification: opts?.verification, }); let data = serializeInitData(implId, contractImpl, opts.initialize); let implementationAddress = contractImplDeployment.implementation ?? contractImplDeployment.address; let { contractBeacon, contractBeaconDeployment, contractBeaconProxy, contractBeaconProxyDeployment, } = await this._proxyDeployment.ensureBeaconProxy({ ImplementationContract: CtorImpl, beaconId, beaconProxyId, deployer: this.deployer, deployments: this, implementation: { address: implementationAddress, initData: data } }); let contract = new CtorImpl(contractBeaconProxy.address, this.client); return { contract: contract, contractReceipt, contractDeployment: contractImplDeployment, contractBeacon, contractBeaconDeployment, contractBeaconProxy, contractBeaconProxyDeployment, }; } private async isSameBytecode<T extends ContractBase>(Ctor: Constructor<T>, deployment: IDeployment) { let bytecodeHash = deployment.bytecodeHash; if (bytecodeHash == null) { let address = deployment.implementation ?? deployment.address; let bytecode = await this.client.getCode(address); $require.True($is.Hex(bytecode), `Bytecode not resolved for ${address}`); bytecodeHash = this.getBytecodeHash(bytecode); } let { deployedBytecode } = await this._hh.getFactoryForClass(Ctor); let newBytecodeHash = this.getBytecodeHash(deployedBytecode); if (newBytecodeHash === bytecodeHash) { this._logger.log(`${deployment.id} bytecode has not changed`); return true; } // recheck v1 if (deployment.bytecodeHash != null) { let address = deployment.implementation ?? deployment.address; let bytecode = await this.client.getCode(address); $require.True($is.Hex(bytecode), `Bytecode not resolved for ${address}`); let currentHash = this.getBytecodeHash(bytecode); if (currentHash == newBytecodeHash) { this._logger.log(`${deployment.id} bytecode has not changed. yellow<v0 bytecode check>`); return true; } if (bytecode.length === deployedBytecode.length) { let { bytecode: bytecodeOnchain } = $bytecode.splitToMetadata(bytecode); let { bytecode: bytecodeLocal } = $bytecode.splitToMetadata(deployedBytecode); let [ localDiff, onchainDiff ] = Str.getDifference(bytecodeLocal, bytecodeOnchain); if (localDiff === '' || /^0+$/.test(localDiff)) { this._logger.log(`${deployment.id} bytecode has only immutable data diff, assume unchanged`); // Local deployedBytecode doesn't contain the immutable data // instead the solc generates the bytecode with 0 as the placeholder return true; } } } this._logger.log(`yellow<${deployment.id} bytecode has changed. Redeploying...>`); return false; } private async ensureVerification <T extends TContract> (Ctor: Constructor<T>, deployment: IDeployment, opts: TVerificationOptions) { if (this.client.platform === 'hardhat' || opts?.verification === false || this.opts.verification === false) { return; } let explorer = await BlockchainExplorerFactory.get(this.client.platform); let verifier = new ContractVerifier(this, explorer); if (deployment.verified != null && /Unable to locate/.test(deployment.verified) === false && /Error 429/.test(deployment.verified) === false && /timeout/.test(deployment.verified) === false) { return; } let diff = (Date.now() / 1000 | 0) - deployment.timestamp; l`Time passed since deployment: bold<${diff}ms>`; if (diff < 5000) { this._logger.log(`Wait to be indexed by explorer: ${diff}ms passed`); await $promise.wait(5000); } let waitConfirmation = opts?.verification !== 'silent'; let address = deployment.implementation ?? deployment.address; try { await verifier.ensure(Ctor, { id: opts?.id, address: address, waitConfirmation: waitConfirmation, proxyFor: opts?.proxyFor, constructorParams: opts?.constructorParams, }); deployment.verified = new Date().toISOString(); await this.store.updateDeployment(deployment); } catch (error) { deployment.verified = error.message; await this.store.updateDeployment(deployment); this._logger.error(`Verification error ${error.stack ?? error.message}`); } } public async fixBytecodeHashesByReread() { let deployments = await this.store.getDeployments(); await alot(deployments).forEachAsync(async (deployment, i) => { this._logger.log(`Fixing BytecodeHashes: ${i}/${deployments.length}`); let address = deployment.implementation ?? deployment.address; let bytecode = await this.client.getCode(address); $require.True($is.Hex(bytecode), `Bytecode not resolved for ${address}`); let bytecodeHash = this.getBytecodeHash(bytecode); deployment.bytecodeHash = bytecodeHash; }).toArrayAsync({ threads: 4 }); await this.store.saveAll(deployments); } private getBytecodeHash(bytecode: TEth.Hex) { let { bytecode: bytecodeRaw } = $bytecode.splitToMetadata(bytecode); return $contract.keccak256(bytecodeRaw); } /** * A simple method to configure the contracts state */ public async configure<T extends TContract, TValue>(Ctor: Constructor<T> | T, opts: { id?: string; // Latest value, if differs from current, the updater will be executed value?: TValue // Current value. current?: TValue | Promise<TValue> | ((x: T) => Promise<TValue>); shouldUpdate?: boolean | (() => boolean | Promise<boolean>) updater: (x: T, value: TValue) => Promise<any> // Will be logged with old and new value title?: string }) { let x: T; if (typeof Ctor === 'function') { x = await this.get(Ctor, { id: opts.id }); } else { x = Ctor; } let currentVal; if ('current' in opts) { let currentMix = opts.current; let current = typeof currentMix === 'function' ? await (currentMix as Function)(x) : await currentMix; if (isEqual(current, opts.value)) { return; } currentVal = current; } if ('shouldUpdate' in opts && opts.shouldUpdate != null) { let shouldUpdate = typeof opts.shouldUpdate === 'boolean' ? opts.shouldUpdate : await opts.shouldUpdate(); if (!shouldUpdate) { return; } } if (opts.title!= null) { let currentStr = currentVal == null || typeof currentVal === 'object' ? '' : ` from ${currentVal}`; this._logger.log(`Update bold<cyan<${opts.title}>> to ${opts.value}${currentStr}`); } await opts.updater(x, opts.value); } } type TInitializer = (...args: any[]) => any type TContract = ContractBase & { $constructor?: (...args: any[]) => any } type TConstructorArgs<T extends TContract> = T['$constructor'] extends Function ? { arguments: ParametersFromSecond<T['$constructor']> } : { arguments?: any[] } function isEqual(a, b) { if (a == null || b == null) { return a == b; } if (typeof a !== 'object' && typeof b !== 'object') { if (typeof a === 'string' && typeof b ==='string') { if (a.startsWith('0x') && b.startsWith('0x') && $is.Hex(a) && $is.Hex(b)) { a = a.toLowerCase(); b = b.toLowerCase(); } } // Not strictly equal return a == b; } // check arrays if (Array.isArray(a) || Array.isArray(b)) { if (a.length !== b.length) { return false; } return a.every((x, i) => { return isEqual(x, b[i]); }); } // check objects for (let key in a) { let aValue = a[key]; let bValue = b[key]; if (isEqual(aValue, bValue) === false) { return false; } } for (let key in b) { if (key in a === false && b[key] != null) { // value present in b, but was not in a return false; } } return true; } function serializeInitData(id: string, contract: ContractBase, initializeParams: any) { let data: TEth.Hex = null; let initializeAbi = contract.abi.find(x => x.name === 'initialize'); if (initializeAbi) { if (initializeParams?.length !== initializeAbi.inputs.length) { throw new Error(`Wrong number of arguments (${initializeParams?.length}) for initializer method (${initializeAbi.inputs.length}) in ${id}.`); } data = $abiUtils.serializeMethodCallData(initializeAbi, initializeParams ?? []); } return data; } /** * Normalize the contract name by removing any versions from name * "FooV1" is actually the "Foo" contract */ function getImplementationId (Ctor: Constructor<TContract>) { let id = Ctor.name; let version = /V?(?<version>\d)$/i.exec(id); if (version) { id = id.substring(0, id.length - version[0].length) }; return id; } namespace Str { export function getDifference (a: TEth.Hex, b: TEth.Hex) { if (a === b) { return [ '', '' ]; } let start = -1; let end = -1; for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) { start = i; break; } } for (let i = a.length - 1; i > start; i--) { if (a[i] !== b[i]) { end = i; break; } } return [ a.substring(start, end + 1), b.substring(start, end + 1), ]; } }