aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/0x.js.ts125
-rw-r--r--src/artifacts/ProtocolToken.json321
-rw-r--r--src/bignumber_config.ts11
-rw-r--r--src/contract_wrappers/exchange_wrapper.ts194
-rw-r--r--src/globals.d.ts1
-rw-r--r--src/schemas/order_schemas.ts50
-rw-r--r--src/types.ts78
-rw-r--r--src/utils/assert.ts3
-rw-r--r--src/utils/schema_validator.ts16
-rw-r--r--src/web3_wrapper.ts27
10 files changed, 756 insertions, 70 deletions
diff --git a/src/0x.js.ts b/src/0x.js.ts
index d231c579e..679d748e7 100644
--- a/src/0x.js.ts
+++ b/src/0x.js.ts
@@ -1,71 +1,39 @@
import * as _ from 'lodash';
import * as BigNumber from 'bignumber.js';
+import {bigNumberConfigs} from './bignumber_config';
import * as ethUtil from 'ethereumjs-util';
import contract = require('truffle-contract');
import * as Web3 from 'web3';
import * as ethABI from 'ethereumjs-abi';
+import findVersions = require('find-versions');
+import compareVersions = require('compare-versions');
import {Web3Wrapper} from './web3_wrapper';
import {constants} from './utils/constants';
import {utils} from './utils/utils';
import {assert} from './utils/assert';
-import findVersions = require('find-versions');
-import compareVersions = require('compare-versions');
+import {SchemaValidator} from './utils/schema_validator';
import {ExchangeWrapper} from './contract_wrappers/exchange_wrapper';
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/order_schemas';
+import * as ExchangeArtifacts from './artifacts/Exchange.json';
+
+// Customize our BigNumber instances
+bigNumberConfigs.configure();
const MAX_DIGITS_IN_UNSIGNED_256_INT = 78;
export class ZeroEx {
+ public static NULL_ADDRESS = constants.NULL_ADDRESS;
+
public exchange: ExchangeWrapper;
public tokenRegistry: TokenRegistryWrapper;
public token: TokenWrapper;
private web3Wrapper: Web3Wrapper;
/**
- * 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);
-
- 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},
- ];
- const types = _.map(orderParts, o => o.type);
- const values = _.map(orderParts, o => o.value);
- const hashBuff = ethABI.soliditySHA3(types, values);
- const hashHex = ethUtil.bufferToHex(hashBuff);
- return hashHex;
- }
- /**
* Verifies that the elliptic curve signature `signature` was generated
* by signing `data` with the private key corresponding to the `signerAddressHex` address.
*/
@@ -135,9 +103,9 @@ export class ZeroEx {
}
constructor(web3: Web3) {
this.web3Wrapper = new Web3Wrapper(web3);
- this.exchange = new ExchangeWrapper(this.web3Wrapper);
- this.tokenRegistry = new TokenRegistryWrapper(this.web3Wrapper);
this.token = new TokenWrapper(this.web3Wrapper);
+ this.exchange = new ExchangeWrapper(this.web3Wrapper, this.token);
+ this.tokenRegistry = new TokenRegistryWrapper(this.web3Wrapper);
}
/**
* Sets a new provider for the web3 instance used by 0x.js
@@ -148,6 +116,49 @@ export class ZeroEx {
this.tokenRegistry.invalidateContractInstance();
this.token.invalidateContractInstances();
}
+
+ /**
+ * Sets default account for sending transactions.
+ */
+ public setTransactionSenderAccount(account: string): void {
+ this.web3Wrapper.setDefaultAccount(account);
+ }
+ /**
+ * Get the default account set for sending transactions.
+ */
+ public async getTransactionSenderAccountIfExistsAsync(): Promise<string|undefined> {
+ const senderAccountIfExists = await this.web3Wrapper.getSenderAddressIfExistsAsync();
+ return senderAccountIfExists;
+ }
+ /**
+ * Computes the orderHash given the order parameters and returns it as a hex encoded string.
+ */
+ public async getOrderHashHexAsync(order: Order): Promise<string> {
+ const exchangeContractAddr = await this.getExchangeAddressAsync();
+ assert.doesConformToSchema('order',
+ SchemaValidator.convertToJSONSchemaCompatibleObject(order as object),
+ orderSchema);
+
+ const orderParts = [
+ {value: exchangeContractAddr, type: SolidityTypes.address},
+ {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);
+ const hashBuff = ethABI.soliditySHA3(types, values);
+ const hashHex = ethUtil.bufferToHex(hashBuff);
+ return hashHex;
+ }
/**
* Signs an orderHash and returns it's elliptic curve signature
* This method currently supports TestRPC, Geth and Parity above and below V1.6.6
@@ -155,6 +166,8 @@ export class ZeroEx {
public async signOrderHashAsync(orderHashHex: string): Promise<ECSignature> {
assert.isHexString('orderHashHex', orderHashHex);
+ const makerAddress = await this.web3Wrapper.getSenderAddressOrThrowAsync();
+
let msgHashHex;
const nodeVersion = await this.web3Wrapper.getNodeVersionAsync();
const isParityNode = utils.isParityNode(nodeVersion);
@@ -167,12 +180,7 @@ export class ZeroEx {
msgHashHex = ethUtil.bufferToHex(msgHashBuff);
}
- const makerAddressIfExists = await this.web3Wrapper.getSenderAddressIfExistsAsync();
- if (_.isUndefined(makerAddressIfExists)) {
- throw new Error(ZeroExError.USER_HAS_NO_ASSOCIATED_ADDRESSES);
- }
-
- const signature = await this.web3Wrapper.signTransactionAsync(makerAddressIfExists, msgHashHex);
+ const signature = await this.web3Wrapper.signTransactionAsync(makerAddress, msgHashHex);
let signatureData;
const [nodeVersionNumber] = findVersions(nodeVersion);
@@ -202,10 +210,21 @@ export class ZeroEx {
r: ethUtil.bufferToHex(r),
s: ethUtil.bufferToHex(s),
};
- const isValidSignature = ZeroEx.isValidSignature(orderHashHex, ecSignature, makerAddressIfExists);
+ const isValidSignature = ZeroEx.isValidSignature(orderHashHex, ecSignature, makerAddress);
if (!isValidSignature) {
throw new Error(ZeroExError.INVALID_SIGNATURE);
}
return ecSignature;
}
+ private async getExchangeAddressAsync() {
+ const networkIdIfExists = await this.web3Wrapper.getNetworkIdIfExistsAsync();
+ const exchangeNetworkConfigsIfExists = _.isUndefined(networkIdIfExists) ?
+ undefined :
+ (ExchangeArtifacts as any).networks[networkIdIfExists];
+ if (_.isUndefined(exchangeNetworkConfigsIfExists)) {
+ throw new Error(ZeroExError.CONTRACT_NOT_DEPLOYED_ON_NETWORK);
+ }
+ const exchangeAddress = exchangeNetworkConfigsIfExists.address;
+ return exchangeAddress;
+ }
}
diff --git a/src/artifacts/ProtocolToken.json b/src/artifacts/ProtocolToken.json
new file mode 100644
index 000000000..a1f8e0ff6
--- /dev/null
+++ b/src/artifacts/ProtocolToken.json
@@ -0,0 +1,321 @@
+{
+ "contract_name": "ProtocolToken",
+ "abi": [
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "name",
+ "outputs": [
+ {
+ "name": "",
+ "type": "string"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_spender",
+ "type": "address"
+ },
+ {
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "approve",
+ "outputs": [
+ {
+ "name": "success",
+ "type": "bool"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "totalSupply",
+ "outputs": [
+ {
+ "name": "",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_from",
+ "type": "address"
+ },
+ {
+ "name": "_to",
+ "type": "address"
+ },
+ {
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "transferFrom",
+ "outputs": [
+ {
+ "name": "success",
+ "type": "bool"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "decimals",
+ "outputs": [
+ {
+ "name": "",
+ "type": "uint8"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "_owner",
+ "type": "address"
+ }
+ ],
+ "name": "balanceOf",
+ "outputs": [
+ {
+ "name": "balance",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [],
+ "name": "symbol",
+ "outputs": [
+ {
+ "name": "",
+ "type": "string"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": false,
+ "inputs": [
+ {
+ "name": "_to",
+ "type": "address"
+ },
+ {
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "transfer",
+ "outputs": [
+ {
+ "name": "success",
+ "type": "bool"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "constant": true,
+ "inputs": [
+ {
+ "name": "_owner",
+ "type": "address"
+ },
+ {
+ "name": "_spender",
+ "type": "address"
+ }
+ ],
+ "name": "allowance",
+ "outputs": [
+ {
+ "name": "remaining",
+ "type": "uint256"
+ }
+ ],
+ "payable": false,
+ "type": "function"
+ },
+ {
+ "inputs": [],
+ "payable": false,
+ "type": "constructor"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_from",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "_to",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Transfer",
+ "type": "event"
+ },
+ {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_owner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "_spender",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Approval",
+ "type": "event"
+ }
+ ],
+ "unlinked_binary": "0x606060405234610000575b6a52b7d2dcc80cd2e40000006002819055600160a060020a0333166000908152602081905260409020555b5b6105fc806100456000396000f3006060604052361561007d5763ffffffff60e060020a60003504166306fdde038114610082578063095ea7b31461010f57806318160ddd1461013f57806323b872dd1461015e578063313ce5671461019457806370a08231146101b757806395d89b41146101e2578063a9059cbb1461026f578063dd62ed3e1461029f575b610000565b346100005761008f6102d0565b6040805160208082528351818301528351919283929083019185019080838382156100d5575b8051825260208311156100d557601f1990920191602091820191016100b5565b505050905090810190601f1680156101015780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b346100005761012b600160a060020a0360043516602435610307565b604080519115158252519081900360200190f35b346100005761014c610372565b60408051918252519081900360200190f35b346100005761012b600160a060020a0360043581169060243516604435610378565b604080519115158252519081900360200190f35b34610000576101a1610485565b6040805160ff9092168252519081900360200190f35b346100005761014c600160a060020a036004351661048a565b60408051918252519081900360200190f35b346100005761008f6104a9565b6040805160208082528351818301528351919283929083019185019080838382156100d5575b8051825260208311156100d557601f1990920191602091820191016100b5565b505050905090810190601f1680156101015780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b346100005761012b600160a060020a03600435166024356104e0565b604080519115158252519081900360200190f35b346100005761014c600160a060020a03600435811690602435166105a3565b60408051918252519081900360200190f35b60408051808201909152601081527f3078204e6574776f726b20546f6b656e00000000000000000000000000000000602082015281565b600160a060020a03338116600081815260016020908152604080832094871680845294825280832086905580518681529051929493927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929181900390910190a35060015b92915050565b60025481565b600160a060020a0383166000908152602081905260408120548290108015906103c85750600160a060020a0380851660009081526001602090815260408083203390941683529290522054829010155b80156103ed5750600160a060020a038316600090815260208190526040902054828101115b1561047957600160a060020a0380841660008181526020818152604080832080548801905588851680845281842080548990039055600183528184203390961684529482529182902080548790039055815186815291519293927fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef9281900390910190a350600161047d565b5060005b5b9392505050565b601281565b600160a060020a0381166000908152602081905260409020545b919050565b60408051808201909152600381527f5a52580000000000000000000000000000000000000000000000000000000000602082015281565b600160a060020a0333166000908152602081905260408120548290108015906105225750600160a060020a038316600090815260208190526040902054828101115b1561059457600160a060020a0333811660008181526020818152604080832080548890039055938716808352918490208054870190558351868152935191937fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef929081900390910190a350600161036c565b50600061036c565b5b92915050565b600160a060020a038083166000908152600160209081526040808320938516835292905220545b929150505600a165627a7a723058209912b9e0769f5bfbb24cc18ed6298aba4698371192fbf585b239f6db1b8bdc2f0029",
+ "networks": {
+ "42": {
+ "links": {},
+ "events": {
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_from",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "_to",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Transfer",
+ "type": "event"
+ },
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_owner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "_spender",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Approval",
+ "type": "event"
+ }
+ },
+ "updated_at": 1495042008608
+ },
+ "50": {
+ "links": {},
+ "events": {
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_from",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "_to",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Transfer",
+ "type": "event"
+ },
+ "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925": {
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true,
+ "name": "_owner",
+ "type": "address"
+ },
+ {
+ "indexed": true,
+ "name": "_spender",
+ "type": "address"
+ },
+ {
+ "indexed": false,
+ "name": "_value",
+ "type": "uint256"
+ }
+ ],
+ "name": "Approval",
+ "type": "event"
+ }
+ },
+ "updated_at": 1495030736785
+ }
+ },
+ "schema_version": "0.0.5",
+ "updated_at": 1495042008608
+} \ No newline at end of file
diff --git a/src/bignumber_config.ts b/src/bignumber_config.ts
new file mode 100644
index 000000000..9c1715f86
--- /dev/null
+++ b/src/bignumber_config.ts
@@ -0,0 +1,11 @@
+import * as BigNumber from 'bignumber.js';
+
+export const bigNumberConfigs = {
+ configure() {
+ // By default BigNumber's `toString` method converts to exponential notation if the value has
+ // more then 20 digits. We want to avoid this behavior, so we set EXPONENTIAL_AT to a high number
+ BigNumber.config({
+ EXPONENTIAL_AT: 1000,
+ });
+ },
+};
diff --git a/src/contract_wrappers/exchange_wrapper.ts b/src/contract_wrappers/exchange_wrapper.ts
index 3f6eb0dab..fa5c16485 100644
--- a/src/contract_wrappers/exchange_wrapper.ts
+++ b/src/contract_wrappers/exchange_wrapper.ts
@@ -1,15 +1,40 @@
import * as _ from 'lodash';
import {Web3Wrapper} from '../web3_wrapper';
-import {ECSignature, ZeroExError, ExchangeContract} from '../types';
+import {
+ ECSignature,
+ ExchangeContract,
+ ExchangeContractErrCodes,
+ ExchangeContractErrs,
+ FillOrderValidationErrs,
+ 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/order_schemas';
+import {SchemaValidator} from '../utils/schema_validator';
+import {ContractResponse} from '../types';
+import {constants} from '../utils/constants';
+import {TokenWrapper} from './token_wrapper';
export class ExchangeWrapper extends ContractWrapper {
+ private exchangeContractErrCodesToMsg = {
+ [ExchangeContractErrCodes.ERROR_FILL_EXPIRED]: ExchangeContractErrs.ORDER_EXPIRED,
+ [ExchangeContractErrCodes.ERROR_CANCEL_EXPIRED]: ExchangeContractErrs.ORDER_EXPIRED,
+ [ExchangeContractErrCodes.ERROR_FILL_NO_VALUE]: ExchangeContractErrs.ORDER_REMAINING_FILL_AMOUNT_ZERO,
+ [ExchangeContractErrCodes.ERROR_CANCEL_NO_VALUE]: ExchangeContractErrs.ORDER_REMAINING_FILL_AMOUNT_ZERO,
+ [ExchangeContractErrCodes.ERROR_FILL_TRUNCATION]: ExchangeContractErrs.ORDER_ROUNDING_ERROR,
+ [ExchangeContractErrCodes.ERROR_FILL_BALANCE_ALLOWANCE]: ExchangeContractErrs.ORDER_BALANCE_ALLOWANCE_ERROR,
+ };
private exchangeContractIfExists?: ExchangeContract;
- constructor(web3Wrapper: Web3Wrapper) {
+ private tokenWrapper: TokenWrapper;
+ constructor(web3Wrapper: Web3Wrapper, tokenWrapper: TokenWrapper) {
super(web3Wrapper);
+ this.tokenWrapper = tokenWrapper;
}
public invalidateContractInstance(): void {
delete this.exchangeContractIfExists;
@@ -20,23 +45,176 @@ 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;
}
+ /**
+ * Fills a signed order with a fillAmount denominated in baseUnits of the taker token. The caller can
+ * decide whether they want the call to throw if the balance/allowance checks fail by setting
+ * shouldCheckTransfer to false. If set to true, the call will fail without throwing, preserving gas costs.
+ */
+ public async fillOrderAsync(signedOrder: SignedOrder, fillTakerAmountInBaseUnits: BigNumber.BigNumber,
+ shouldCheckTransfer: boolean): Promise<void> {
+ assert.doesConformToSchema('signedOrder',
+ SchemaValidator.convertToJSONSchemaCompatibleObject(signedOrder as object),
+ signedOrderSchema);
+ assert.isBigNumber('fillTakerAmountInBaseUnits', fillTakerAmountInBaseUnits);
+ assert.isBoolean('shouldCheckTransfer', shouldCheckTransfer);
+
+ const senderAddress = await this.web3Wrapper.getSenderAddressOrThrowAsync();
+ const exchangeInstance = await this.getExchangeContractAsync();
+ const zrxTokenAddress = await exchangeInstance.ZRX.call();
+ await this.validateFillOrderAsync(signedOrder, fillTakerAmountInBaseUnits, senderAddress, zrxTokenAddress);
+
+ const orderAddresses: OrderAddresses = [
+ signedOrder.maker,
+ signedOrder.taker,
+ signedOrder.makerTokenAddress,
+ signedOrder.takerTokenAddress,
+ signedOrder.feeRecipient,
+ ];
+ const orderValues: OrderValues = [
+ signedOrder.makerTokenAmount,
+ signedOrder.takerTokenAmount,
+ signedOrder.makerFee,
+ signedOrder.takerFee,
+ signedOrder.expirationUnixTimestampSec,
+ signedOrder.salt,
+ ];
+ const gas = await exchangeInstance.fill.estimateGas(
+ orderAddresses,
+ orderValues,
+ fillTakerAmountInBaseUnits,
+ shouldCheckTransfer,
+ signedOrder.ecSignature.v,
+ signedOrder.ecSignature.r,
+ signedOrder.ecSignature.s,
+ {
+ from: senderAddress,
+ },
+ );
+ const response: ContractResponse = await exchangeInstance.fill(
+ orderAddresses,
+ orderValues,
+ fillTakerAmountInBaseUnits,
+ shouldCheckTransfer,
+ signedOrder.ecSignature.v,
+ signedOrder.ecSignature.r,
+ signedOrder.ecSignature.s,
+ {
+ from: senderAddress,
+ gas,
+ },
+ );
+ this.throwErrorLogsAsErrors(response.logs);
+ }
+ private async validateFillOrderAsync(signedOrder: SignedOrder, fillTakerAmountInBaseUnits: BigNumber.BigNumber,
+ senderAddress: string, zrxTokenAddress: string): Promise<void> {
+ if (fillTakerAmountInBaseUnits.eq(0)) {
+ throw new Error(FillOrderValidationErrs.FILL_AMOUNT_IS_ZERO);
+ }
+ if (signedOrder.taker !== constants.NULL_ADDRESS && signedOrder.taker !== senderAddress) {
+ throw new Error(FillOrderValidationErrs.NOT_A_TAKER);
+ }
+ if (signedOrder.expirationUnixTimestampSec.lessThan(Date.now() / 1000)) {
+ throw new Error(FillOrderValidationErrs.EXPIRED);
+ }
+
+ await this.validateFillOrderBalancesAndAllowancesAsync(signedOrder, fillTakerAmountInBaseUnits,
+ senderAddress, zrxTokenAddress);
+
+ if (await this.isRoundingErrorAsync(signedOrder.takerTokenAmount, fillTakerAmountInBaseUnits,
+ signedOrder.makerTokenAmount)) {
+ throw new Error(FillOrderValidationErrs.ROUNDING_ERROR);
+ }
+ }
+ private async validateFillOrderBalancesAndAllowancesAsync(signedOrder: SignedOrder,
+ fillTakerAmountInBaseUnits: BigNumber.BigNumber,
+ senderAddress: string,
+ zrxTokenAddress: string): Promise<void> {
+ // TODO: There is a possibility that the user might have enough funds
+ // to fulfill the order or pay fees but not both. This will happen if
+ // makerToken === zrxToken || makerToken === zrxToken
+ // We don't check it for now. The contract checks it and throws.
+
+ const makerBalance = await this.tokenWrapper.getBalanceAsync(signedOrder.makerTokenAddress,
+ signedOrder.maker);
+ const takerBalance = await this.tokenWrapper.getBalanceAsync(signedOrder.takerTokenAddress, senderAddress);
+ const makerAllowance = await this.tokenWrapper.getProxyAllowanceAsync(signedOrder.makerTokenAddress,
+ signedOrder.maker);
+ const takerAllowance = await this.tokenWrapper.getProxyAllowanceAsync(signedOrder.takerTokenAddress,
+ senderAddress);
+
+ // How many taker tokens would you get for 1 maker token;
+ const exchangeRate = signedOrder.takerTokenAmount.div(signedOrder.makerTokenAmount);
+ const fillMakerAmountInBaseUnits = fillTakerAmountInBaseUnits.div(exchangeRate);
+
+ if (fillTakerAmountInBaseUnits.greaterThan(takerBalance)) {
+ throw new Error(FillOrderValidationErrs.NOT_ENOUGH_TAKER_BALANCE);
+ }
+ if (fillTakerAmountInBaseUnits.greaterThan(takerAllowance)) {
+ throw new Error(FillOrderValidationErrs.NOT_ENOUGH_TAKER_ALLOWANCE);
+ }
+ if (fillMakerAmountInBaseUnits.greaterThan(makerBalance)) {
+ throw new Error(FillOrderValidationErrs.NOT_ENOUGH_MAKER_BALANCE);
+ }
+ if (fillMakerAmountInBaseUnits.greaterThan(makerAllowance)) {
+ throw new Error(FillOrderValidationErrs.NOT_ENOUGH_MAKER_ALLOWANCE);
+ }
+
+ const makerFeeBalance = await this.tokenWrapper.getBalanceAsync(zrxTokenAddress,
+ signedOrder.maker);
+ const takerFeeBalance = await this.tokenWrapper.getBalanceAsync(zrxTokenAddress, senderAddress);
+ const makerFeeAllowance = await this.tokenWrapper.getProxyAllowanceAsync(zrxTokenAddress,
+ signedOrder.maker);
+ const takerFeeAllowance = await this.tokenWrapper.getProxyAllowanceAsync(zrxTokenAddress,
+ senderAddress);
+
+ if (signedOrder.takerFee.greaterThan(takerFeeBalance)) {
+ throw new Error(FillOrderValidationErrs.NOT_ENOUGH_TAKER_FEE_BALANCE);
+ }
+ if (signedOrder.takerFee.greaterThan(takerFeeAllowance)) {
+ throw new Error(FillOrderValidationErrs.NOT_ENOUGH_TAKER_FEE_ALLOWANCE);
+ }
+ if (signedOrder.makerFee.greaterThan(makerFeeBalance)) {
+ throw new Error(FillOrderValidationErrs.NOT_ENOUGH_MAKER_FEE_BALANCE);
+ }
+ if (signedOrder.makerFee.greaterThan(makerFeeAllowance)) {
+ throw new Error(FillOrderValidationErrs.NOT_ENOUGH_MAKER_FEE_ALLOWANCE);
+ }
+ }
+ private throwErrorLogsAsErrors(logs: ContractEvent[]): void {
+ const errEvent = _.find(logs, {event: 'LogError'});
+ if (!_.isUndefined(errEvent)) {
+ const errCode = errEvent.args.errorId.toNumber();
+ const errMessage = this.exchangeContractErrCodesToMsg[errCode];
+ throw new Error(errMessage);
+ }
+ }
+ private async isRoundingErrorAsync(takerTokenAmount: BigNumber.BigNumber,
+ fillTakerAmountInBaseUnits: BigNumber.BigNumber,
+ makerTokenAmount: BigNumber.BigNumber): Promise<boolean> {
+ const exchangeInstance = await this.getExchangeContractAsync();
+ const senderAddress = await this.web3Wrapper.getSenderAddressOrThrowAsync();
+ const isRoundingError = await exchangeInstance.isRoundingError.call(
+ takerTokenAmount, fillTakerAmountInBaseUnits, makerTokenAmount, {
+ from: senderAddress,
+ },
+ );
+ return isRoundingError;
+ }
private async getExchangeContractAsync(): Promise<ExchangeContract> {
if (!_.isUndefined(this.exchangeContractIfExists)) {
return this.exchangeContractIfExists;
diff --git a/src/globals.d.ts b/src/globals.d.ts
index 0f2fe0f2f..d86f54dfc 100644
--- a/src/globals.d.ts
+++ b/src/globals.d.ts
@@ -1,4 +1,5 @@
declare module 'chai-bignumber';
+declare module 'dirty-chai';
declare module 'bn.js';
declare module 'request-promise-native';
declare module 'web3-provider-engine';
diff --git a/src/schemas/order_schemas.ts b/src/schemas/order_schemas.ts
new file mode 100644
index 000000000..72012dc26
--- /dev/null
+++ b/src/schemas/order_schemas.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 717257492..4819b13ac 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -27,8 +27,26 @@ 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 ExchangeContract {
isValidSignature: any;
+ isRoundingError: {
+ call: (takerTokenAmount: BigNumber.BigNumber, fillTakerAmountInBaseUnits: BigNumber.BigNumber,
+ makerTokenAmount: BigNumber.BigNumber, txOpts: TxOpts) => Promise<boolean>;
+ };
+ fill: {
+ (orderAddresses: OrderAddresses, orderValues: OrderValues, fillAmount: BigNumber.BigNumber,
+ shouldCheckTransfer: boolean, v: number, r: string, s: string, txOpts: TxOpts): ContractResponse;
+ estimateGas: (orderAddresses: OrderAddresses, orderValues: OrderValues, fillAmount: BigNumber.BigNumber,
+ shouldCheckTransfer: boolean, v: number, r: string, s: string, txOpts: TxOpts) => number;
+ };
+ ZRX: {
+ call: () => Promise<string>;
+ };
}
export interface TokenContract {
@@ -57,6 +75,66 @@ export const SolidityTypes = strEnum([
]);
export type SolidityTypes = keyof typeof SolidityTypes;
+export enum ExchangeContractErrCodes {
+ 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 const ExchangeContractErrs = strEnum([
+ 'ORDER_EXPIRED',
+ 'ORDER_REMAINING_FILL_AMOUNT_ZERO',
+ 'ORDER_ROUNDING_ERROR',
+ 'ORDER_BALANCE_ALLOWANCE_ERROR',
+]);
+export type ExchangeContractErrs = keyof typeof ExchangeContractErrs;
+
+export const FillOrderValidationErrs = strEnum([
+ 'FILL_AMOUNT_IS_ZERO',
+ 'NOT_A_TAKER',
+ 'EXPIRED',
+ 'NOT_ENOUGH_TAKER_BALANCE',
+ 'NOT_ENOUGH_TAKER_ALLOWANCE',
+ 'NOT_ENOUGH_MAKER_BALANCE',
+ 'NOT_ENOUGH_MAKER_ALLOWANCE',
+ 'NOT_ENOUGH_TAKER_FEE_BALANCE',
+ 'NOT_ENOUGH_TAKER_FEE_ALLOWANCE',
+ 'NOT_ENOUGH_MAKER_FEE_BALANCE',
+ 'NOT_ENOUGH_MAKER_FEE_ALLOWANCE',
+ 'ROUNDING_ERROR',
+]);
+export type FillOrderValidationErrs = keyof typeof FillOrderValidationErrs;
+
+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];
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..db8a960ba 100644
--- a/src/utils/schema_validator.ts
+++ b/src/utils/schema_validator.ts
@@ -1,14 +1,26 @@
import {Validator, ValidatorResult} from 'jsonschema';
import {ecSignatureSchema, ecSignatureParameter} from '../schemas/ec_signature_schema';
+import {addressSchema, numberSchema, orderSchema, signedOrderSchema} from '../schemas/order_schemas';
import {tokenSchema} from '../schemas/token_schema';
export class SchemaValidator {
private validator: Validator;
+ // In order to validate a complex JS object using jsonschema, we must replace any complex
+ // sub-types (e.g BigNumber) with a simpler string represenation. Since BigNumber and other
+ // complex types implement the `toString` method, we can stringify the object and
+ // then parse it. The resultant object can then be checked using jsonschema.
+ public static convertToJSONSchemaCompatibleObject(obj: object): object {
+ return JSON.parse(JSON.stringify(obj));
+ }
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..49bd8b67d 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;
@@ -15,13 +17,16 @@ export class Web3Wrapper {
public isAddress(address: string): boolean {
return this.web3.isAddress(address);
}
- public async getSenderAddressIfExistsAsync(): Promise<string|undefined> {
- const defaultAccount = this.web3.eth.defaultAccount;
- if (!_.isUndefined(defaultAccount)) {
- return defaultAccount;
- }
- const firstAccount = await this.getFirstAddressIfExistsAsync();
- return firstAccount;
+ public getDefaultAccount(): string {
+ return this.web3.eth.defaultAccount;
+ }
+ public setDefaultAccount(address: string): void {
+ this.web3.eth.defaultAccount = address;
+ }
+ 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)();
@@ -64,6 +69,14 @@ export class Web3Wrapper {
const {timestamp} = await promisify(this.web3.eth.getBlock)(blockHash);
return timestamp;
}
+ public async getSenderAddressIfExistsAsync(): Promise<string|undefined> {
+ const defaultAccount = this.web3.eth.defaultAccount;
+ if (!_.isUndefined(defaultAccount)) {
+ return defaultAccount;
+ }
+ const firstAccount = await this.getFirstAddressIfExistsAsync();
+ return firstAccount;
+ }
private async getNetworkAsync(): Promise<number> {
const networkId = await promisify(this.web3.version.getNetwork)();
return networkId;