aboutsummaryrefslogtreecommitdiffstats
path: root/packages/contracts/deploy/src/deployer.ts
blob: 991504972f00af3f2947f0bee78d53531453f2ce (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import {TxData} from '@0xproject/types';
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<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;
    }
}