Add initial exchange contract function, set up web3Wrapper, added types and utils

This commit is contained in:
Fabio Berger 2017-05-25 19:47:11 +02:00
parent 74e7dc46b4
commit bc8fc53433
9 changed files with 252 additions and 19 deletions

View File

@ -52,14 +52,16 @@
"tslint-config-0xproject": "^0.0.2", "tslint-config-0xproject": "^0.0.2",
"typedoc": "^0.7.1", "typedoc": "^0.7.1",
"typescript": "^2.3.3", "typescript": "^2.3.3",
"web3-typescript-typings": "0.0.3", "web3-typescript-typings": "0.0.7",
"webpack": "^2.6.0" "webpack": "^2.6.0"
}, },
"dependencies": { "dependencies": {
"bignumber.js": "^4.0.2", "bignumber.js": "^4.0.2",
"es6-promisify": "^5.0.0",
"ethereumjs-util": "^5.1.1", "ethereumjs-util": "^5.1.1",
"jsonschema": "^1.1.1", "jsonschema": "^1.1.1",
"lodash": "^4.17.4", "lodash": "^4.17.4",
"truffle-contract": "^2.0.0",
"web3": "^0.19.0" "web3": "^0.19.0"
} }
} }

View File

@ -1,21 +1,21 @@
import * as BigNumber from 'bignumber.js'; import * as BigNumber from 'bignumber.js';
import * as ethUtil from 'ethereumjs-util'; import * as ethUtil from 'ethereumjs-util';
import * as _ from 'lodash'; import * as _ from 'lodash';
import Web3 from 'web3';
import {assert} from './utils/assert'; 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'; import {ECSignatureSchema} from './schemas/ec_signature_schema';
import {ECSignature} from './types';
/**
* Elliptic Curve signature
*/
export interface ECSignature {
v: number;
r: string;
s: string;
}
const MAX_DIGITS_IN_UNSIGNED_256_INT = 78; const MAX_DIGITS_IN_UNSIGNED_256_INT = 78;
export class ZeroEx { export class ZeroEx {
public web3Wrapper: Web3Wrapper;
public exchange: ContractInstance;
/** /**
* Verifies that the elliptic curve signature `signature` was generated * Verifies that the elliptic curve signature `signature` was generated
* by signing `data` with the private key corresponding to the `signerAddressHex` address. * by signing `data` with the private key corresponding to the `signerAddressHex` address.
@ -83,4 +83,8 @@ export class ZeroEx {
const baseUnitAmount = amount.times(unit); const baseUnitAmount = amount.times(unit);
return baseUnitAmount; return baseUnitAmount;
} }
constructor(web3: Web3) {
this.web3Wrapper = new Web3Wrapper(web3);
this.exchange = new ExchangeWrapper(this.web3Wrapper);
}
} }

View File

@ -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<ContractInstance> {
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);
}
}
}
}

View File

@ -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;
}
}

28
src/ts/globals.d.ts vendored
View File

@ -15,6 +15,13 @@ declare namespace Chai {
} }
/* tslint:enable */ /* tslint:enable */
declare module '*.json' {
const json: any;
/* tslint:disable */
export default json;
/* tslint:enable */
}
declare module 'ethereumjs-util' { declare module 'ethereumjs-util' {
const toBuffer: (dataHex: string) => Buffer; const toBuffer: (dataHex: string) => Buffer;
const hashPersonalMessage: (msg: Buffer) => Buffer; const hashPersonalMessage: (msg: Buffer) => Buffer;
@ -23,3 +30,24 @@ declare module 'ethereumjs-util' {
const pubToAddress: (pubKey: string) => Buffer; const pubToAddress: (pubKey: string) => Buffer;
const isValidAddress: (address: string) => boolean; 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<any>);
declare module 'es6-promisify' {
export = promisify;
}

30
src/ts/types.ts Normal file
View File

@ -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;
}

View File

@ -1,30 +1,33 @@
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as BigNumber from 'bignumber.js'; import * as BigNumber from 'bignumber.js';
import Web3 = require('web3'); import Web3 from 'web3';
import {SchemaValidator} from './schema_validator'; import {SchemaValidator} from './schema_validator';
const HEX_REGEX = /^0x[0-9A-F]*$/i; const HEX_REGEX = /^0x[0-9A-F]*$/i;
export const assert = { export const assert = {
isBigNumber(variableName: string, value: BigNumber.BigNumber) { isBigNumber(variableName: string, value: BigNumber.BigNumber): void {
const isBigNumber = _.isObject(value) && value.isBigNumber; const isBigNumber = _.isObject(value) && value.isBigNumber;
this.assert(isBigNumber, this.typeAssertionMessage(variableName, 'BigNumber', value)); 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)); 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.assert(_.isString(value) && HEX_REGEX.test(value),
this.typeAssertionMessage(variableName, 'HexString', value)); this.typeAssertionMessage(variableName, 'HexString', value));
}, },
isETHAddressHex(variableName: string, value: string) { isETHAddressHex(variableName: string, value: string): void {
const web3 = new Web3(); const web3 = new Web3();
this.assert(web3.isAddress(value), this.typeAssertionMessage(variableName, 'ETHAddressHex', value)); 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)); 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 schemaValidator = new SchemaValidator();
const validationResult = schemaValidator.validate(value, schema); const validationResult = schemaValidator.validate(value, schema);
const hasValidationErrors = validationResult.errors.length > 0; const hasValidationErrors = validationResult.errors.length > 0;
@ -33,12 +36,12 @@ Encountered: ${JSON.stringify(value, null, '\t')}
Validation errors: ${validationResult.errors.join(', ')}`; Validation errors: ${validationResult.errors.join(', ')}`;
this.assert(!hasValidationErrors, msg); this.assert(!hasValidationErrors, msg);
}, },
assert(condition: boolean, message: string) { assert(condition: boolean, message: string): void {
if (!condition) { if (!condition) {
throw new Error(message); 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}`; return `Expected ${variableName} to be of type ${type}, encountered: ${value}`;
}, },
}; };

7
src/ts/utils/utils.ts Normal file
View File

@ -0,0 +1,7 @@
export const utils = {
consoleLog(message: string) {
/* tslint:disable */
console.log(message);
/* tslint:enable */
},
};

71
src/ts/web3_wrapper.ts Normal file
View File

@ -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<string> {
const defaultAccount = this.web3.eth.defaultAccount;
if (!_.isUndefined(defaultAccount)) {
return defaultAccount;
}
const firstAccount = await this.getFirstAddressIfExistsAsync();
return firstAccount;
}
public async getFirstAddressIfExistsAsync(): Promise<string> {
const addresses = await promisify(this.web3.eth.getAccounts)();
if (_.isEmpty(addresses)) {
return '';
}
return (addresses as string[])[0];
}
public async getNodeVersionAsync(): Promise<string> {
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<BigNumber.BigNumber> {
const balanceInWei = await promisify(this.web3.eth.getBalance)(owner);
const balanceEth = this.web3.fromWei(balanceInWei, 'ether');
return balanceEth;
}
public async doesContractExistAtAddressAsync(address: string): Promise<boolean> {
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<string> {
const signData = await promisify(this.web3.eth.sign)(address, message);
return signData;
}
public async getBlockTimestampAsync(blockHash: string): Promise<number> {
const {timestamp} = await promisify(this.web3.eth.getBlock)(blockHash);
return timestamp;
}
private async getNetworkAsync() {
const networkId = await promisify(this.web3.version.getNetwork)();
return networkId;
}
}