import {TxData} from '@0xproject/types'; import {promisify} from '@0xproject/utils'; import {Web3Wrapper} from '@0xproject/web3-wrapper'; import * as _ from 'lodash'; import * as Web3 from 'web3'; import {Contract} from './utils/contract'; import {encoder} from './utils/encoder'; import {fsWrapper} from './utils/fs_wrapper'; import { ContractArtifact, ContractData, DeployerOptions, } from './utils/types'; import {utils} from './utils/utils'; // Gas added to gas estimate to make sure there is sufficient gas for deployment. const EXTRA_GAS = 200000; export class Deployer { public web3Wrapper: Web3Wrapper; private artifactsDir: string; private jsonrpcPort: number; private networkId: number; private defaults: Partial; constructor(opts: DeployerOptions) { this.artifactsDir = opts.artifactsDir; this.jsonrpcPort = opts.jsonrpcPort; this.networkId = opts.networkId; const jsonrpcUrl = `http://localhost:${this.jsonrpcPort}`; const web3Provider = new Web3.providers.HttpProvider(jsonrpcUrl); this.defaults = opts.defaults; this.web3Wrapper = new Web3Wrapper(web3Provider, this.networkId, this.defaults); } /** * Loads contract artifact and deploys contract with given arguments. * @param contractName Name of the contract to deploy. Must match name of an artifact in artifacts directory. * @param args Array of contract constructor arguments. * @return Deployed contract instance. */ public async deployAsync(contractName: string, args: any[] = []): Promise { const contractArtifact: ContractArtifact = this.loadContractArtifactIfExists(contractName); const contractData: ContractData = this.getContractDataFromArtifactIfExists(contractArtifact); const data = contractData.unlinked_binary; const from = await this.getFromAddressAsync(); const gas = await this.getAllowableGasEstimateAsync(data); const txData = { gasPrice: this.defaults.gasPrice, from, data, gas, }; const abi = contractData.abi; const web3ContractInstance = await this.deployFromAbiAsync(abi, args, txData); utils.consoleLog(`${contractName}.sol successfully deployed at ${web3ContractInstance.address}`); const contractInstance = new Contract(web3ContractInstance, this.defaults); return contractInstance; } /** * Loads contract artifact, deploys with given arguments, and saves updated data to artifact. * @param contractName Name of the contract to deploy. Must match name of an artifact in artifacts directory. * @param args Array of contract constructor arguments. * @return Deployed contract instance. */ public async deployAndSaveAsync(contractName: string, args: any[] = []): Promise { const contractInstance = await this.deployAsync(contractName, args); await this.saveContractDataToArtifactAsync(contractName, contractInstance.address, args); return contractInstance; } /** * Deploys a contract given its ABI, arguments, and transaction data. * @param abi ABI of contract to deploy. * @param args Constructor arguments to use in deployment. * @param txData Tx options used for deployment. * @return Promise that resolves to a web3 contract instance. */ private async deployFromAbiAsync(abi: Web3.ContractAbi, args: any[], txData: Web3.TxData): Promise { const contract: Web3.Contract = this.web3Wrapper.getContractFromAbi(abi); const deployPromise = new Promise((resolve, reject) => { /** * Contract is inferred as 'any' because TypeScript * is not able to read 'new' from the Contract interface */ (contract as any).new(...args, txData, (err: Error, res: any): any => { if (err) { reject(err); } else if (_.isUndefined(res.address) && !_.isUndefined(res.transactionHash)) { utils.consoleLog(`transactionHash: ${res.transactionHash}`); } else { resolve(res); } }); }); return deployPromise; } /** * Updates a contract artifact's address and encoded constructor arguments. * @param contractName Name of contract. Must match an existing artifact. * @param contractAddress Contract address to save to artifact. * @param args Contract constructor arguments that will be encoded and saved to artifact. */ private async saveContractDataToArtifactAsync(contractName: string, contractAddress: string, args: any[]): Promise { const contractArtifact: ContractArtifact = this.loadContractArtifactIfExists(contractName); const contractData: ContractData = this.getContractDataFromArtifactIfExists(contractArtifact); const abi = contractData.abi; const encodedConstructorArgs = encoder.encodeConstructorArgsFromAbi(args, abi); const newContractData = { ...contractData, address: contractAddress, constructor_args: encodedConstructorArgs, }; const newArtifact = { ...contractArtifact, networks: { ...contractArtifact.networks, [this.networkId]: newContractData, }, }; const artifactString = utils.stringifyWithFormatting(newArtifact); const artifactPath = `${this.artifactsDir}/${contractName}.json`; await fsWrapper.writeFileAsync(artifactPath, artifactString); } /** * Loads a contract artifact, if it exists. * @param contractName Name of the contract, without the extension. * @return The contract artifact. */ private loadContractArtifactIfExists(contractName: string): ContractArtifact { const artifactPath = `${this.artifactsDir}/${contractName}.json`; try { const contractArtifact: ContractArtifact = require(artifactPath); return contractArtifact; } catch (err) { throw new Error(`Artifact not found for contract: ${contractName}`); } } /** * Gets data for current networkId stored in artifact. * @param contractArtifact The contract artifact. * @return Network specific contract data. */ private getContractDataFromArtifactIfExists(contractArtifact: ContractArtifact): ContractData { const contractData = contractArtifact.networks[this.networkId]; if (_.isUndefined(contractData)) { throw new Error(`Data not found in artifact for contract: ${contractArtifact.contract_name}`); } return contractData; } /** * Gets the address to use for sending a transaction. * @return The default from address. If not specified, returns the first address accessible by web3. */ private async getFromAddressAsync(): Promise { let from: string; if (_.isUndefined(this.defaults.from)) { const accounts = await this.web3Wrapper.getAvailableAddressesAsync(); from = accounts[0]; } else { from = this.defaults.from; } return from; } /** * Estimates the gas required for a transaction. * If gas would be over the block gas limit, the max allowable gas is returned instead. * @param data Bytecode to estimate gas for. * @return Gas estimate for transaction data. */ private async getAllowableGasEstimateAsync(data: string): Promise { const block = await this.web3Wrapper.getBlockAsync('latest'); let gas: number; try { const gasEstimate: number = await this.web3Wrapper.estimateGasAsync(data); gas = Math.min(gasEstimate + EXTRA_GAS, block.gasLimit); } catch (err) { gas = block.gasLimit; } return gas; } }