From bc8fc534332b5ea82f881bdd3a75773384714f4d Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Thu, 25 May 2017 19:47:11 +0200 Subject: Add initial exchange contract function, set up web3Wrapper, added types and utils --- src/ts/0x.js.ts | 22 +++++---- src/ts/contract_wrappers/contract_wrapper.ts | 51 ++++++++++++++++++++ src/ts/contract_wrappers/exchange_wrapper.ts | 37 +++++++++++++++ src/ts/globals.d.ts | 28 +++++++++++ src/ts/types.ts | 30 ++++++++++++ src/ts/utils/assert.ts | 21 ++++---- src/ts/utils/utils.ts | 7 +++ src/ts/web3_wrapper.ts | 71 ++++++++++++++++++++++++++++ 8 files changed, 249 insertions(+), 18 deletions(-) create mode 100644 src/ts/contract_wrappers/contract_wrapper.ts create mode 100644 src/ts/contract_wrappers/exchange_wrapper.ts create mode 100644 src/ts/types.ts create mode 100644 src/ts/utils/utils.ts create mode 100644 src/ts/web3_wrapper.ts (limited to 'src/ts') diff --git a/src/ts/0x.js.ts b/src/ts/0x.js.ts index ead1f56df..7025f8804 100644 --- a/src/ts/0x.js.ts +++ b/src/ts/0x.js.ts @@ -1,21 +1,21 @@ import * as BigNumber from 'bignumber.js'; import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; +import Web3 from 'web3'; import {assert} from './utils/assert'; +import {utils} from './utils/utils'; +import {ZeroExError} from './types'; +import {Web3Wrapper} from './web3_wrapper'; +import {ExchangeWrapper} from './contract_wrappers/exchange_wrapper'; +import contract = require('truffle-contract'); import {ECSignatureSchema} from './schemas/ec_signature_schema'; - -/** - * Elliptic Curve signature - */ -export interface ECSignature { - v: number; - r: string; - s: string; -} +import {ECSignature} from './types'; const MAX_DIGITS_IN_UNSIGNED_256_INT = 78; export class ZeroEx { + public web3Wrapper: Web3Wrapper; + public exchange: ContractInstance; /** * Verifies that the elliptic curve signature `signature` was generated * by signing `data` with the private key corresponding to the `signerAddressHex` address. @@ -83,4 +83,8 @@ export class ZeroEx { const baseUnitAmount = amount.times(unit); return baseUnitAmount; } + constructor(web3: Web3) { + this.web3Wrapper = new Web3Wrapper(web3); + this.exchange = new ExchangeWrapper(this.web3Wrapper); + } } diff --git a/src/ts/contract_wrappers/contract_wrapper.ts b/src/ts/contract_wrappers/contract_wrapper.ts new file mode 100644 index 000000000..72bfffe95 --- /dev/null +++ b/src/ts/contract_wrappers/contract_wrapper.ts @@ -0,0 +1,51 @@ +import * as _ from 'lodash'; +import {Web3Wrapper} from '../web3_wrapper'; +import {ZeroExError} from '../types'; +import {utils} from '../utils/utils'; + +export class ContractWrapper { + public web3Wrapper: Web3Wrapper; + constructor(web3Wrapper: Web3Wrapper) { + this.web3Wrapper = web3Wrapper; + } + // this.exchange = await this.instantiateContractIfExistsAsync(ExchangeArtifacts); + protected async instantiateContractIfExistsAsync(artifact: Artifact, address?: string): Promise { + const c = await contract(artifact); + const providerObj = this.web3Wrapper.getCurrentProvider(); + c.setProvider(providerObj); + + const networkId = await this.web3Wrapper.getNetworkIdIfExistsAsync(); + const artifactNetworkConfigs = _.isUndefined(networkId) ? undefined : artifact.networks[networkId]; + let contractAddress; + if (!_.isUndefined(address)) { + contractAddress = address; + } else if (!_.isUndefined(artifactNetworkConfigs)) { + contractAddress = artifactNetworkConfigs.address; + } + + if (!_.isUndefined(contractAddress)) { + const doesContractExist = await this.web3Wrapper.doesContractExistAtAddressAsync(contractAddress); + if (!doesContractExist) { + throw new Error(ZeroExError.CONTRACT_DOES_NOT_EXIST); + } + } + + try { + let contractInstance; + if (_.isUndefined(address)) { + contractInstance = await c.deployed(); + } else { + contractInstance = await c.at(address); + } + return contractInstance; + } catch (err) { + const errMsg = `${err}`; + utils.consoleLog(`Notice: Error encountered: ${err} ${err.stack}`); + if (_.includes(errMsg, 'not been deployed to detected network')) { + throw new Error(ZeroExError.CONTRACT_DOES_NOT_EXIST); + } else { + throw new Error(ZeroExError.UNHANDLED_ERROR); + } + } + } +} diff --git a/src/ts/contract_wrappers/exchange_wrapper.ts b/src/ts/contract_wrappers/exchange_wrapper.ts new file mode 100644 index 000000000..502a1089b --- /dev/null +++ b/src/ts/contract_wrappers/exchange_wrapper.ts @@ -0,0 +1,37 @@ +import * as _ from 'lodash'; +import {Web3Wrapper} from '../web3_wrapper'; +import {ECSignature, ZeroExError, ExchangeContract} from '../types'; +import {assert} from '../utils/assert'; +import {ContractWrapper} from './contract_wrapper'; +import * as ExchangeArtifacts from '../artifacts/Exchange.json'; +import {ECSignatureSchema} from '../schemas/ec_signature_schema'; + +export class ExchangeWrapper extends ContractWrapper { + constructor(web3Wrapper: Web3Wrapper) { + super(web3Wrapper); + } + public async isValidSignatureAsync(maker: string, ecSignature: ECSignature, dataHex: string) { + assert.isString('maker', maker); + assert.doesConformToSchema('ecSignature', ecSignature, ECSignatureSchema); + assert.isHexString('dataHex', dataHex); + + const senderAddressIfExists = this.web3Wrapper.getSenderAddressIfExistsAsync(); + assert.assert(!_.isUndefined(senderAddressIfExists), ZeroExError.USER_HAS_NO_ASSOCIATED_ADDRESSES); + + // TODO: remove any here + const contractInstance = await this.instantiateContractIfExistsAsync((ExchangeArtifacts as any)); + const exchangeInstance = contractInstance as ExchangeContract; + + const isValidSignature = await exchangeInstance.isValidSignature.call( + maker, + dataHex, + ecSignature.v, + ecSignature.r, + ecSignature.s, + { + from: senderAddressIfExists, + }, + ); + return isValidSignature; + } +} diff --git a/src/ts/globals.d.ts b/src/ts/globals.d.ts index 796812c87..04328509a 100644 --- a/src/ts/globals.d.ts +++ b/src/ts/globals.d.ts @@ -15,6 +15,13 @@ declare namespace Chai { } /* tslint:enable */ +declare module '*.json' { + const json: any; + /* tslint:disable */ + export default json; + /* tslint:enable */ +} + declare module 'ethereumjs-util' { const toBuffer: (dataHex: string) => Buffer; const hashPersonalMessage: (msg: Buffer) => Buffer; @@ -23,3 +30,24 @@ declare module 'ethereumjs-util' { const pubToAddress: (pubKey: string) => Buffer; const isValidAddress: (address: string) => boolean; } + +// truffle-contract declarations +declare interface ContractInstance {} +declare interface ContractFactory { + setProvider: (providerObj: any) => void; + deployed: () => ContractInstance; + at: (address: string) => ContractInstance; +} +declare interface Artifact { + networks: {[networkId: number]: any}; +} +declare function contract(artifacts: Artifact): ContractFactory; +declare module 'truffle-contract' { + export = contract; +} + +// es6-promisify declarations +declare function promisify(original: any, settings?: any): ((...arg: any[]) => Promise); +declare module 'es6-promisify' { + export = promisify; +} diff --git a/src/ts/types.ts b/src/ts/types.ts new file mode 100644 index 000000000..01380af02 --- /dev/null +++ b/src/ts/types.ts @@ -0,0 +1,30 @@ +import * as _ from 'lodash'; + +// Utility function to create a K:V from a list of strings +// Adapted from: https://basarat.gitbooks.io/typescript/content/docs/types/literal-types.html +function strEnum(values: string[]): {[key: string]: string} { + return _.reduce(values, (result, key) => { + result[key] = key; + return result; + }, Object.create(null)); +} + +export const ZeroExError = strEnum([ + 'CONTRACT_DOES_NOT_EXIST', + 'UNHANDLED_ERROR', + 'USER_HAS_NO_ASSOCIATED_ADDRESSES', +]); +export type ZeroExError = keyof typeof ZeroExError; + +/** + * Elliptic Curve signature + */ +export interface ECSignature { + v: number; + r: string; + s: string; +} + +export interface ExchangeContract { + isValidSignature: any; +} diff --git a/src/ts/utils/assert.ts b/src/ts/utils/assert.ts index 2f52c6a3b..15d3031ff 100644 --- a/src/ts/utils/assert.ts +++ b/src/ts/utils/assert.ts @@ -1,30 +1,33 @@ import * as _ from 'lodash'; import * as BigNumber from 'bignumber.js'; -import Web3 = require('web3'); +import Web3 from 'web3'; import {SchemaValidator} from './schema_validator'; const HEX_REGEX = /^0x[0-9A-F]*$/i; export const assert = { - isBigNumber(variableName: string, value: BigNumber.BigNumber) { + isBigNumber(variableName: string, value: BigNumber.BigNumber): void { const isBigNumber = _.isObject(value) && value.isBigNumber; this.assert(isBigNumber, this.typeAssertionMessage(variableName, 'BigNumber', value)); }, - isString(variableName: string, value: string) { + isUndefined(value: any, variableName?: string): void { + this.assert(_.isUndefined(value), this.typeAssertionMessage(variableName, 'undefined', value)); + }, + isString(variableName: string, value: string): void { this.assert(_.isString(value), this.typeAssertionMessage(variableName, 'string', value)); }, - isHexString(variableName: string, value: string) { + isHexString(variableName: string, value: string): void { this.assert(_.isString(value) && HEX_REGEX.test(value), this.typeAssertionMessage(variableName, 'HexString', value)); }, - isETHAddressHex(variableName: string, value: string) { + isETHAddressHex(variableName: string, value: string): void { const web3 = new Web3(); this.assert(web3.isAddress(value), this.typeAssertionMessage(variableName, 'ETHAddressHex', value)); }, - isNumber(variableName: string, value: number) { + isNumber(variableName: string, value: number): void { this.assert(_.isFinite(value), this.typeAssertionMessage(variableName, 'number', value)); }, - doesConformToSchema(variableName: string, value: object, schema: Schema) { + doesConformToSchema(variableName: string, value: object, schema: Schema): void { const schemaValidator = new SchemaValidator(); const validationResult = schemaValidator.validate(value, schema); const hasValidationErrors = validationResult.errors.length > 0; @@ -33,12 +36,12 @@ Encountered: ${JSON.stringify(value, null, '\t')} Validation errors: ${validationResult.errors.join(', ')}`; this.assert(!hasValidationErrors, msg); }, - assert(condition: boolean, message: string) { + assert(condition: boolean, message: string): void { if (!condition) { throw new Error(message); } }, - typeAssertionMessage(variableName: string, type: string, value: any) { + typeAssertionMessage(variableName: string, type: string, value: any): string { return `Expected ${variableName} to be of type ${type}, encountered: ${value}`; }, }; diff --git a/src/ts/utils/utils.ts b/src/ts/utils/utils.ts new file mode 100644 index 000000000..893f82ca3 --- /dev/null +++ b/src/ts/utils/utils.ts @@ -0,0 +1,7 @@ +export const utils = { + consoleLog(message: string) { + /* tslint:disable */ + console.log(message); + /* tslint:enable */ + }, +}; diff --git a/src/ts/web3_wrapper.ts b/src/ts/web3_wrapper.ts new file mode 100644 index 000000000..92781687e --- /dev/null +++ b/src/ts/web3_wrapper.ts @@ -0,0 +1,71 @@ +import * as _ from 'lodash'; +import Web3 from 'web3'; +import * as BigNumber from 'bignumber.js'; +import promisify = require('es6-promisify'); + +export class Web3Wrapper { + private web3: Web3; + constructor(web3: Web3) { + this.web3 = new Web3(); + this.web3.setProvider(web3.currentProvider); + } + public isAddress(address: string): boolean { + return this.web3.isAddress(address); + } + public async getSenderAddressIfExistsAsync(): Promise { + const defaultAccount = this.web3.eth.defaultAccount; + if (!_.isUndefined(defaultAccount)) { + return defaultAccount; + } + const firstAccount = await this.getFirstAddressIfExistsAsync(); + return firstAccount; + } + public async getFirstAddressIfExistsAsync(): Promise { + const addresses = await promisify(this.web3.eth.getAccounts)(); + if (_.isEmpty(addresses)) { + return ''; + } + return (addresses as string[])[0]; + } + public async getNodeVersionAsync(): Promise { + const nodeVersion = await promisify(this.web3.version.getNode)(); + return nodeVersion; + } + public getCurrentProvider(): Web3.Provider { + return this.web3.currentProvider; + } + public async getNetworkIdIfExistsAsync() { + try { + const networkId = await this.getNetworkAsync(); + return Number(networkId); + } catch (err) { + return undefined; + } + } + public async getBalanceInEthAsync(owner: string): Promise { + const balanceInWei = await promisify(this.web3.eth.getBalance)(owner); + const balanceEth = this.web3.fromWei(balanceInWei, 'ether'); + return balanceEth; + } + public async doesContractExistAtAddressAsync(address: string): Promise { + const code = await promisify(this.web3.eth.getCode)(address); + // Regex matches 0x0, 0x00, 0x in order to accomodate poorly implemented clients + const zeroHexAddressRegex = /^0[xX][0]*$/; + const didFindCode = _.isNull(code.match(zeroHexAddressRegex)); + return didFindCode; + } + // Note: since `sign` is overloaded to be both a sync and async method, it doesn't play nice + // with our callAsync method. We therefore handle it here as a special case. + public async signTransactionAsync(address: string, message: string): Promise { + const signData = await promisify(this.web3.eth.sign)(address, message); + return signData; + } + public async getBlockTimestampAsync(blockHash: string): Promise { + const {timestamp} = await promisify(this.web3.eth.getBlock)(blockHash); + return timestamp; + } + private async getNetworkAsync() { + const networkId = await promisify(this.web3.version.getNetwork)(); + return networkId; + } +} -- cgit v1.2.3