aboutsummaryrefslogblamecommitdiffstats
path: root/packages/deployer/src/deployer.ts
blob: c8c3a9a064f8ffe1bc0c7acff21a7a9404877133 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
                                                                                          
                                            
                                                      
                            
                             

                             


                                               



                        
                            
                       
                       
                                      



                                                                                 



                                                                                 

                                    
                                  

                                       













                                                                                                                    




                                                                                      
                                        
                                               
                                         
                                       
                               
                                                                        

                                                                       
                                                                                                                   
             
                                                                   
                
                                                                  
         
                                                                     

       

                                                                                                                          



                                                                                                      
                                                                                                            

                                                                                                                 

                                                                   
                        
                                              



                 


                                                                          
                                       
                                                                                            










                                                                                                                      
                                                                                       
                                                                                                     
                                                                                    


                                

                                                                                                              





                                                                                                                 
                                                                                                  








                                                                         
                                                                                                    









                                                                                                        
                                                                            












                                                                                             




                                                   
                                                                                                            
                                                                                                                 


                                                                          
                                       
                                                                                       
                                                      
                                     

                                                    

                             
                                        
                       
                                                     
                                                   


                                                                          
                                                                          






                                                                       

                                                                                   



                                                                             
                                                                                                   


         


                                                                                                        
                                                           
                         
                                                 


                                                                                 
                                       








                                                                                           
                                                                                


                                                                     
                                                                                          






                                                                    
import { AbiType, ConstructorAbi, ContractAbi, Provider, TxData } from '@0xproject/types';
import { logUtils } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as _ from 'lodash';
import * as solc from 'solc';
import * as Web3 from 'web3';

import { Contract } from './utils/contract';
import { encoder } from './utils/encoder';
import { fsWrapper } from './utils/fs_wrapper';
import {
    ContractArtifact,
    ContractNetworkData,
    DeployerOptions,
    ProviderDeployerOptions,
    UrlDeployerOptions,
} 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;

/**
 * The Deployer facilitates deploying Solidity smart contracts to the blockchain.
 * It can be used to build custom migration scripts.
 */
export class Deployer {
    public web3Wrapper: Web3Wrapper;
    private _artifactsDir: string;
    private _networkId: number;
    private _defaults: Partial<TxData>;
    /**
     * Gets data for current version stored in artifact.
     * @param contractArtifact The contract artifact.
     * @return Version specific contract data.
     */
    private static _getContractCompilerOutputFromArtifactIfExists(
        contractArtifact: ContractArtifact,
    ): solc.StandardContractOutput {
        const compilerOutputIfExists = contractArtifact.compilerOutput;
        if (_.isUndefined(compilerOutputIfExists)) {
            throw new Error(`Compiler output not found in artifact for contract: ${contractArtifact.contractName}`);
        }
        return compilerOutputIfExists;
    }
    /**
     * Instantiate a new instance of the Deployer class.
     * @param opts Deployer options, including either an RPC url or Provider instance.
     * @returns A Deployer instance
     */
    constructor(opts: DeployerOptions) {
        this._artifactsDir = opts.artifactsDir;
        this._networkId = opts.networkId;
        this._defaults = opts.defaults;
        let provider: Provider;
        if (_.isUndefined((opts as ProviderDeployerOptions).provider)) {
            const jsonrpcUrl = (opts as UrlDeployerOptions).jsonrpcUrl;
            if (_.isUndefined(jsonrpcUrl)) {
                throw new Error(`Deployer options don't contain provider nor jsonrpcUrl. Please pass one of them`);
            }
            provider = new Web3.providers.HttpProvider(jsonrpcUrl);
        } else {
            provider = (opts as ProviderDeployerOptions).provider;
        }
        this.web3Wrapper = new Web3Wrapper(provider, this._defaults);
    }
    /**
     * Loads a contract's corresponding artifacts and deploys it with the supplied constructor arguments.
     * @param contractName Name of the contract to deploy. Must match name of an artifact in supplied artifacts directory.
     * @param args Array of contract constructor arguments.
     * @return Deployed contract instance.
     */
    public async deployAsync(contractName: string, args: any[] = []): Promise<Web3.ContractInstance> {
        const contractArtifactIfExists: ContractArtifact = this._loadContractArtifactIfExists(contractName);
        const compilerOutput = Deployer._getContractCompilerOutputFromArtifactIfExists(contractArtifactIfExists);
        const data = compilerOutput.evm.bytecode.object;
        const from = await this._getFromAddressAsync();
        const gas = await this._getAllowableGasEstimateAsync(data);
        const txData = {
            gasPrice: this._defaults.gasPrice,
            from,
            data,
            gas,
        };
        if (_.isUndefined(compilerOutput.abi)) {
            throw new Error(`ABI not found in ${contractName} artifacts`);
        }
        const abi = compilerOutput.abi;
        const constructorAbi = _.find(abi, { type: AbiType.Constructor }) as ConstructorAbi;
        const constructorArgs = _.isUndefined(constructorAbi) ? [] : constructorAbi.inputs;
        if (constructorArgs.length !== args.length) {
            const constructorSignature = `constructor(${_.map(constructorArgs, arg => `${arg.type} ${arg.name}`).join(
                ', ',
            )})`;
            throw new Error(
                `${contractName} expects ${constructorArgs.length} constructor params: ${constructorSignature}. Got ${
                    args.length
                }`,
            );
        }
        const web3ContractInstance = await this._deployFromAbiAsync(abi, args, txData);
        logUtils.log(`${contractName}.sol successfully deployed at ${web3ContractInstance.address}`);
        const contractInstance = new Contract(web3ContractInstance, this._defaults);
        return contractInstance;
    }
    /**
     * Loads a contract's artifact, deploys it with supplied constructor arguments, and saves the updated data
     * back to the artifact file.
     * @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: ContractAbi, args: any[], txData: 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)) {
                    logUtils.log(`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 contractArtifactIfExists: ContractArtifact = this._loadContractArtifactIfExists(contractName);
        const compilerOutput = Deployer._getContractCompilerOutputFromArtifactIfExists(contractArtifactIfExists);
        if (_.isUndefined(compilerOutput.abi)) {
            throw new Error(`ABI not found in ${contractName} artifacts`);
        }
        const abi = compilerOutput.abi;
        const encodedConstructorArgs = encoder.encodeConstructorArgsFromAbi(args, abi);
        const newContractData: ContractNetworkData = {
            address: contractAddress,
            links: {},
            constructorArgs: encodedConstructorArgs,
        };
        const newArtifact = {
            ...contractArtifactIfExists,
            networks: {
                ...contractArtifactIfExists.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} at ${artifactPath}`);
        }
    }
    /**
     * 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;
    }
}