diff options
-rw-r--r-- | circle.yml | 2 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | src/0x.js.ts | 45 | ||||
-rw-r--r-- | src/contract_wrappers/exchange_wrapper.ts | 84 | ||||
-rw-r--r-- | src/schemas/signed_order_schema.ts | 50 | ||||
-rw-r--r-- | src/types.ts | 53 | ||||
-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/0x.js_test.ts | 47 | ||||
-rw-r--r-- | test/exchange_wrapper_test.ts (renamed from test/exchange_wrapper.ts) | 10 | ||||
-rw-r--r-- | test/utils/blockchain_lifecycle.ts | 2 | ||||
-rw-r--r-- | test/utils/order.ts | 28 |
13 files changed, 272 insertions, 70 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..59e33a7dd 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", diff --git a/src/0x.js.ts b/src/0x.js.ts index d231c579e..97eb4d126 100644 --- a/src/0x.js.ts +++ b/src/0x.js.ts @@ -15,6 +15,8 @@ import {TokenRegistryWrapper} from './contract_wrappers/token_registry_wrapper'; import {ecSignatureSchema} from './schemas/ec_signature_schema'; import {TokenWrapper} from './contract_wrappers/token_wrapper'; import {SolidityTypes, ECSignature, ZeroExError} from './types'; +import {Order} from './types'; +import {orderSchema} from "./schemas/signed_order_schema"; const MAX_DIGITS_IN_UNSIGNED_256_INT = 78; @@ -26,38 +28,23 @@ export class ZeroEx { /** * Computes the orderHash given the order parameters and returns it as a hex encoded string. */ - public static getOrderHashHex(exchangeContractAddr: string, makerAddr: string, takerAddr: string, - tokenMAddress: string, tokenTAddress: string, feeRecipient: string, - valueM: BigNumber.BigNumber, valueT: BigNumber.BigNumber, - makerFee: BigNumber.BigNumber, takerFee: BigNumber.BigNumber, - expiration: BigNumber.BigNumber, salt: BigNumber.BigNumber): string { - takerAddr = _.isEmpty(takerAddr) ? constants.NULL_ADDRESS : takerAddr ; - assert.isETHAddressHex('exchangeContractAddr', exchangeContractAddr); - assert.isETHAddressHex('makerAddr', makerAddr); - assert.isETHAddressHex('takerAddr', takerAddr); - assert.isETHAddressHex('tokenMAddress', tokenMAddress); - assert.isETHAddressHex('tokenTAddress', tokenTAddress); - assert.isETHAddressHex('feeRecipient', feeRecipient); - assert.isBigNumber('valueM', valueM); - assert.isBigNumber('valueT', valueT); - assert.isBigNumber('makerFee', makerFee); - assert.isBigNumber('takerFee', takerFee); - assert.isBigNumber('expiration', expiration); - assert.isBigNumber('salt', salt); + public static getOrderHashHex(exchangeContractAddr: string, order: Order): string { + assert.doesConformToSchema('order', JSON.parse(JSON.stringify(order)), orderSchema); + const taker = _.isEmpty(order.taker) ? constants.NULL_ADDRESS : order.taker ; const orderParts = [ {value: exchangeContractAddr, type: SolidityTypes.address}, - {value: makerAddr, type: SolidityTypes.address}, - {value: takerAddr, type: SolidityTypes.address}, - {value: tokenMAddress, type: SolidityTypes.address}, - {value: tokenTAddress, type: SolidityTypes.address}, - {value: feeRecipient, type: SolidityTypes.address}, - {value: utils.bigNumberToBN(valueM), type: SolidityTypes.uint256}, - {value: utils.bigNumberToBN(valueT), type: SolidityTypes.uint256}, - {value: utils.bigNumberToBN(makerFee), type: SolidityTypes.uint256}, - {value: utils.bigNumberToBN(takerFee), type: SolidityTypes.uint256}, - {value: utils.bigNumberToBN(expiration), type: SolidityTypes.uint256}, - {value: utils.bigNumberToBN(salt), type: SolidityTypes.uint256}, + {value: order.maker, type: SolidityTypes.address}, + {value: order.taker, type: SolidityTypes.address}, + {value: order.makerTokenAddress, type: SolidityTypes.address}, + {value: order.takerTokenAddress, type: SolidityTypes.address}, + {value: order.feeRecipient, type: SolidityTypes.address}, + {value: utils.bigNumberToBN(order.makerTokenAmount), type: SolidityTypes.uint256}, + {value: utils.bigNumberToBN(order.takerTokenAmount), type: SolidityTypes.uint256}, + {value: utils.bigNumberToBN(order.makerFee), type: SolidityTypes.uint256}, + {value: utils.bigNumberToBN(order.takerFee), type: SolidityTypes.uint256}, + {value: utils.bigNumberToBN(order.expirationUnixTimestampSec), type: SolidityTypes.uint256}, + {value: utils.bigNumberToBN(order.salt), type: SolidityTypes.uint256}, ]; const types = _.map(orderParts, o => o.type); const values = _.map(orderParts, o => o.value); diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts index 3f6eb0dab..0749a7e80 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,74 @@ 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.isBigNumber('fillAmount', fillAmount); + 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 418cf9802..861df3187 100644 --- a/src/types.ts +++ b/src/types.ts @@ -31,8 +31,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 TokenContract { @@ -61,6 +76,42 @@ 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]; @@ -70,4 +121,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/0x.js_test.ts b/test/0x.js_test.ts index 5d23d7094..a84785f4b 100644 --- a/test/0x.js_test.ts +++ b/test/0x.js_test.ts @@ -7,6 +7,7 @@ import * as Sinon from 'sinon'; import {ZeroEx} from '../src/0x.js'; import {constants} from './utils/constants'; import {web3Factory} from './utils/web3_factory'; +import {Order} from '../src/types'; // Use BigNumber chai add-on chai.use(ChaiBigNumber()); @@ -43,38 +44,28 @@ describe('ZeroEx library', () => { }); describe('#getOrderHash', () => { const expectedOrderHash = '0x103a5e97dab5dbeb8f385636f86a7d1e458a7ccbe1bd194727f0b2f85ab116c7'; + const order: Order = { + maker: constants.NULL_ADDRESS, + feeRecipient: constants.NULL_ADDRESS, + makerTokenAddress: constants.NULL_ADDRESS, + takerTokenAddress: constants.NULL_ADDRESS, + salt: new BigNumber(0), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + makerTokenAmount: new BigNumber(0), + takerTokenAmount: new BigNumber(0), + expirationUnixTimestampSec: new BigNumber(0), + }; + const exchangeAddress = constants.NULL_ADDRESS; it('defaults takerAddress to NULL address', () => { - const orderHash = ZeroEx.getOrderHashHex( - constants.NULL_ADDRESS, - constants.NULL_ADDRESS, - '', - constants.NULL_ADDRESS, - constants.NULL_ADDRESS, - constants.NULL_ADDRESS, - new BigNumber(0), - new BigNumber(0), - new BigNumber(0), - new BigNumber(0), - new BigNumber(0), - new BigNumber(0), - ); + const orderHash = ZeroEx.getOrderHashHex(exchangeAddress, order); expect(orderHash).to.be.equal(expectedOrderHash); }); it('calculates the order hash', () => { - const orderHash = ZeroEx.getOrderHashHex( - constants.NULL_ADDRESS, - constants.NULL_ADDRESS, - constants.NULL_ADDRESS, - constants.NULL_ADDRESS, - constants.NULL_ADDRESS, - constants.NULL_ADDRESS, - new BigNumber(0), - new BigNumber(0), - new BigNumber(0), - new BigNumber(0), - new BigNumber(0), - new BigNumber(0), - ); + const orderWithZeroTaker = _.assign(order, { + taker: constants.NULL_ADDRESS, + }); + const orderHash = ZeroEx.getOrderHashHex(exchangeAddress, orderWithZeroTaker); expect(orderHash).to.be.equal(expectedOrderHash); }); }); diff --git a/test/exchange_wrapper.ts b/test/exchange_wrapper_test.ts index e42454089..ede4198d8 100644 --- a/test/exchange_wrapper.ts +++ b/test/exchange_wrapper_test.ts @@ -3,6 +3,8 @@ import * as chai from 'chai'; import {web3Factory} from './utils/web3_factory'; import {ZeroEx} from '../src/0x.js'; import {BlockchainLifecycle} from './utils/blockchain_lifecycle'; +import * as BigNumber from 'bignumber.js'; +import {createSignedOrder} from './utils/order'; const expect = chai.expect; const blockchainLifecycle = new BlockchainLifecycle(); @@ -89,4 +91,12 @@ describe('ExchangeWrapper', () => { expect(isValid).to.be.true; }); }); + describe('#fillOrderAsync', async () => { + const signedOrder = await createSignedOrder(zeroEx); + it('should throw when the fill amount is zero', async () => { + const fillAmount = new BigNumber(0); + expect(zeroEx.exchange.fillOrderAsync(signedOrder, fillAmount)) + .to.be.rejectedWith('This order has already been filled or cancelled'); + }); + }); }); 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..a69c1b62f --- /dev/null +++ b/test/utils/order.ts @@ -0,0 +1,28 @@ +import {SignedOrder} from '../../src/types'; +import * as BigNumber from 'bignumber.js'; +import * as _ from 'lodash'; +import {ZeroEx} from '../../src/0x.js'; +import {constants} from './constants'; + +export async function createSignedOrder(zeroEx: ZeroEx): Promise<SignedOrder> { + // TODO: fetch properly + const EXCHANGE_ADDRESS = '0xb69e673309512a9d726f87304c6984054f87a93b'; + const INF_TIMESTAMP = 2524604400; + const order = { + maker: '0x5409ed021d9299bf6814279a6a1411a7e866a631', + taker: undefined, + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + makerTokenAmount: new BigNumber(5000000000000000000), + takerTokenAmount: new BigNumber(42000000000000000000), + makerTokenAddress: '0x07f96aa816c1f244cbc6ef114bb2b023ba54a2eb', + takerTokenAddress: '0x1e2f9e10d02a6b8f8f69fcbf515e75039d2ea30d', + salt: ZeroEx.generatePseudoRandomSalt(), + feeRecipient: constants.NULL_ADDRESS, + expirationUnixTimestampSec: new BigNumber(INF_TIMESTAMP), + }; + const orderHash = ZeroEx.getOrderHashHex(EXCHANGE_ADDRESS, order); + const ecSignature = await zeroEx.signOrderHashAsync(orderHash); + const signedOrder: SignedOrder = _.assign(order, {ecSignature}); + return signedOrder; +} |