diff options
-rw-r--r-- | circle.yml | 2 | ||||
-rw-r--r-- | package.json | 7 | ||||
-rw-r--r-- | src/contract_wrappers/exchange_wrapper.ts | 83 | ||||
-rw-r--r-- | src/schemas/signed_order_schema.ts | 50 | ||||
-rw-r--r-- | src/types.ts | 57 | ||||
-rw-r--r-- | src/utils/assert.ts | 3 | ||||
-rw-r--r-- | src/utils/schema_validator.ts | 9 | ||||
-rw-r--r-- | src/web3_wrapper.ts | 7 | ||||
-rw-r--r-- | test/exchange_wrapper_test.ts (renamed from test/exchange_wrapper.ts) | 10 | ||||
-rw-r--r-- | test/fixtures/orders/5_MKR_for_42_MLN.json | 35 | ||||
-rw-r--r-- | test/utils/blockchain_lifecycle.ts | 2 | ||||
-rw-r--r-- | test/utils/order.ts | 21 |
12 files changed, 271 insertions, 15 deletions
diff --git a/circle.yml b/circle.yml index 448524aa1..4919516f2 100644 --- a/circle.yml +++ b/circle.yml @@ -4,7 +4,7 @@ machine: test: override: - - node node_modules/ethereumjs-testrpc/bin/testrpc -m "concert load couple harbor equip island argue ramp clarify fence smart topic": + - npm run testrpc: background: true - git clone git@github.com:0xProject/contracts.git ../contracts - cd ../contracts; git checkout 38c2b4c; npm install && npm run migrate diff --git a/package.json b/package.json index 75a55b899..c232e2828 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "scripts": { "prebuild": "npm run clean", "build": "run-p build:*:prod", - "lint": "tslint src/**/*.ts", + "lint": "tslint src/**/*.ts test/**/*.ts", "test": "run-s clean test:commonjs", "test:umd": "run-s substitute_umd_bundle run_mocha; npm run clean", "test:coverage": "nyc npm run test --all", @@ -27,11 +27,12 @@ "build:umd:dev": "webpack", "build:umd:prod": "webpack -p", "build:commonjs:dev": "tsc; copyfiles -u 2 ./src/artifacts/*.json ../0x.js/lib/src/artifacts;", - "test:commonjs": "run-s build:commonjs:dev run_mocha", + "test:commonjs": "run-s build:commonjs:dev copy_fixtures run_mocha", "pretest:umd": "run-s clean build:*:dev", "substitute_umd_bundle": "npm run remove_src_files_not_used_by_tests; shx mv _bundles/* lib/src", "remove_src_files_not_used_by_tests": "find ./lib/src \\( -path ./lib/src/utils -o -path ./lib/src/schemas -o -path \"./lib/src/types.*\" \\) -prune -o -type f -print | xargs rm", - "run_mocha": "mocha lib/test/**/*_test.js" + "run_mocha": "mocha lib/test/**/*_test.js", + "copy_fixtures": "shx cp -r ./test/fixtures ./lib/test/fixtures" }, "config": { "artifacts": "Proxy Exchange TokenRegistry Token Mintable EtherToken", diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index 3f6eb0dab..66e53a7d4 100644 --- a/src/contract_wrappers/exchange_wrapper.ts +++ b/src/contract_wrappers/exchange_wrapper.ts @@ -1,12 +1,31 @@ import * as _ from 'lodash'; import {Web3Wrapper} from '../web3_wrapper'; -import {ECSignature, ZeroExError, ExchangeContract} from '../types'; +import { + ECSignature, + ExchangeContract, + ExchangeContractErrs, + OrderValues, + OrderAddresses, + SignedOrder, + ContractEvent, +} 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'; +import {signedOrderSchema} from '../schemas/signed_order_schema'; +import {ContractResponse} from '../types'; +import {constants} from '../utils/constants'; export class ExchangeWrapper extends ContractWrapper { + private exchangeContractErrToMsg = { + [ExchangeContractErrs.ERROR_FILL_EXPIRED]: 'The order you attempted to fill is expired', + [ExchangeContractErrs.ERROR_CANCEL_EXPIRED]: 'The order you attempted to cancel is expired', + [ExchangeContractErrs.ERROR_FILL_NO_VALUE]: 'This order has already been filled or cancelled', + [ExchangeContractErrs.ERROR_CANCEL_NO_VALUE]: 'This order has already been filled or cancelled', + [ExchangeContractErrs.ERROR_FILL_TRUNCATION]: 'The rounding error was too large when filling this order', + [ExchangeContractErrs.ERROR_FILL_BALANCE_ALLOWANCE]: 'Maker or taker has insufficient balance or allowance', + }; private exchangeContractIfExists?: ExchangeContract; constructor(web3Wrapper: Web3Wrapper) { super(web3Wrapper); @@ -20,23 +39,73 @@ export class ExchangeWrapper extends ContractWrapper { assert.doesConformToSchema('ecSignature', ecSignature, ecSignatureSchema); assert.isETHAddressHex('signerAddressHex', signerAddressHex); - const senderAddressIfExists = await this.web3Wrapper.getSenderAddressIfExistsAsync(); - assert.assert(!_.isUndefined(senderAddressIfExists), ZeroExError.USER_HAS_NO_ASSOCIATED_ADDRESSES); + const senderAddress = await this.web3Wrapper.getSenderAddressOrThrowAsync(); + const exchangeInstance = await this.getExchangeContractAsync(); - const exchangeContract = await this.getExchangeContractAsync(); - - const isValidSignature = await exchangeContract.isValidSignature.call( + const isValidSignature = await exchangeInstance.isValidSignature.call( signerAddressHex, dataHex, ecSignature.v, ecSignature.r, ecSignature.s, { - from: senderAddressIfExists, + from: senderAddress, }, ); return isValidSignature; } + public async fillOrderAsync(signedOrder: SignedOrder, fillAmount: BigNumber.BigNumber, + shouldCheckTransfer: boolean = true): Promise<ContractResponse> { + assert.doesConformToSchema('signedOrder', JSON.parse(JSON.stringify(signedOrder)), signedOrderSchema); + assert.isBoolean('shouldCheckTransfer', shouldCheckTransfer); + + const senderAddress = await this.web3Wrapper.getSenderAddressOrThrowAsync(); + const exchangeInstance = await this.getExchangeInstanceOrThrowAsync(); + + const taker = _.isUndefined(signedOrder.taker) ? constants.NULL_ADDRESS : signedOrder.taker; + + const orderAddresses: OrderAddresses = [ + signedOrder.maker, + taker, + signedOrder.makerTokenAddress, + signedOrder.takerTokenAddress, + signedOrder.feeRecipient, + ]; + const orderValues: OrderValues = [ + signedOrder.makerTokenAmount, + signedOrder.takerTokenAmount, + signedOrder.makerFee, + signedOrder.takerFee, + signedOrder.expirationUnixTimestampSec, + signedOrder.salt, + ]; + const response: ContractResponse = await exchangeInstance.fill( + orderAddresses, + orderValues, + fillAmount, + shouldCheckTransfer, + signedOrder.ecSignature.v, + signedOrder.ecSignature.r, + signedOrder.ecSignature.s, + { + from: senderAddress, + }, + ); + this.throwErrorLogsAsErrors(response.logs); + return response; + } + private async getExchangeInstanceOrThrowAsync(): Promise<ExchangeContract> { + const contractInstance = await this.instantiateContractIfExistsAsync((ExchangeArtifacts as any)); + return contractInstance as ExchangeContract; + } + private throwErrorLogsAsErrors(logs: ContractEvent[]): void { + const errEvent = _.find(logs, {event: 'LogError'}); + if (!_.isUndefined(errEvent)) { + const errCode = errEvent.args.errorId.toNumber(); + const humanReadableErrMessage = this.exchangeContractErrToMsg[errCode]; + throw new Error(humanReadableErrMessage); + } + } private async getExchangeContractAsync(): Promise<ExchangeContract> { if (!_.isUndefined(this.exchangeContractIfExists)) { return this.exchangeContractIfExists; diff --git a/src/schemas/signed_order_schema.ts b/src/schemas/signed_order_schema.ts new file mode 100644 index 000000000..dc7e51e40 --- /dev/null +++ b/src/schemas/signed_order_schema.ts @@ -0,0 +1,50 @@ +export const addressSchema = { + id: '/addressSchema', + type: 'string', + pattern: '^0[xX][0-9A-Fa-f]{40}$', +}; + +export const numberSchema = { + id: '/numberSchema', + type: 'string', + format: '\d+(\.\d+)?', +}; + +export const orderSchema = { + id: '/orderSchema', + properties: { + maker: {$ref: '/addressSchema'}, + taker: {$ref: '/addressSchema'}, + + makerFee: {$ref: '/numberSchema'}, + takerFee: {$ref: '/numberSchema'}, + + makerTokenAmount: {$ref: '/numberSchema'}, + takerTokenAmount: {$ref: '/numberSchema'}, + + makerTokenAddress: {$ref: '/addressSchema'}, + takerTokenAddress: {$ref: '/addressSchema'}, + + salt: {$ref: '/numberSchema'}, + feeRecipient: {$ref: '/addressSchema'}, + expirationUnixTimestampSec: {$ref: '/numberSchema'}, + }, + required: [ + 'maker', /*'taker',*/ 'makerFee', 'takerFee', 'makerTokenAmount', 'takerTokenAmount', + 'salt', 'feeRecipient', 'expirationUnixTimestampSec', + ], + type: 'object', +}; + +export const signedOrderSchema = { + id: '/signedOrderSchema', + allOf: [ + { $ref: '/orderSchema' }, + { + properties: { + ecSignature: {$ref: '/ECSignature'}, + }, + required: ['ecSignature'], + }, + ], +}; diff --git a/src/types.ts b/src/types.ts index 6fce95706..2ada20e3c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,8 +26,23 @@ export interface ECSignature { s: string; } +export type OrderAddresses = [string, string, string, string, string]; + +export type OrderValues = [ + BigNumber.BigNumber, BigNumber.BigNumber, BigNumber.BigNumber, + BigNumber.BigNumber, BigNumber.BigNumber, BigNumber.BigNumber +]; + +export interface TxData { + from: string; +} + export interface ExchangeContract { isValidSignature: any; + fill: ( + orderAddresses: OrderAddresses, orderValues: OrderValues, fillAmount: BigNumber.BigNumber, + shouldCheckTransfer: boolean, v: number, r: string, s: string, txData: TxData, + ) => ContractResponse; } export interface TokenRegistryContract { @@ -45,6 +60,46 @@ export const SolidityTypes = strEnum([ ]); export type SolidityTypes = keyof typeof SolidityTypes; +export enum ExchangeContractErrs { + ERROR_FILL_EXPIRED, // Order has already expired + ERROR_FILL_NO_VALUE, // Order has already been fully filled or cancelled + ERROR_FILL_TRUNCATION, // Rounding error too large + ERROR_FILL_BALANCE_ALLOWANCE, // Insufficient balance or allowance for token transfer + ERROR_CANCEL_EXPIRED, // Order has already expired + ERROR_CANCEL_NO_VALUE, // Order has already been fully filled or cancelled +}; + +export interface ContractResponse { + logs: ContractEvent[]; +} + +export interface ContractEvent { + event: string; + args: any; +} + +export interface Order { + maker: string; + taker?: string; + + makerFee: BigNumber.BigNumber; + takerFee: BigNumber.BigNumber; + + makerTokenAmount: BigNumber.BigNumber; + takerTokenAmount: BigNumber.BigNumber; + + makerTokenAddress: string; + takerTokenAddress: string; + + salt: BigNumber.BigNumber; + feeRecipient: string; + expirationUnixTimestampSec: BigNumber.BigNumber; +} + +export interface SignedOrder extends Order { + ecSignature: ECSignature; +} + // [address, name, symbol, projectUrl, decimals, ipfsHash, swarmHash] export type TokenMetadata = [string, string, string, string, BigNumber.BigNumber, string, string]; @@ -54,4 +109,4 @@ export interface Token { symbol: string; decimals: number; url: string; -}; +} diff --git a/src/utils/assert.ts b/src/utils/assert.ts index 1baf572d1..aeed1c6dc 100644 --- a/src/utils/assert.ts +++ b/src/utils/assert.ts @@ -27,6 +27,9 @@ export const assert = { isNumber(variableName: string, value: number): void { this.assert(_.isFinite(value), this.typeAssertionMessage(variableName, 'number', value)); }, + isBoolean(variableName: string, value: boolean): void { + this.assert(_.isBoolean(value), this.typeAssertionMessage(variableName, 'boolean', value)); + }, doesConformToSchema(variableName: string, value: object, schema: Schema): void { const schemaValidator = new SchemaValidator(); const validationResult = schemaValidator.validate(value, schema); diff --git a/src/utils/schema_validator.ts b/src/utils/schema_validator.ts index 8132f7414..cf45d0343 100644 --- a/src/utils/schema_validator.ts +++ b/src/utils/schema_validator.ts @@ -1,14 +1,19 @@ import {Validator, ValidatorResult} from 'jsonschema'; import {ecSignatureSchema, ecSignatureParameter} from '../schemas/ec_signature_schema'; +import {addressSchema, numberSchema, orderSchema, signedOrderSchema} from '../schemas/signed_order_schema'; import {tokenSchema} from '../schemas/token_schema'; export class SchemaValidator { private validator: Validator; constructor() { this.validator = new Validator(); - this.validator.addSchema(ecSignatureParameter, ecSignatureParameter.id); - this.validator.addSchema(ecSignatureSchema, ecSignatureSchema.id); this.validator.addSchema(tokenSchema, tokenSchema.id); + this.validator.addSchema(orderSchema, orderSchema.id); + this.validator.addSchema(numberSchema, numberSchema.id); + this.validator.addSchema(addressSchema, addressSchema.id); + this.validator.addSchema(ecSignatureSchema, ecSignatureSchema.id); + this.validator.addSchema(signedOrderSchema, signedOrderSchema.id); + this.validator.addSchema(ecSignatureParameter, ecSignatureParameter.id); } public validate(instance: object, schema: Schema): ValidatorResult { return this.validator.validate(instance, schema); diff --git a/src/web3_wrapper.ts b/src/web3_wrapper.ts index e65f29b56..361f28476 100644 --- a/src/web3_wrapper.ts +++ b/src/web3_wrapper.ts @@ -2,6 +2,8 @@ import * as _ from 'lodash'; import * as Web3 from 'web3'; import * as BigNumber from 'bignumber.js'; import promisify = require('es6-promisify'); +import {ZeroExError} from "./types"; +import {assert} from "./utils/assert"; export class Web3Wrapper { private web3: Web3; @@ -23,6 +25,11 @@ export class Web3Wrapper { const firstAccount = await this.getFirstAddressIfExistsAsync(); return firstAccount; } + public async getSenderAddressOrThrowAsync(): Promise<string> { + const senderAddressIfExists = await this.getSenderAddressIfExistsAsync(); + assert.assert(!_.isUndefined(senderAddressIfExists), ZeroExError.USER_HAS_NO_ASSOCIATED_ADDRESSES); + return senderAddressIfExists as string; + } public async getFirstAddressIfExistsAsync(): Promise<string|undefined> { const addresses = await promisify(this.web3.eth.getAccounts)(); if (_.isEmpty(addresses)) { diff --git a/test/exchange_wrapper.ts b/test/exchange_wrapper_test.ts index 55b84ce36..b97a62100 100644 --- a/test/exchange_wrapper.ts +++ b/test/exchange_wrapper_test.ts @@ -4,6 +4,9 @@ import chaiAsPromised = require('chai-as-promised'); import {web3Factory} from './utils/web3_factory'; import {ZeroEx} from '../src/0x.js'; import {BlockchainLifecycle} from './utils/blockchain_lifecycle'; +import * as OrderJSON from './fixtures/orders/5_MKR_for_42_MLN.json'; +import * as BigNumber from 'bignumber.js'; +import {signedOrderFromJSON} from './utils/order'; const expect = chai.expect; chai.use(chaiAsPromised); @@ -91,4 +94,11 @@ describe('ExchangeWrapper', () => { expect(isValid).to.be.true; }); }); + describe('#fillOrderAsync', () => { + const fillAmount = new BigNumber(1); + const signedOrder = signedOrderFromJSON(OrderJSON); + it('fillsOrder', async () => { + // const orderFillResponse = await zeroEx.exchange.fillOrderAsync(signedOrder, fillAmount); + }); + }); }); diff --git a/test/fixtures/orders/5_MKR_for_42_MLN.json b/test/fixtures/orders/5_MKR_for_42_MLN.json new file mode 100644 index 000000000..2c2a3d73e --- /dev/null +++ b/test/fixtures/orders/5_MKR_for_42_MLN.json @@ -0,0 +1,35 @@ +{ + "maker": { + "address": "0xffa119a5761eb93eacfe5d2b0e8944648c3d7164", + "token": { + "name": "MakerDAO", + "symbol": "MKR", + "decimals": 18, + "address": "0x1dad4783cf3fe3085c1426157ab175a6119a04ba" + }, + "amount": "5000000000000000000", + "feeAmount": "0" + }, + "taker": { + "address": "", + "token": { + "name": "Melon Token", + "symbol": "MLN", + "decimals": 18, + "address": "0x323b5d4c32345ced77393b3530b1eed0f346429d" + }, + "amount": "42000000000000000000", + "feeAmount": "0" + }, + "expiration": "1496181600", + "feeRecipient": "0x0000000000000000000000000000000000000000", + "salt": "28894038927316056783595066163529969528517282468995826064292810658599101267743", + "signature": { + "v": 28, + "r": "0x1e84173b09bc51e4e3f923718d747bd91c3584cfa3556d79294891ddd740e819", + "s": "0x36c0f453a01487f49e16157703cacb6cee08d337549f805c25d3f78221823eed", + "hash": "0x3f64711f39393f7f60a0980e3effba087c15bb9b1cbf678ede63a69798e5dc14" + }, + "exchangeContract": "0x9ce1a5e2311f9b8b8e6b40ed20b5b090de4a4c4d", + "networkId": 42 +}
\ No newline at end of file diff --git a/test/utils/blockchain_lifecycle.ts b/test/utils/blockchain_lifecycle.ts index 68e169ac0..50eb57b95 100644 --- a/test/utils/blockchain_lifecycle.ts +++ b/test/utils/blockchain_lifecycle.ts @@ -17,4 +17,4 @@ export class BlockchainLifecycle { throw new Error(`Snapshot with id #${this.snapshotId} failed to revert`); } } -}; +} diff --git a/test/utils/order.ts b/test/utils/order.ts new file mode 100644 index 000000000..dc0be670c --- /dev/null +++ b/test/utils/order.ts @@ -0,0 +1,21 @@ +import {SignedOrder} from '../../lib/src/types'; +import * as BigNumber from 'bignumber.js'; +import * as _ from 'lodash'; + +export function signedOrderFromJSON(signedOrderJSON: any): SignedOrder { + const signedOrder = { + maker: signedOrderJSON.maker.address, + taker: _.isEmpty(signedOrderJSON.taker.address) ? undefined : signedOrderJSON.taker.address, + makerTokenAddress: signedOrderJSON.maker.token.address, + takerTokenAddress: signedOrderJSON.taker.token.address, + makerTokenAmount: new BigNumber(signedOrderJSON.maker.amount), + takerTokenAmount: new BigNumber(signedOrderJSON.taker.amount), + makerFee: new BigNumber(signedOrderJSON.maker.feeAmount), + takerFee: new BigNumber(signedOrderJSON.taker.feeAmount), + expirationUnixTimestampSec: new BigNumber(signedOrderJSON.expiration), + feeRecipient: signedOrderJSON.feeRecipient, + ecSignature: signedOrderJSON.signature, + salt: new BigNumber(signedOrderJSON.salt), + }; + return signedOrder; +} |