diff options
Diffstat (limited to 'packages/contracts/deploy/src/deployer.ts')
-rw-r--r-- | packages/contracts/deploy/src/deployer.ts | 181 |
1 files changed, 181 insertions, 0 deletions
diff --git a/packages/contracts/deploy/src/deployer.ts b/packages/contracts/deploy/src/deployer.ts new file mode 100644 index 000000000..48d175a42 --- /dev/null +++ b/packages/contracts/deploy/src/deployer.ts @@ -0,0 +1,181 @@ +import promisify = require('es6-promisify'); +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'; +import {Web3Wrapper} from './utils/web3_wrapper'; + +// 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<Web3.TxData>; + + 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.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<Web3.ContractInstance> { + 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<Web3.ContractInstance> { + 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<any> { + const contract: Web3.Contract<Web3.ContractInstance> = 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<void> { + 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<string> { + 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<number> { + 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; + } +} |