diff options
Diffstat (limited to 'packages/contracts/deploy/src')
-rw-r--r-- | packages/contracts/deploy/src/commands.ts | 19 | ||||
-rw-r--r-- | packages/contracts/deploy/src/compiler.ts | 248 | ||||
-rw-r--r-- | packages/contracts/deploy/src/deployer.ts | 181 | ||||
-rw-r--r-- | packages/contracts/deploy/src/utils/constants.ts | 3 | ||||
-rw-r--r-- | packages/contracts/deploy/src/utils/contract.ts | 81 | ||||
-rw-r--r-- | packages/contracts/deploy/src/utils/encoder.ts | 20 | ||||
-rw-r--r-- | packages/contracts/deploy/src/utils/fs_wrapper.ts | 11 | ||||
-rw-r--r-- | packages/contracts/deploy/src/utils/network.ts | 15 | ||||
-rw-r--r-- | packages/contracts/deploy/src/utils/types.ts | 95 | ||||
-rw-r--r-- | packages/contracts/deploy/src/utils/utils.ts | 13 | ||||
-rw-r--r-- | packages/contracts/deploy/src/utils/web3_wrapper.ts | 132 |
11 files changed, 818 insertions, 0 deletions
diff --git a/packages/contracts/deploy/src/commands.ts b/packages/contracts/deploy/src/commands.ts new file mode 100644 index 000000000..fc421a760 --- /dev/null +++ b/packages/contracts/deploy/src/commands.ts @@ -0,0 +1,19 @@ +import {migrator} from './../migrations/migrate'; +import {Compiler} from './compiler'; +import {Deployer} from './deployer'; +import {CompilerOptions, DeployerOptions} from './utils/types'; + +export const commands = { + async compileAsync(opts: CompilerOptions): Promise<void> { + const compiler = new Compiler(opts); + await compiler.compileAllAsync(); + }, + async migrateAsync(opts: DeployerOptions): Promise<void> { + const deployer = new Deployer(opts); + await migrator.runMigrationsAsync(deployer); + }, + async deployAsync(contractName: string, args: any[], opts: DeployerOptions): Promise<void> { + const deployer = new Deployer(opts); + await deployer.deployAndSaveAsync(contractName, args); + }, +}; diff --git a/packages/contracts/deploy/src/compiler.ts b/packages/contracts/deploy/src/compiler.ts new file mode 100644 index 000000000..5909bdda1 --- /dev/null +++ b/packages/contracts/deploy/src/compiler.ts @@ -0,0 +1,248 @@ +import promisify = require('es6-promisify'); +import * as ethUtil from 'ethereumjs-util'; +import * as _ from 'lodash'; +import * as path from 'path'; +import solc = require('solc'); +import * as Web3 from 'web3'; + +import {binPaths} from './../solc/bin_paths'; +import {fsWrapper} from './utils/fs_wrapper'; +import { + CompilerOptions, + ContractArtifact, + ContractData, + ContractNetworks, + ContractSources, + ImportContents, + SolcErrors, +} from './utils/types'; +import {utils} from './utils/utils'; + +const SOLIDITY_FILE_EXTENSION = '.sol'; + +/** + * Recursively retrieves Solidity source code from directory. + * @param dirPath Directory to search. + * @return Mapping of contract name to contract source. + */ +async function getContractSourcesAsync(dirPath: string): Promise<ContractSources> { + let dirContents: string[] = []; + try { + dirContents = await fsWrapper.readdirAsync(dirPath); + } catch (err) { + throw new Error(`No directory found at ${dirPath}`); + } + let sources: ContractSources = {}; + for (const name of dirContents) { + const contentPath = `${dirPath}/${name}`; + if (path.extname(name) === SOLIDITY_FILE_EXTENSION) { + try { + const opts = { + encoding: 'utf8', + }; + sources[name] = await fsWrapper.readFileAsync(contentPath, opts); + utils.consoleLog(`Reading ${name} source...`); + } catch (err) { + utils.consoleLog(`Could not find file at ${contentPath}`); + } + } else { + try { + const nestedSources = await getContractSourcesAsync(contentPath); + sources = { + ...sources, + ...nestedSources, + }; + } catch (err) { + utils.consoleLog(`${contentPath} is not a directory or ${SOLIDITY_FILE_EXTENSION} file`); + } + } + } + return sources; +} +/** + * Searches Solidity source code for compiler version. + * @param source Source code of contract. + * @return Solc compiler version. + */ +function parseSolidityVersion(source: string): string { + const solcVersionMatch = source.match(/(?:solidity\s\^?)([0-9]{1,2}[.][0-9]{1,2}[.][0-9]{1,2})/); + if (_.isNull(solcVersionMatch)) { + throw new Error('Could not find Solidity version in source'); + } + const solcVersion = solcVersionMatch[1]; + return solcVersion; +} +/** + * Normalizes the path found in the error message. + * Example: converts 'base/Token.sol:6:46: Warning: Unused local variable' + * to 'Token.sol:6:46: Warning: Unused local variable' + * This is used to prevent logging the same error multiple times. + * @param errMsg An error message from the compiled output. + * @return The error message with directories truncated from the contract path. + */ +function getNormalizedErrMsg(errMsg: string): string { + const errPathMatch = errMsg.match(/(.*\.sol)/); + if (_.isNull(errPathMatch)) { + throw new Error('Could not find a path in error message'); + } + const errPath = errPathMatch[0]; + const baseContract = path.basename(errPath); + const normalizedErrMsg = errMsg.replace(errPath, baseContract); + return normalizedErrMsg; +} + +export class Compiler { + private contractsDir: string; + private networkId: number; + private optimizerEnabled: number; + private artifactsDir: string; + private contractSourcesIfExists?: ContractSources; + private solcErrors: Set<string>; + + constructor(opts: CompilerOptions) { + this.contractsDir = opts.contractsDir; + this.networkId = opts.networkId; + this.optimizerEnabled = opts.optimizerEnabled; + this.artifactsDir = opts.artifactsDir; + this.solcErrors = new Set(); + } + /** + * Compiles all Solidity files found in contractsDir and writes JSON artifacts to artifactsDir. + */ + public async compileAllAsync(): Promise<void> { + await this.createArtifactsDirIfDoesNotExistAsync(); + this.contractSourcesIfExists = await getContractSourcesAsync(this.contractsDir); + + const contractBaseNames = _.keys(this.contractSourcesIfExists); + const compiledContractPromises = _.map(contractBaseNames, async (contractBaseName: string): Promise<void> => { + return this.compileContractAsync(contractBaseName); + }); + await Promise.all(compiledContractPromises); + + this.solcErrors.forEach(errMsg => { + utils.consoleLog(errMsg); + }); + } + /** + * Compiles contract and saves artifact to artifactsDir. + * @param contractBaseName Name of contract with '.sol' extension. + */ + private async compileContractAsync(contractBaseName: string): Promise<void> { + if (_.isUndefined(this.contractSourcesIfExists)) { + throw new Error('Contract sources not yet initialized'); + } + + const source = this.contractSourcesIfExists[contractBaseName]; + const contractName = path.basename(contractBaseName, SOLIDITY_FILE_EXTENSION); + const currentArtifactPath = `${this.artifactsDir}/${contractName}.json`; + const sourceHash = `0x${ethUtil.sha3(source).toString('hex')}`; + + let currentArtifactString: string; + let currentArtifact: ContractArtifact; + let oldNetworks: ContractNetworks; + let shouldCompile: boolean; + try { + const opts = { + encoding: 'utf8', + }; + currentArtifactString = await fsWrapper.readFileAsync(currentArtifactPath, opts); + currentArtifact = JSON.parse(currentArtifactString); + oldNetworks = currentArtifact.networks; + const oldNetwork: ContractData = oldNetworks[this.networkId]; + shouldCompile = _.isUndefined(oldNetwork) || + oldNetwork.keccak256 !== sourceHash || + oldNetwork.optimizer_enabled !== this.optimizerEnabled; + } catch (err) { + shouldCompile = true; + } + + if (!shouldCompile) { + return; + } + + const input = { + [contractBaseName]: source, + }; + const solcVersion = parseSolidityVersion(source); + const fullSolcVersion = binPaths[solcVersion]; + const solcBinPath = `./../solc/solc_bin/${fullSolcVersion}`; + const solcBin = require(solcBinPath); + const solcInstance = solc.setupMethods(solcBin); + + utils.consoleLog(`Compiling ${contractBaseName}...`); + const sourcesToCompile = { + sources: input, + }; + const compiled = solcInstance.compile(sourcesToCompile, + this.optimizerEnabled, + this.findImportsIfSourcesExist.bind(this)); + + if (!_.isUndefined(compiled.errors)) { + _.each(compiled.errors, errMsg => { + const normalizedErrMsg = getNormalizedErrMsg(errMsg); + this.solcErrors.add(normalizedErrMsg); + }); + } + + const contractIdentifier = `${contractBaseName}:${contractName}`; + const abi: Web3.ContractAbi = JSON.parse(compiled.contracts[contractIdentifier].interface); + const unlinked_binary = `0x${compiled.contracts[contractIdentifier].bytecode}`; + const updated_at = Date.now(); + const contractData: ContractData = { + solc_version: solcVersion, + keccak256: sourceHash, + optimizer_enabled: this.optimizerEnabled, + abi, + unlinked_binary, + updated_at, + }; + + let newArtifact: ContractArtifact; + if (!_.isUndefined(currentArtifactString)) { + newArtifact = { + ...currentArtifact, + networks: { + ...oldNetworks, + [this.networkId]: contractData, + }, + }; + } else { + newArtifact = { + contract_name: contractName, + networks: { + [this.networkId]: contractData, + }, + }; + } + + const artifactString = utils.stringifyWithFormatting(newArtifact); + await fsWrapper.writeFileAsync(currentArtifactPath, artifactString); + utils.consoleLog(`${contractBaseName} artifact saved!`); + } + /** + * Callback to resolve dependencies with `solc.compile`. + * Throws error if contractSources not yet initialized. + * @param importPath Path to an imported dependency. + * @return Import contents object containing source code of dependency. + */ + private findImportsIfSourcesExist(importPath: string): ImportContents { + if (_.isUndefined(this.contractSourcesIfExists)) { + throw new Error('Contract sources not yet initialized'); + } + const contractBaseName = path.basename(importPath); + const source = this.contractSourcesIfExists[contractBaseName]; + const importContents: ImportContents = { + contents: source, + }; + return importContents; + } + /** + * Creates the artifacts directory if it does not already exist. + */ + private async createArtifactsDirIfDoesNotExistAsync(): Promise<void> { + if (!fsWrapper.doesPathExistSync(this.artifactsDir)) { + utils.consoleLog('Creating artifacts directory...'); + await fsWrapper.mkdirAsync(this.artifactsDir); + } + } +} 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; + } +} diff --git a/packages/contracts/deploy/src/utils/constants.ts b/packages/contracts/deploy/src/utils/constants.ts new file mode 100644 index 000000000..8871a470d --- /dev/null +++ b/packages/contracts/deploy/src/utils/constants.ts @@ -0,0 +1,3 @@ +export const constants = { + NULL_BYTES: '0x', +}; diff --git a/packages/contracts/deploy/src/utils/contract.ts b/packages/contracts/deploy/src/utils/contract.ts new file mode 100644 index 000000000..e9c49c9f1 --- /dev/null +++ b/packages/contracts/deploy/src/utils/contract.ts @@ -0,0 +1,81 @@ +import {schemas, SchemaValidator} from '@0xproject/json-schemas'; +import promisify = require('es6-promisify'); +import * as _ from 'lodash'; +import * as Web3 from 'web3'; + +import {AbiType} from './types'; + +export class Contract implements Web3.ContractInstance { + public address: string; + public abi: Web3.ContractAbi; + private contract: Web3.ContractInstance; + private defaults: Partial<Web3.TxData>; + private validator: SchemaValidator; + // This class instance is going to be populated with functions and events depending on the ABI + // and we don't know their types in advance + [name: string]: any; + constructor(web3ContractInstance: Web3.ContractInstance, defaults: Partial<Web3.TxData>) { + this.contract = web3ContractInstance; + this.address = web3ContractInstance.address; + this.abi = web3ContractInstance.abi; + this.defaults = defaults; + this.populateEvents(); + this.populateFunctions(); + this.validator = new SchemaValidator(); + } + private populateFunctions(): void { + const functionsAbi = _.filter(this.abi, abiPart => abiPart.type === AbiType.Function); + _.forEach(functionsAbi, (functionAbi: Web3.MethodAbi) => { + if (functionAbi.constant) { + const cbStyleCallFunction = this.contract[functionAbi.name].call; + this[functionAbi.name] = { + callAsync: promisify(cbStyleCallFunction, this.contract), + }; + } else { + const cbStyleFunction = this.contract[functionAbi.name]; + const cbStyleEstimateGasFunction = this.contract[functionAbi.name].estimateGas; + this[functionAbi.name] = { + estimateGasAsync: promisify(cbStyleEstimateGasFunction, this.contract), + sendTransactionAsync: this.promisifyWithDefaultParams(cbStyleFunction), + }; + } + }); + } + private populateEvents(): void { + const eventsAbi = _.filter(this.abi, abiPart => abiPart.type === AbiType.Event); + _.forEach(eventsAbi, (eventAbi: Web3.EventAbi) => { + this[eventAbi.name] = this.contract[eventAbi.name]; + }); + } + private promisifyWithDefaultParams(fn: (...args: any[]) => void): (...args: any[]) => Promise<any> { + const promisifiedWithDefaultParams = async (...args: any[]) => { + const promise = new Promise((resolve, reject) => { + const lastArg = args[args.length - 1]; + let txData: Partial<Web3.TxData> = {}; + if (this.isTxData(lastArg)) { + txData = args.pop(); + } + txData = { + ...this.defaults, + ...txData, + }; + const callback = (err: Error, data: any) => { + if (_.isNull(err)) { + resolve(data); + } else { + reject(err); + } + }; + args.push(txData); + args.push(callback); + fn.apply(this.contract, args); + }); + return promise; + }; + return promisifiedWithDefaultParams; + } + private isTxData(lastArg: any): boolean { + const isValid = this.validator.isValid(lastArg, schemas.txDataSchema); + return isValid; + } +} diff --git a/packages/contracts/deploy/src/utils/encoder.ts b/packages/contracts/deploy/src/utils/encoder.ts new file mode 100644 index 000000000..0248e9f03 --- /dev/null +++ b/packages/contracts/deploy/src/utils/encoder.ts @@ -0,0 +1,20 @@ +import * as _ from 'lodash'; +import * as Web3 from 'web3'; +import * as web3Abi from 'web3-eth-abi'; + +import {AbiType} from './types'; + +export const encoder = { + encodeConstructorArgsFromAbi(args: any[], abi: Web3.ContractAbi): string { + const constructorTypes: string[] = []; + _.each(abi, (element: Web3.AbiDefinition) => { + if (element.type === AbiType.Constructor) { + _.each(element.inputs, (input: Web3.FunctionParameter) => { + constructorTypes.push(input.type); + }); + } + }); + const encodedParameters = web3Abi.encodeParameters(constructorTypes, args); + return encodedParameters; + }, +}; diff --git a/packages/contracts/deploy/src/utils/fs_wrapper.ts b/packages/contracts/deploy/src/utils/fs_wrapper.ts new file mode 100644 index 000000000..6b4fd625c --- /dev/null +++ b/packages/contracts/deploy/src/utils/fs_wrapper.ts @@ -0,0 +1,11 @@ +import promisify = require('es6-promisify'); +import * as fs from 'fs'; + +export const fsWrapper = { + readdirAsync: promisify(fs.readdir), + readFileAsync: promisify(fs.readFile), + writeFileAsync: promisify(fs.writeFile), + mkdirAsync: promisify(fs.mkdir), + doesPathExistSync: fs.existsSync, + removeFileAsync: promisify(fs.unlink), +}; diff --git a/packages/contracts/deploy/src/utils/network.ts b/packages/contracts/deploy/src/utils/network.ts new file mode 100644 index 000000000..74123e6a5 --- /dev/null +++ b/packages/contracts/deploy/src/utils/network.ts @@ -0,0 +1,15 @@ +import promisify = require('es6-promisify'); +import * as Web3 from 'web3'; + +import {Web3Wrapper} from './web3_wrapper'; + +export const network = { + async getNetworkIdIfExistsAsync(port: number): Promise<number> { + const url = `http://localhost:${port}`; + const web3Provider = new Web3.providers.HttpProvider(url); + const defaults = {}; + const web3Wrapper = new Web3Wrapper(web3Provider, defaults); + const networkIdIfExists = await web3Wrapper.getNetworkIdIfExistsAsync(); + return networkIdIfExists; + }, +}; diff --git a/packages/contracts/deploy/src/utils/types.ts b/packages/contracts/deploy/src/utils/types.ts new file mode 100644 index 000000000..855f1e849 --- /dev/null +++ b/packages/contracts/deploy/src/utils/types.ts @@ -0,0 +1,95 @@ +import * as Web3 from 'web3'; + +export enum AbiType { + Function = 'function', + Constructor = 'constructor', + Event = 'event', + Fallback = 'fallback', +} + +export interface ContractArtifact { + contract_name: string; + networks: ContractNetworks; +} + +export interface ContractNetworks { + [key: number]: ContractData; +} + +export interface ContractData { + solc_version: string; + optimizer_enabled: number; + keccak256: string; + abi: Web3.ContractAbi; + unlinked_binary: string; + address?: string; + constructor_args?: string; + updated_at: number; +} + +export interface SolcErrors { + [key: string]: boolean; +} + +export interface CliOptions { + artifactsDir: string; + contractsDir: string; + jsonrpcPort: number; + networkId: number; + shouldOptimize: boolean; + gasPrice: string; + account?: string; + contract?: string; + args?: string; +} + +export interface CompilerOptions { + contractsDir: string; + networkId: number; + optimizerEnabled: number; + artifactsDir: string; +} + +export interface DeployerOptions { + artifactsDir: string; + jsonrpcPort: number; + networkId: number; + defaults: Partial<Web3.TxData>; +} + +export interface ContractSources { + [key: string]: string; +} + +export interface ImportContents { + contents: string; +} + +// TODO: Consolidate with 0x.js definitions once types are moved into a separate package. +export enum ZeroExError { + ContractDoesNotExist = 'CONTRACT_DOES_NOT_EXIST', + ExchangeContractDoesNotExist = 'EXCHANGE_CONTRACT_DOES_NOT_EXIST', + UnhandledError = 'UNHANDLED_ERROR', + UserHasNoAssociatedAddress = 'USER_HAS_NO_ASSOCIATED_ADDRESSES', + InvalidSignature = 'INVALID_SIGNATURE', + ContractNotDeployedOnNetwork = 'CONTRACT_NOT_DEPLOYED_ON_NETWORK', + InsufficientAllowanceForTransfer = 'INSUFFICIENT_ALLOWANCE_FOR_TRANSFER', + InsufficientBalanceForTransfer = 'INSUFFICIENT_BALANCE_FOR_TRANSFER', + InsufficientEthBalanceForDeposit = 'INSUFFICIENT_ETH_BALANCE_FOR_DEPOSIT', + InsufficientWEthBalanceForWithdrawal = 'INSUFFICIENT_WETH_BALANCE_FOR_WITHDRAWAL', + InvalidJump = 'INVALID_JUMP', + OutOfGas = 'OUT_OF_GAS', + NoNetworkId = 'NO_NETWORK_ID', + SubscriptionNotFound = 'SUBSCRIPTION_NOT_FOUND', +} + +export interface Token { + address?: string; + name: string; + symbol: string; + decimals: number; + ipfsHash: string; + swarmHash: string; +} + +export type DoneCallback = (err?: Error) => void; diff --git a/packages/contracts/deploy/src/utils/utils.ts b/packages/contracts/deploy/src/utils/utils.ts new file mode 100644 index 000000000..4390d8813 --- /dev/null +++ b/packages/contracts/deploy/src/utils/utils.ts @@ -0,0 +1,13 @@ +export const utils = { + consoleLog(message: string): void { + /* tslint:disable */ + console.log(message); + /* tslint:enable */ + }, + stringifyWithFormatting(obj: any): string { + const jsonReplacer: null = null; + const numberOfJsonSpaces = 4; + const stringifiedObj = JSON.stringify(obj, jsonReplacer, numberOfJsonSpaces); + return stringifiedObj; + }, +}; diff --git a/packages/contracts/deploy/src/utils/web3_wrapper.ts b/packages/contracts/deploy/src/utils/web3_wrapper.ts new file mode 100644 index 000000000..0209da26d --- /dev/null +++ b/packages/contracts/deploy/src/utils/web3_wrapper.ts @@ -0,0 +1,132 @@ +import BigNumber from 'bignumber.js'; +import promisify = require('es6-promisify'); +import * as _ from 'lodash'; +import * as Web3 from 'web3'; + +import {Contract} from './contract'; +import {ZeroExError} from './types'; + +export class Web3Wrapper { + private web3: Web3; + private defaults: Partial<Web3.TxData>; + private networkIdIfExists?: number; + private jsonRpcRequestId: number; + constructor(provider: Web3.Provider, defaults: Partial<Web3.TxData>) { + this.web3 = new Web3(); + this.web3.setProvider(provider); + this.defaults = defaults; + this.jsonRpcRequestId = 0; + } + public setProvider(provider: Web3.Provider) { + delete this.networkIdIfExists; + this.web3.setProvider(provider); + } + public isAddress(address: string): boolean { + return this.web3.isAddress(address); + } + public getContractFromAbi(abi: Web3.ContractAbi): Web3.Contract<Web3.ContractInstance> { + const contract = this.web3.eth.contract(abi); + return contract; + } + public async isSenderAddressAvailableAsync(senderAddress: string): Promise<boolean> { + const addresses = await this.getAvailableAddressesAsync(); + return _.includes(addresses, senderAddress); + } + public async getNodeVersionAsync(): Promise<string> { + const nodeVersion = await promisify(this.web3.version.getNode)(); + return nodeVersion; + } + public async getTransactionReceiptAsync(txHash: string): Promise<Web3.TransactionReceipt> { + const transactionReceipt = await promisify(this.web3.eth.getTransactionReceipt)(txHash); + return transactionReceipt; + } + public getCurrentProvider(): Web3.Provider { + return this.web3.currentProvider; + } + public async getNetworkIdIfExistsAsync(): Promise<number|undefined> { + if (!_.isUndefined(this.networkIdIfExists)) { + return this.networkIdIfExists; + } + + try { + const networkId = await this.getNetworkAsync(); + this.networkIdIfExists = Number(networkId); + return this.networkIdIfExists; + } catch (err) { + return undefined; + } + } + public toWei(ethAmount: BigNumber): BigNumber { + const balanceWei = this.web3.toWei(ethAmount, 'ether'); + return balanceWei; + } + public async getBalanceInWeiAsync(owner: string): Promise<BigNumber> { + let balanceInWei = await promisify(this.web3.eth.getBalance)(owner); + balanceInWei = new BigNumber(balanceInWei); + return balanceInWei; + } + public async doesContractExistAtAddressAsync(address: string): Promise<boolean> { + const code = await promisify(this.web3.eth.getCode)(address); + // Regex matches 0x0, 0x00, 0x in order to accommodate poorly implemented clients + const codeIsEmpty = /^0x0{0,40}$/i.test(code); + return !codeIsEmpty; + } + public async signTransactionAsync(address: string, message: string): Promise<string> { + const signData = await promisify(this.web3.eth.sign)(address, message); + return signData; + } + public async getBlockAsync(blockParam: string|Web3.BlockParam): Promise<Web3.BlockWithoutTransactionData> { + const block = await promisify(this.web3.eth.getBlock)(blockParam); + return block; + } + public async getBlockTimestampAsync(blockParam: string|Web3.BlockParam): Promise<number> { + const {timestamp} = await this.getBlockAsync(blockParam); + return timestamp; + } + public async getAvailableAddressesAsync(): Promise<string[]> { + const addresses: string[] = await promisify(this.web3.eth.getAccounts)(); + return addresses; + } + public async getLogsAsync(filter: Web3.FilterObject): Promise<Web3.LogEntry[]> { + let fromBlock = filter.fromBlock; + if (_.isNumber(fromBlock)) { + fromBlock = this.web3.toHex(fromBlock); + } + let toBlock = filter.toBlock; + if (_.isNumber(toBlock)) { + toBlock = this.web3.toHex(toBlock); + } + const serializedFilter = { + ...filter, + fromBlock, + toBlock, + }; + const payload = { + jsonrpc: '2.0', + id: this.jsonRpcRequestId++, + method: 'eth_getLogs', + params: [serializedFilter], + }; + const logs = await this.sendRawPayloadAsync(payload); + return logs; + } + public async estimateGasAsync(callData: Web3.CallData): Promise<number> { + const gasEstimate = await promisify(this.web3.eth.estimateGas)(callData); + return gasEstimate; + } + private getContractInstance<A extends Web3.ContractInstance>(abi: Web3.ContractAbi, address: string): A { + const web3ContractInstance = this.web3.eth.contract(abi).at(address); + const contractInstance = new Contract(web3ContractInstance, this.defaults) as any as A; + return contractInstance; + } + private async getNetworkAsync(): Promise<number> { + const networkId = await promisify(this.web3.version.getNetwork)(); + return networkId; + } + private async sendRawPayloadAsync(payload: Web3.JSONRPCRequestPayload): Promise<any> { + const sendAsync = this.web3.currentProvider.sendAsync.bind(this.web3.currentProvider); + const response = await promisify(sendAsync)(payload); + const result = response.result; + return result; + } +} |