Switch over to Lerna + Yarn Workspaces setup for a mono-repo approach
This commit is contained in:
37
packages/0x.js/README.md
Normal file
37
packages/0x.js/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
## Installation
|
||||
|
||||
0x.js ships as both a [UMD](https://github.com/umdjs/umd) module and a [CommonJS](https://en.wikipedia.org/wiki/CommonJS) package.
|
||||
|
||||
#### CommonJS *(recommended)*:
|
||||
|
||||
**Install**
|
||||
|
||||
```bash
|
||||
npm install 0x.js --save
|
||||
```
|
||||
|
||||
**Import**
|
||||
|
||||
```javascript
|
||||
import {ZeroEx} from '0x.js';
|
||||
```
|
||||
|
||||
#### UMD:
|
||||
|
||||
**Install**
|
||||
|
||||
Download the UMD module from our [releases page](https://github.com/0xProject/0x.js/releases) and add it to your project.
|
||||
|
||||
**Import**
|
||||
|
||||
```html
|
||||
<script type="text/javascript" src="0x.js"></script>
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Extensive documentation of 0x.js can be found on [our website][docs-url].
|
||||
|
||||
[website-url]: https://0xproject.com/
|
||||
[whitepaper-url]: https://0xproject.com/pdfs/0x_white_paper.pdf
|
||||
[docs-url]: https://0xproject.com/docs/0xjs
|
106
packages/0x.js/package.json
Normal file
106
packages/0x.js/package.json
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"name": "0x.js",
|
||||
"version": "0.23.0",
|
||||
"description": "A javascript library for interacting with the 0x protocol",
|
||||
"keywords": [
|
||||
"0x.js",
|
||||
"0xproject",
|
||||
"ethereum",
|
||||
"tokens",
|
||||
"exchange"
|
||||
],
|
||||
"main": "lib/src/index.js",
|
||||
"types": "lib/src/index.d.ts",
|
||||
"scripts": {
|
||||
"prebuild": "npm run clean",
|
||||
"build": "run-p build:umd:prod build:commonjs",
|
||||
"prepublishOnly": "run-p build",
|
||||
"postpublish": "run-s release docs:json upload_docs_json",
|
||||
"release": "publish-release --assets _bundles/index.js,_bundles/index.min.js --tag $(git describe --tags) --owner 0xProject --repo 0x.js",
|
||||
"upload_docs_json": "aws s3 cp docs/index.json s3://0xjs-docs-jsons/$(git describe --tags).json --profile 0xproject --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --content-type aplication/json",
|
||||
"lint": "tslint src/**/*.ts test/**/*.ts",
|
||||
"test": "run-s clean test:commonjs",
|
||||
"test:umd": "./scripts/test_umd.sh",
|
||||
"test:coverage": "nyc npm run test --all",
|
||||
"report_test_coverage": "nyc report --reporter=text-lcov | coveralls",
|
||||
"update_contracts": "for i in ${npm_package_config_artifacts}; do copyfiles -u 4 ../contracts/build/contracts/$i.json ../0x.js/src/artifacts; done;",
|
||||
"testrpc": "testrpc -p 8545 --networkId 50 -m \"${npm_package_config_mnemonic}\"",
|
||||
"docs:json": "typedoc --excludePrivate --excludeExternals --target ES5 --json docs/index.json .",
|
||||
"docs:generate": "typedoc --out docs .",
|
||||
"docs:open": "opn docs/index.html",
|
||||
"clean": "shx rm -rf _bundles lib test_temp",
|
||||
"build:umd:dev": "webpack",
|
||||
"build:umd:prod": "NODE_ENV=production webpack",
|
||||
"build:commonjs": "tsc; copyfiles -u 2 './src/artifacts/**/*.json' ./lib/src/artifacts;",
|
||||
"test:commonjs": "run-s build:commonjs run_mocha",
|
||||
"pretest:umd": "run-s clean build:umd:dev build:commonjs",
|
||||
"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/subproviders -o -path ./lib/src/schemas -o -path \"./lib/src/types.*\" \\) -prune -o -type f -print | xargs rm",
|
||||
"run_mocha": "mocha lib/test/**/*_test.js --timeout 5000 --bail --exit"
|
||||
},
|
||||
"config": {
|
||||
"artifacts": "TokenTransferProxy Exchange TokenRegistry Token EtherToken",
|
||||
"mnemonic": "concert load couple harbor equip island argue ramp clarify fence smart topic"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/0xProject/0x.js"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonschema": "^1.1.1",
|
||||
"@types/lodash": "^4.14.64",
|
||||
"@types/mocha": "^2.2.41",
|
||||
"@types/node": "^8.0.1",
|
||||
"@types/sinon": "^2.2.2",
|
||||
"@types/uuid": "^3.4.2",
|
||||
"awesome-typescript-loader": "^3.1.3",
|
||||
"chai": "^4.0.1",
|
||||
"chai-as-promised": "^7.1.0",
|
||||
"chai-as-promised-typescript-typings": "0.0.3",
|
||||
"chai-bignumber": "^2.0.1",
|
||||
"chai-typescript-typings": "^0.0.1",
|
||||
"copyfiles": "^1.2.0",
|
||||
"coveralls": "^3.0.0",
|
||||
"dirty-chai": "^2.0.1",
|
||||
"ethereumjs-testrpc": "4.0.1",
|
||||
"json-loader": "^0.5.4",
|
||||
"mocha": "^4.0.0",
|
||||
"npm-run-all": "^4.0.2",
|
||||
"nyc": "^11.0.1",
|
||||
"opn-cli": "^3.1.0",
|
||||
"request": "^2.81.0",
|
||||
"request-promise-native": "^1.0.4",
|
||||
"shx": "^0.2.2",
|
||||
"sinon": "^4.0.0",
|
||||
"source-map-support": "^0.5.0",
|
||||
"truffle-hdwallet-provider": "^0.0.3",
|
||||
"tslint": "~5.5.0",
|
||||
"tslint-config-0xproject": "^0.0.2",
|
||||
"typedoc": "~0.8.0",
|
||||
"types-bn": "^0.0.1",
|
||||
"types-ethereumjs-util": "0xProject/types-ethereumjs-util",
|
||||
"typescript": "^2.4.1",
|
||||
"web3-provider-engine": "^13.0.1",
|
||||
"web3-typescript-typings": "^0.7.1",
|
||||
"webpack": "^3.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"0x-json-schemas": "^0.6.1",
|
||||
"bignumber.js": "^4.1.0",
|
||||
"compare-versions": "^3.0.1",
|
||||
"es6-promisify": "^5.0.0",
|
||||
"ethereumjs-abi": "^0.6.4",
|
||||
"ethereumjs-blockstream": "^2.0.6",
|
||||
"ethereumjs-util": "^5.1.1",
|
||||
"find-versions": "^2.0.0",
|
||||
"js-sha3": "^0.6.1",
|
||||
"lodash": "^4.17.4",
|
||||
"publish-release": "^1.3.3",
|
||||
"uuid": "^3.1.0",
|
||||
"web3": "^0.20.0"
|
||||
}
|
||||
}
|
7
packages/0x.js/scripts/test_umd.sh
Executable file
7
packages/0x.js/scripts/test_umd.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# This script runs umd tests and cleans up after them while preserving the `return_code` for CI
|
||||
# UMD tests should only be run after building the commonjs because they reuse some of the commonjs build artifacts
|
||||
run-s substitute_umd_bundle run_mocha
|
||||
return_code=$?
|
||||
npm run clean
|
||||
exit $return_code
|
333
packages/0x.js/src/0x.ts
Normal file
333
packages/0x.js/src/0x.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
import * as _ from 'lodash';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {SchemaValidator, schemas} from '0x-json-schemas';
|
||||
import {bigNumberConfigs} from './bignumber_config';
|
||||
import * as ethUtil from 'ethereumjs-util';
|
||||
import {Web3Wrapper} from './web3_wrapper';
|
||||
import {constants} from './utils/constants';
|
||||
import {utils} from './utils/utils';
|
||||
import {signatureUtils} from './utils/signature_utils';
|
||||
import {assert} from './utils/assert';
|
||||
import {AbiDecoder} from './utils/abi_decoder';
|
||||
import {intervalUtils} from './utils/interval_utils';
|
||||
import {artifacts} from './artifacts';
|
||||
import {ExchangeWrapper} from './contract_wrappers/exchange_wrapper';
|
||||
import {TokenRegistryWrapper} from './contract_wrappers/token_registry_wrapper';
|
||||
import {EtherTokenWrapper} from './contract_wrappers/ether_token_wrapper';
|
||||
import {TokenWrapper} from './contract_wrappers/token_wrapper';
|
||||
import {TokenTransferProxyWrapper} from './contract_wrappers/token_transfer_proxy_wrapper';
|
||||
import {OrderStateWatcher} from './order_watcher/order_state_watcher';
|
||||
import {OrderStateUtils} from './utils/order_state_utils';
|
||||
import {
|
||||
ECSignature,
|
||||
ZeroExError,
|
||||
Order,
|
||||
SignedOrder,
|
||||
Web3Provider,
|
||||
ZeroExConfig,
|
||||
OrderStateWatcherConfig,
|
||||
TransactionReceiptWithDecodedLogs,
|
||||
} from './types';
|
||||
import {zeroExConfigSchema} from './schemas/zero_ex_config_schema';
|
||||
|
||||
// Customize our BigNumber instances
|
||||
bigNumberConfigs.configure();
|
||||
|
||||
/**
|
||||
* The ZeroEx class is the single entry-point into the 0x.js library. It contains all of the library's functionality
|
||||
* and all calls to the library should be made through a ZeroEx instance.
|
||||
*/
|
||||
export class ZeroEx {
|
||||
/**
|
||||
* When creating an order without a specified taker or feeRecipient you must supply the Solidity
|
||||
* address null type (as opposed to Javascripts `null`, `undefined` or empty string). We expose
|
||||
* this constant for your convenience.
|
||||
*/
|
||||
public static NULL_ADDRESS = constants.NULL_ADDRESS;
|
||||
|
||||
/**
|
||||
* An instance of the ExchangeWrapper class containing methods for interacting with the 0x Exchange smart contract.
|
||||
*/
|
||||
public exchange: ExchangeWrapper;
|
||||
/**
|
||||
* An instance of the TokenRegistryWrapper class containing methods for interacting with the 0x
|
||||
* TokenRegistry smart contract.
|
||||
*/
|
||||
public tokenRegistry: TokenRegistryWrapper;
|
||||
/**
|
||||
* An instance of the TokenWrapper class containing methods for interacting with any ERC20 token smart contract.
|
||||
*/
|
||||
public token: TokenWrapper;
|
||||
/**
|
||||
* An instance of the EtherTokenWrapper class containing methods for interacting with the
|
||||
* wrapped ETH ERC20 token smart contract.
|
||||
*/
|
||||
public etherToken: EtherTokenWrapper;
|
||||
/**
|
||||
* An instance of the TokenTransferProxyWrapper class containing methods for interacting with the
|
||||
* tokenTransferProxy smart contract.
|
||||
*/
|
||||
public proxy: TokenTransferProxyWrapper;
|
||||
/**
|
||||
* An instance of the OrderStateWatcher class containing methods for watching a set of orders for relevant
|
||||
* blockchain state changes.
|
||||
*/
|
||||
public orderStateWatcher: OrderStateWatcher;
|
||||
private _web3Wrapper: Web3Wrapper;
|
||||
private _abiDecoder: AbiDecoder;
|
||||
/**
|
||||
* Verifies that the elliptic curve signature `signature` was generated
|
||||
* by signing `data` with the private key corresponding to the `signerAddress` address.
|
||||
* @param data The hex encoded data signed by the supplied signature.
|
||||
* @param signature An object containing the elliptic curve signature parameters.
|
||||
* @param signerAddress The hex encoded address that signed the data, producing the supplied signature.
|
||||
* @return Whether the signature is valid for the supplied signerAddress and data.
|
||||
*/
|
||||
public static isValidSignature(data: string, signature: ECSignature, signerAddress: string): boolean {
|
||||
assert.isHexString('data', data);
|
||||
assert.doesConformToSchema('signature', signature, schemas.ecSignatureSchema);
|
||||
assert.isETHAddressHex('signerAddress', signerAddress);
|
||||
|
||||
const isValidSignature = signatureUtils.isValidSignature(data, signature, signerAddress);
|
||||
return isValidSignature;
|
||||
}
|
||||
/**
|
||||
* Generates a pseudo-random 256-bit salt.
|
||||
* The salt can be included in an 0x order, ensuring that the order generates a unique orderHash
|
||||
* and will not collide with other outstanding orders that are identical in all other parameters.
|
||||
* @return A pseudo-random 256-bit number that can be used as a salt.
|
||||
*/
|
||||
public static generatePseudoRandomSalt(): BigNumber {
|
||||
// BigNumber.random returns a pseudo-random number between 0 & 1 with a passed in number of decimal places.
|
||||
// Source: https://mikemcl.github.io/bignumber.js/#random
|
||||
const randomNumber = BigNumber.random(constants.MAX_DIGITS_IN_UNSIGNED_256_INT);
|
||||
const factor = new BigNumber(10).pow(constants.MAX_DIGITS_IN_UNSIGNED_256_INT - 1);
|
||||
const salt = randomNumber.times(factor).round();
|
||||
return salt;
|
||||
}
|
||||
/**
|
||||
* Checks if the supplied hex encoded order hash is valid.
|
||||
* Note: Valid means it has the expected format, not that an order with the orderHash exists.
|
||||
* Use this method when processing orderHashes submitted as user input.
|
||||
* @param orderHash Hex encoded orderHash.
|
||||
* @return Whether the supplied orderHash has the expected format.
|
||||
*/
|
||||
public static isValidOrderHash(orderHash: string): boolean {
|
||||
// Since this method can be called to check if any arbitrary string conforms to an orderHash's
|
||||
// format, we only assert that we were indeed passed a string.
|
||||
assert.isString('orderHash', orderHash);
|
||||
const schemaValidator = new SchemaValidator();
|
||||
const isValidOrderHash = schemaValidator.validate(orderHash, schemas.orderHashSchema).valid;
|
||||
return isValidOrderHash;
|
||||
}
|
||||
/**
|
||||
* A unit amount is defined as the amount of a token above the specified decimal places (integer part).
|
||||
* E.g: If a currency has 18 decimal places, 1e18 or one quintillion of the currency is equivalent
|
||||
* to 1 unit.
|
||||
* @param amount The amount in baseUnits that you would like converted to units.
|
||||
* @param decimals The number of decimal places the unit amount has.
|
||||
* @return The amount in units.
|
||||
*/
|
||||
public static toUnitAmount(amount: BigNumber, decimals: number): BigNumber {
|
||||
assert.isBigNumber('amount', amount);
|
||||
assert.isNumber('decimals', decimals);
|
||||
|
||||
const aUnit = new BigNumber(10).pow(decimals);
|
||||
const unit = amount.div(aUnit);
|
||||
return unit;
|
||||
}
|
||||
/**
|
||||
* A baseUnit is defined as the smallest denomination of a token. An amount expressed in baseUnits
|
||||
* is the amount expressed in the smallest denomination.
|
||||
* E.g: 1 unit of a token with 18 decimal places is expressed in baseUnits as 1000000000000000000
|
||||
* @param amount The amount of units that you would like converted to baseUnits.
|
||||
* @param decimals The number of decimal places the unit amount has.
|
||||
* @return The amount in baseUnits.
|
||||
*/
|
||||
public static toBaseUnitAmount(amount: BigNumber, decimals: number): BigNumber {
|
||||
assert.isBigNumber('amount', amount);
|
||||
assert.isNumber('decimals', decimals);
|
||||
|
||||
const unit = new BigNumber(10).pow(decimals);
|
||||
const baseUnitAmount = amount.times(unit);
|
||||
return baseUnitAmount;
|
||||
}
|
||||
/**
|
||||
* Computes the orderHash for a supplied order.
|
||||
* @param order An object that conforms to the Order or SignedOrder interface definitions.
|
||||
* @return The resulting orderHash from hashing the supplied order.
|
||||
*/
|
||||
public static getOrderHashHex(order: Order|SignedOrder): string {
|
||||
assert.doesConformToSchema('order', order, schemas.orderSchema);
|
||||
const orderHashHex = utils.getOrderHashHex(order);
|
||||
return orderHashHex;
|
||||
}
|
||||
/**
|
||||
* Instantiates a new ZeroEx instance that provides the public interface to the 0x.js library.
|
||||
* @param provider The Web3.js Provider instance you would like the 0x.js library to use for interacting with
|
||||
* the Ethereum network.
|
||||
* @param config The configuration object. Look up the type for the description.
|
||||
* @return An instance of the 0x.js ZeroEx class.
|
||||
*/
|
||||
constructor(provider: Web3Provider, config?: ZeroExConfig) {
|
||||
assert.isWeb3Provider('provider', provider);
|
||||
if (!_.isUndefined(config)) {
|
||||
assert.doesConformToSchema('config', config, zeroExConfigSchema);
|
||||
}
|
||||
const artifactJSONs = _.values(artifacts);
|
||||
const abiArrays = _.map(artifactJSONs, artifact => artifact.abi);
|
||||
this._abiDecoder = new AbiDecoder(abiArrays);
|
||||
const gasPrice = _.isUndefined(config) ? undefined : config.gasPrice;
|
||||
const defaults = {
|
||||
gasPrice,
|
||||
};
|
||||
this._web3Wrapper = new Web3Wrapper(provider, defaults);
|
||||
this.token = new TokenWrapper(
|
||||
this._web3Wrapper,
|
||||
this._abiDecoder,
|
||||
this._getTokenTransferProxyAddressAsync.bind(this),
|
||||
);
|
||||
const exchageContractAddressIfExists = _.isUndefined(config) ? undefined : config.exchangeContractAddress;
|
||||
this.exchange = new ExchangeWrapper(
|
||||
this._web3Wrapper,
|
||||
this._abiDecoder,
|
||||
this.token,
|
||||
exchageContractAddressIfExists,
|
||||
);
|
||||
this.proxy = new TokenTransferProxyWrapper(
|
||||
this._web3Wrapper,
|
||||
this._getTokenTransferProxyAddressAsync.bind(this),
|
||||
);
|
||||
const tokenRegistryContractAddressIfExists = _.isUndefined(config) ?
|
||||
undefined :
|
||||
config.tokenRegistryContractAddress;
|
||||
this.tokenRegistry = new TokenRegistryWrapper(this._web3Wrapper, tokenRegistryContractAddressIfExists);
|
||||
const etherTokenContractAddressIfExists = _.isUndefined(config) ? undefined : config.etherTokenContractAddress;
|
||||
this.etherToken = new EtherTokenWrapper(this._web3Wrapper, this.token, etherTokenContractAddressIfExists);
|
||||
const orderWatcherConfig = _.isUndefined(config) ? undefined : config.orderWatcherConfig;
|
||||
this.orderStateWatcher = new OrderStateWatcher(
|
||||
this._web3Wrapper, this._abiDecoder, this.token, this.exchange, orderWatcherConfig,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Sets a new web3 provider for 0x.js. Updating the provider will stop all
|
||||
* subscriptions so you will need to re-subscribe to all events relevant to your app after this call.
|
||||
* @param provider The Web3Provider you would like the 0x.js library to use from now on.
|
||||
*/
|
||||
public async setProviderAsync(provider: Web3Provider) {
|
||||
this._web3Wrapper.setProvider(provider);
|
||||
await (this.exchange as any)._invalidateContractInstancesAsync();
|
||||
(this.tokenRegistry as any)._invalidateContractInstance();
|
||||
await (this.token as any)._invalidateContractInstancesAsync();
|
||||
(this.proxy as any)._invalidateContractInstance();
|
||||
(this.etherToken as any)._invalidateContractInstance();
|
||||
}
|
||||
/**
|
||||
* Get user Ethereum addresses available through the supplied web3 provider available for sending transactions.
|
||||
* @return An array of available user Ethereum addresses.
|
||||
*/
|
||||
public async getAvailableAddressesAsync(): Promise<string[]> {
|
||||
const availableAddresses = await this._web3Wrapper.getAvailableAddressesAsync();
|
||||
return availableAddresses;
|
||||
}
|
||||
/**
|
||||
* Signs an orderHash and returns it's elliptic curve signature.
|
||||
* This method currently supports TestRPC, Geth and Parity above and below V1.6.6
|
||||
* @param orderHash Hex encoded orderHash to sign.
|
||||
* @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address
|
||||
* must be available via the Web3.Provider supplied to 0x.js.
|
||||
* @return An object containing the Elliptic curve signature parameters generated by signing the orderHash.
|
||||
*/
|
||||
public async signOrderHashAsync(orderHash: string, signerAddress: string): Promise<ECSignature> {
|
||||
assert.isHexString('orderHash', orderHash);
|
||||
await assert.isSenderAddressAsync('signerAddress', signerAddress, this._web3Wrapper);
|
||||
|
||||
let msgHashHex;
|
||||
const nodeVersion = await this._web3Wrapper.getNodeVersionAsync();
|
||||
const isParityNode = utils.isParityNode(nodeVersion);
|
||||
const isTestRpc = utils.isTestRpc(nodeVersion);
|
||||
if (isParityNode || isTestRpc) {
|
||||
// Parity and TestRpc nodes add the personalMessage prefix itself
|
||||
msgHashHex = orderHash;
|
||||
} else {
|
||||
const orderHashBuff = ethUtil.toBuffer(orderHash);
|
||||
const msgHashBuff = ethUtil.hashPersonalMessage(orderHashBuff);
|
||||
msgHashHex = ethUtil.bufferToHex(msgHashBuff);
|
||||
}
|
||||
|
||||
const signature = await this._web3Wrapper.signTransactionAsync(signerAddress, msgHashHex);
|
||||
|
||||
// HACK: There is no consensus on whether the signatureHex string should be formatted as
|
||||
// v + r + s OR r + s + v, and different clients (even different versions of the same client)
|
||||
// return the signature params in different orders. In order to support all client implementations,
|
||||
// we parse the signature in both ways, and evaluate if either one is a valid signature.
|
||||
const validVParamValues = [27, 28];
|
||||
const ecSignatureVRS = signatureUtils.parseSignatureHexAsVRS(signature);
|
||||
if (_.includes(validVParamValues, ecSignatureVRS.v)) {
|
||||
const isValidVRSSignature = ZeroEx.isValidSignature(orderHash, ecSignatureVRS, signerAddress);
|
||||
if (isValidVRSSignature) {
|
||||
return ecSignatureVRS;
|
||||
}
|
||||
}
|
||||
|
||||
const ecSignatureRSV = signatureUtils.parseSignatureHexAsRSV(signature);
|
||||
if (_.includes(validVParamValues, ecSignatureRSV.v)) {
|
||||
const isValidRSVSignature = ZeroEx.isValidSignature(orderHash, ecSignatureRSV, signerAddress);
|
||||
if (isValidRSVSignature) {
|
||||
return ecSignatureRSV;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(ZeroExError.InvalidSignature);
|
||||
}
|
||||
/**
|
||||
* Waits for a transaction to be mined and returns the transaction receipt.
|
||||
* @param txHash Transaction hash
|
||||
* @param pollingIntervalMs How often (in ms) should we check if the transaction is mined.
|
||||
* @param timeoutMs How long (in ms) to poll for transaction mined until aborting.
|
||||
* @return Transaction receipt with decoded log args.
|
||||
*/
|
||||
public async awaitTransactionMinedAsync(
|
||||
txHash: string, pollingIntervalMs = 1000, timeoutMs?: number): Promise<TransactionReceiptWithDecodedLogs> {
|
||||
let timeoutExceeded = false;
|
||||
if (timeoutMs) {
|
||||
setTimeout(() => timeoutExceeded = true, timeoutMs);
|
||||
}
|
||||
|
||||
const txReceiptPromise = new Promise(
|
||||
(resolve: (receipt: TransactionReceiptWithDecodedLogs) => void, reject) => {
|
||||
const intervalId = intervalUtils.setAsyncExcludingInterval(async () => {
|
||||
if (timeoutExceeded) {
|
||||
intervalUtils.clearAsyncExcludingInterval(intervalId);
|
||||
return reject(ZeroExError.TransactionMiningTimeout);
|
||||
}
|
||||
|
||||
const transactionReceipt = await this._web3Wrapper.getTransactionReceiptAsync(txHash);
|
||||
if (!_.isNull(transactionReceipt)) {
|
||||
intervalUtils.clearAsyncExcludingInterval(intervalId);
|
||||
const logsWithDecodedArgs = _.map(
|
||||
transactionReceipt.logs,
|
||||
this._abiDecoder.tryToDecodeLogOrNoop.bind(this._abiDecoder),
|
||||
);
|
||||
const transactionReceiptWithDecodedLogArgs: TransactionReceiptWithDecodedLogs = {
|
||||
...transactionReceipt,
|
||||
logs: logsWithDecodedArgs,
|
||||
};
|
||||
resolve(transactionReceiptWithDecodedLogArgs);
|
||||
}
|
||||
}, pollingIntervalMs);
|
||||
});
|
||||
|
||||
return txReceiptPromise;
|
||||
}
|
||||
/*
|
||||
* HACK: `TokenWrapper` needs a token transfer proxy address. `TokenTransferProxy` address is fetched from
|
||||
* an `ExchangeWrapper`. `ExchangeWrapper` needs `TokenWrapper` to validate orders, creating a dependency cycle.
|
||||
* In order to break this - we create this function here and pass it as a parameter to the `TokenWrapper`
|
||||
* and `ProxyWrapper`.
|
||||
*/
|
||||
private async _getTokenTransferProxyAddressAsync(): Promise<string> {
|
||||
const tokenTransferProxyAddress = await (this.exchange as any)._getTokenTransferProxyAddressAsync();
|
||||
return tokenTransferProxyAddress;
|
||||
}
|
||||
}
|
14
packages/0x.js/src/artifacts.ts
Normal file
14
packages/0x.js/src/artifacts.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {Artifact} from './types';
|
||||
import * as TokenArtifact from './artifacts/Token.json';
|
||||
import * as ExchangeArtifact from './artifacts/Exchange.json';
|
||||
import * as EtherTokenArtifact from './artifacts/EtherToken.json';
|
||||
import * as TokenRegistryArtifact from './artifacts/TokenRegistry.json';
|
||||
import * as TokenTransferProxyArtifact from './artifacts/TokenTransferProxy.json';
|
||||
|
||||
export const artifacts = {
|
||||
TokenArtifact: TokenArtifact as any as Artifact,
|
||||
ExchangeArtifact: ExchangeArtifact as any as Artifact,
|
||||
EtherTokenArtifact: EtherTokenArtifact as any as Artifact,
|
||||
TokenRegistryArtifact: TokenRegistryArtifact as any as Artifact,
|
||||
TokenTransferProxyArtifact: TokenTransferProxyArtifact as any as Artifact,
|
||||
};
|
445
packages/0x.js/src/artifacts/EtherToken.json
Normal file
445
packages/0x.js/src/artifacts/EtherToken.json
Normal file
@@ -0,0 +1,445 @@
|
||||
{
|
||||
"contract_name": "EtherToken",
|
||||
"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": "",
|
||||
"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": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "withdraw",
|
||||
"outputs": [],
|
||||
"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": "",
|
||||
"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": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [],
|
||||
"name": "deposit",
|
||||
"outputs": [],
|
||||
"payable": true,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_spender",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "allowance",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"payable": true,
|
||||
"type": "fallback"
|
||||
},
|
||||
{
|
||||
"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": "0x6060604052341561000c57fe5b5b6107598061001c6000396000f300606060405236156100935763ffffffff60e060020a60003504166306fdde0381146100a4578063095ea7b31461013457806318160ddd1461016757806323b872dd146101895780632e1a7d4d146101c2578063313ce567146101d757806370a08231146101fd57806395d89b411461022b578063a9059cbb146102bb578063d0e30db0146102ee578063dd62ed3e146102f8575b6100a25b61009f61032c565b5b565b005b34156100ac57fe5b6100b461037b565b6040805160208082528351818301528351919283929083019185019080838382156100fa575b8051825260208311156100fa57601f1990920191602091820191016100da565b505050905090810190601f1680156101265780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b341561013c57fe5b610153600160a060020a03600435166024356103a3565b604080519115158252519081900360200190f35b341561016f57fe5b61017761040e565b60408051918252519081900360200190f35b341561019157fe5b610153600160a060020a0360043581169060243516604435610414565b604080519115158252519081900360200190f35b34156101ca57fe5b6100a2600435610537565b005b34156101df57fe5b6101e76105b8565b6040805160ff9092168252519081900360200190f35b341561020557fe5b610177600160a060020a03600435166105bd565b60408051918252519081900360200190f35b341561023357fe5b6100b46105dc565b6040805160208082528351818301528351919283929083019185019080838382156100fa575b8051825260208311156100fa57601f1990920191602091820191016100da565b505050905090810190601f1680156101265780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34156102c357fe5b610153600160a060020a03600435166024356105fd565b604080519115158252519081900360200190f35b6100a261032c565b005b341561030057fe5b610177600160a060020a03600435811690602435166106af565b60408051918252519081900360200190f35b600160a060020a03331660009081526020819052604090205461034f90346106dc565b600160a060020a03331660009081526020819052604090205560025461037590346106dc565b6002555b565b60408051808201909152600b815260a960020a6a22ba3432b9102a37b5b2b702602082015281565b600160a060020a03338116600081815260016020908152604080832094871680845294825280832086905580518681529051929493927f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925929181900390910190a35060015b92915050565b60025481565b600160a060020a03808416600081815260016020908152604080832033909516835293815283822054928252819052918220548390108015906104575750828110155b801561047d5750600160a060020a03841660009081526020819052604090205483810110155b1561052957600160a060020a03808516600090815260208190526040808220805487019055918716815220805484900390556000198110156104e757600160a060020a03808616600090815260016020908152604080832033909416835292905220805484900390555b83600160a060020a031685600160a060020a031660008051602061070e833981519152856040518082815260200191505060405180910390a36001915061052e565b600091505b5b509392505050565b600160a060020a03331660009081526020819052604090205461055a90826106f6565b600160a060020a03331660009081526020819052604090205560025461058090826106f6565b600255604051600160a060020a0333169082156108fc029083906000818181858888f1935050505015156105b45760006000fd5b5b50565b601281565b600160a060020a0381166000908152602081905260409020545b919050565b604080518082019091526004815260e360020a630ae8aa8902602082015281565b600160a060020a0333166000908152602081905260408120548290108015906106405750600160a060020a03831660009081526020819052604090205482810110155b156106a057600160a060020a03338116600081815260208181526040808320805488900390559387168083529184902080548701905583518681529351919360008051602061070e833981519152929081900390910190a3506001610408565b506000610408565b5b92915050565b600160a060020a038083166000908152600160209081526040808320938516835292905220545b92915050565b6000828201838110156106eb57fe5b8091505b5092915050565b60008282111561070257fe5b508082035b929150505600ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa165627a7a72305820ec42c469bb8ddd5de28c55b9cc393c812397c063a57fb88926e3f6de246318b70029",
|
||||
"networks": {
|
||||
"1": {
|
||||
"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": 1502488087000,
|
||||
"address": "0x2956356cd2a2bf3202f771f50d3d14a367b48070"
|
||||
},
|
||||
"3": {
|
||||
"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": 1506602007000,
|
||||
"address": "0xc00fd9820cd2898cc4c054b7bf142de637ad129a"
|
||||
},
|
||||
"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": 1502391794392,
|
||||
"address": "0x05d090b51c40b020eab3bfcb6a2dff130df22e9c"
|
||||
},
|
||||
"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": 1503318938233,
|
||||
"address": "0x48bacb9266a570d521063ef5dd96e61686dbe788"
|
||||
}
|
||||
},
|
||||
"schema_version": "0.0.5",
|
||||
"updated_at": 1503318938233
|
||||
}
|
1130
packages/0x.js/src/artifacts/Exchange.json
Normal file
1130
packages/0x.js/src/artifacts/Exchange.json
Normal file
File diff suppressed because one or more lines are too long
176
packages/0x.js/src/artifacts/Token.json
Normal file
176
packages/0x.js/src/artifacts/Token.json
Normal file
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"contract_name": "Token",
|
||||
"abi": [
|
||||
{
|
||||
"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": "supply",
|
||||
"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": "_owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "balanceOf",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "balance",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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": "0x6060604052341561000c57fe5b5b6101e08061001c6000396000f3006060604052361561005c5763ffffffff60e060020a600035041663095ea7b3811461005e57806318160ddd1461009157806323b872dd146100b357806370a08231146100ec578063a9059cbb1461005e578063dd62ed3e1461014d575bfe5b341561006657fe5b61007d600160a060020a0360043516602435610181565b604080519115158252519081900360200190f35b341561009957fe5b6100a161018a565b60408051918252519081900360200190f35b34156100bb57fe5b61007d600160a060020a0360043581169060243516604435610190565b604080519115158252519081900360200190f35b34156100f457fe5b6100a1600160a060020a036004351661019a565b60408051918252519081900360200190f35b341561006657fe5b61007d600160a060020a0360043516602435610181565b604080519115158252519081900360200190f35b341561015557fe5b6100a1600160a060020a0360043581169060243516610181565b60408051918252519081900360200190f35b60005b92915050565b60005b90565b60005b9392505050565b60005b919050565b60005b92915050565b60005b929150505600a165627a7a723058202e3f7ac17048343c0d0ea24fccb64620577374eeeed61539e543df4025d7d0db0029",
|
||||
"networks": {},
|
||||
"schema_version": "0.0.5",
|
||||
"updated_at": 1503317882695
|
||||
}
|
1211
packages/0x.js/src/artifacts/TokenRegistry.json
Normal file
1211
packages/0x.js/src/artifacts/TokenRegistry.json
Normal file
File diff suppressed because one or more lines are too long
174
packages/0x.js/src/artifacts/TokenTransferProxy.json
Normal file
174
packages/0x.js/src/artifacts/TokenTransferProxy.json
Normal file
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"contract_name": "TokenTransferProxy",
|
||||
"abi": [
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transferFrom",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "target",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "addAuthorizedAddress",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "authorities",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "target",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "removeAuthorizedAddress",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "owner",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "authorized",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "getAuthorizedAddresses",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "address[]"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "newOwner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "transferOwnership",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "target",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "caller",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "LogAuthorizedAddressAdded",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "target",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "caller",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "LogAuthorizedAddressRemoved",
|
||||
"type": "event"
|
||||
}
|
||||
],
|
||||
"unlinked_binary": "0x60606040525b60008054600160a060020a03191633600160a060020a03161790555b5b6106e6806100316000396000f300606060405236156100725763ffffffff60e060020a60003504166315dacbea811461007457806342f1181e146100b3578063494503d4146100d157806370712939146101005780638da5cb5b1461011e578063b91816111461014a578063d39de6e91461017a578063f2fde38b146101e5575bfe5b341561007c57fe5b61009f600160a060020a0360043581169060243581169060443516606435610203565b604080519115158252519081900360200190f35b34156100bb57fe5b6100cf600160a060020a03600435166102ae565b005b34156100d957fe5b6100e4600435610390565b60408051600160a060020a039092168252519081900360200190f35b341561010857fe5b6100cf600160a060020a03600435166103c2565b005b341561012657fe5b6100e461055a565b60408051600160a060020a039092168252519081900360200190f35b341561015257fe5b61009f600160a060020a0360043516610569565b604080519115158252519081900360200190f35b341561018257fe5b61018a61057e565b60408051602080825283518183015283519192839290830191858101910280838382156101d2575b8051825260208311156101d257601f1990920191602091820191016101b2565b5050509050019250505060405180910390f35b34156101ed57fe5b6100cf600160a060020a03600435166105e7565b005b600160a060020a03331660009081526001602052604081205460ff16151561022b5760006000fd5b6040805160006020918201819052825160e060020a6323b872dd028152600160a060020a0388811660048301528781166024830152604482018790529351938916936323b872dd9360648084019491938390030190829087803b151561028d57fe5b6102c65a03f1151561029b57fe5b5050604051519150505b5b949350505050565b60005433600160a060020a039081169116146102ca5760006000fd5b600160a060020a038116600090815260016020526040902054819060ff16156102f35760006000fd5b600160a060020a0382166000908152600160208190526040909120805460ff191682179055600280549091810161032a8382610633565b916000526020600020900160005b81546101009190910a600160a060020a0381810219909216868316918202179092556040513390911692507f94bb87f4c15c4587ff559a7584006fa01ddf9299359be6b512b94527aa961aca90600090a35b5b505b50565b600280548290811061039e57fe5b906000526020600020900160005b915054906101000a9004600160a060020a031681565b6000805433600160a060020a039081169116146103df5760006000fd5b600160a060020a038216600090815260016020526040902054829060ff1615156104095760006000fd5b600160a060020a0383166000908152600160205260408120805460ff1916905591505b6002548210156105195782600160a060020a031660028381548110151561044f57fe5b906000526020600020900160005b9054906101000a9004600160a060020a0316600160a060020a0316141561050d5760028054600019810190811061049057fe5b906000526020600020900160005b9054906101000a9004600160a060020a03166002838154811015156104bf57fe5b906000526020600020900160005b6101000a815481600160a060020a030219169083600160a060020a0316021790555060016002818180549050039150816105079190610633565b50610519565b5b60019091019061042c565b604051600160a060020a0333811691908516907ff5b347a1e40749dd050f5f07fbdbeb7e3efa9756903044dd29401fd1d4bb4a1c90600090a35b5b505b5050565b600054600160a060020a031681565b60016020526000908152604090205460ff1681565b610586610687565b60028054806020026020016040519081016040528092919081815260200182805480156105dc57602002820191906000526020600020905b8154600160a060020a031681526001909101906020018083116105be575b505050505090505b90565b60005433600160a060020a039081169116146106035760006000fd5b600160a060020a0381161561038d5760008054600160a060020a031916600160a060020a0383161790555b5b5b50565b81548183558181151161055357600083815260209020610553918101908301610699565b5b505050565b81548183558181151161055357600083815260209020610553918101908301610699565b5b505050565b60408051602081019091526000815290565b6105e491905b808211156106b3576000815560010161069f565b5090565b905600a165627a7a72305820d2924957bb88a128789172e164d874fe5445218fc2dde2f5eb265839a1f341a20029",
|
||||
"networks": {},
|
||||
"schema_version": "0.0.5",
|
||||
"updated_at": 1503318938227
|
||||
}
|
11
packages/0x.js/src/bignumber_config.ts
Normal file
11
packages/0x.js/src/bignumber_config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import 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,
|
||||
});
|
||||
},
|
||||
};
|
80
packages/0x.js/src/contract.ts
Normal file
80
packages/0x.js/src/contract.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import * as Web3 from 'web3';
|
||||
import * as _ from 'lodash';
|
||||
import promisify = require('es6-promisify');
|
||||
import {SchemaValidator, schemas} from '0x-json-schemas';
|
||||
import {AbiType} from './types';
|
||||
|
||||
export class Contract implements Web3.ContractInstance {
|
||||
public address: string;
|
||||
public abi: Web3.ContractAbi;
|
||||
private contract: Web3.ContractInstance;
|
||||
private defaults: Partial<Web3.TxData>;
|
||||
private validator: SchemaValidator;
|
||||
// This class instance is going to be populated with functions and events depending on the ABI
|
||||
// and we don't know their types in advance
|
||||
[name: string]: any;
|
||||
constructor(web3ContractInstance: Web3.ContractInstance, defaults: Partial<Web3.TxData>) {
|
||||
this.contract = web3ContractInstance;
|
||||
this.address = web3ContractInstance.address;
|
||||
this.abi = web3ContractInstance.abi;
|
||||
this.defaults = defaults;
|
||||
this.populateEvents();
|
||||
this.populateFunctions();
|
||||
this.validator = new SchemaValidator();
|
||||
}
|
||||
private populateFunctions(): void {
|
||||
const functionsAbi = _.filter(this.abi, abiPart => abiPart.type === AbiType.Function);
|
||||
_.forEach(functionsAbi, (functionAbi: Web3.MethodAbi) => {
|
||||
if (functionAbi.constant) {
|
||||
const cbStyleCallFunction = this.contract[functionAbi.name].call;
|
||||
this[functionAbi.name] = {
|
||||
callAsync: promisify(cbStyleCallFunction, this.contract),
|
||||
};
|
||||
} else {
|
||||
const cbStyleFunction = this.contract[functionAbi.name];
|
||||
const cbStyleEstimateGasFunction = this.contract[functionAbi.name].estimateGas;
|
||||
this[functionAbi.name] = {
|
||||
estimateGasAsync: promisify(cbStyleEstimateGasFunction, this.contract),
|
||||
sendTransactionAsync: this.promisifyWithDefaultParams(cbStyleFunction),
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
private populateEvents(): void {
|
||||
const eventsAbi = _.filter(this.abi, abiPart => abiPart.type === AbiType.Event);
|
||||
_.forEach(eventsAbi, (eventAbi: Web3.EventAbi) => {
|
||||
this[eventAbi.name] = this.contract[eventAbi.name];
|
||||
});
|
||||
}
|
||||
private promisifyWithDefaultParams(fn: (...args: any[]) => void): (...args: any[]) => Promise<any> {
|
||||
const promisifiedWithDefaultParams = (...args: any[]) => {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const lastArg = args[args.length - 1];
|
||||
let txData: Partial<Web3.TxData> = {};
|
||||
if (this.isTxData(lastArg)) {
|
||||
txData = args.pop();
|
||||
}
|
||||
txData = {
|
||||
...this.defaults,
|
||||
...txData,
|
||||
};
|
||||
const callback = (err: Error, data: any) => {
|
||||
if (_.isNull(err)) {
|
||||
resolve(data);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
args.push(txData);
|
||||
args.push(callback);
|
||||
fn.apply(this.contract, args);
|
||||
});
|
||||
return promise;
|
||||
};
|
||||
return promisifiedWithDefaultParams;
|
||||
}
|
||||
private isTxData(lastArg: any): boolean {
|
||||
const isValid = this.validator.isValid(lastArg, schemas.txDataSchema);
|
||||
return isValid;
|
||||
}
|
||||
}
|
152
packages/0x.js/src/contract_wrappers/contract_wrapper.ts
Normal file
152
packages/0x.js/src/contract_wrappers/contract_wrapper.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as Web3 from 'web3';
|
||||
import {BlockAndLogStreamer, Block} from 'ethereumjs-blockstream';
|
||||
import {Web3Wrapper} from '../web3_wrapper';
|
||||
import {AbiDecoder} from '../utils/abi_decoder';
|
||||
import {
|
||||
ZeroExError,
|
||||
InternalZeroExError,
|
||||
Artifact,
|
||||
LogWithDecodedArgs,
|
||||
RawLog,
|
||||
ContractEvents,
|
||||
SubscriptionOpts,
|
||||
IndexedFilterValues,
|
||||
EventCallback,
|
||||
BlockParamLiteral,
|
||||
ContractEventArgs,
|
||||
} from '../types';
|
||||
import {constants} from '../utils/constants';
|
||||
import {intervalUtils} from '../utils/interval_utils';
|
||||
import {filterUtils} from '../utils/filter_utils';
|
||||
|
||||
export class ContractWrapper {
|
||||
protected _web3Wrapper: Web3Wrapper;
|
||||
private _abiDecoder?: AbiDecoder;
|
||||
private _blockAndLogStreamer: BlockAndLogStreamer|undefined;
|
||||
private _blockAndLogStreamInterval: NodeJS.Timer;
|
||||
private _filters: {[filterToken: string]: Web3.FilterObject};
|
||||
private _filterCallbacks: {[filterToken: string]: EventCallback<ContractEventArgs>};
|
||||
private _onLogAddedSubscriptionToken: string|undefined;
|
||||
private _onLogRemovedSubscriptionToken: string|undefined;
|
||||
constructor(web3Wrapper: Web3Wrapper, abiDecoder?: AbiDecoder) {
|
||||
this._web3Wrapper = web3Wrapper;
|
||||
this._abiDecoder = abiDecoder;
|
||||
this._filters = {};
|
||||
this._filterCallbacks = {};
|
||||
this._blockAndLogStreamer = undefined;
|
||||
this._onLogAddedSubscriptionToken = undefined;
|
||||
this._onLogRemovedSubscriptionToken = undefined;
|
||||
}
|
||||
/**
|
||||
* Cancels all existing subscriptions
|
||||
*/
|
||||
public unsubscribeAll(): void {
|
||||
const filterTokens = _.keys(this._filterCallbacks);
|
||||
_.each(filterTokens, filterToken => {
|
||||
this._unsubscribe(filterToken);
|
||||
});
|
||||
}
|
||||
protected _unsubscribe(filterToken: string, err?: Error): void {
|
||||
if (_.isUndefined(this._filters[filterToken])) {
|
||||
throw new Error(ZeroExError.SubscriptionNotFound);
|
||||
}
|
||||
if (!_.isUndefined(err)) {
|
||||
const callback = this._filterCallbacks[filterToken];
|
||||
callback(err, undefined);
|
||||
}
|
||||
delete this._filters[filterToken];
|
||||
delete this._filterCallbacks[filterToken];
|
||||
if (_.isEmpty(this._filters)) {
|
||||
this._stopBlockAndLogStream();
|
||||
}
|
||||
}
|
||||
protected _subscribe<ArgsType extends ContractEventArgs>(
|
||||
address: string, eventName: ContractEvents, indexFilterValues: IndexedFilterValues, abi: Web3.ContractAbi,
|
||||
callback: EventCallback<ArgsType>): string {
|
||||
const filter = filterUtils.getFilter(address, eventName, indexFilterValues, abi);
|
||||
if (_.isUndefined(this._blockAndLogStreamer)) {
|
||||
this._startBlockAndLogStream();
|
||||
}
|
||||
const filterToken = filterUtils.generateUUID();
|
||||
this._filters[filterToken] = filter;
|
||||
this._filterCallbacks[filterToken] = callback;
|
||||
return filterToken;
|
||||
}
|
||||
protected async _getLogsAsync<ArgsType extends ContractEventArgs>(
|
||||
address: string, eventName: ContractEvents, subscriptionOpts: SubscriptionOpts,
|
||||
indexFilterValues: IndexedFilterValues, abi: Web3.ContractAbi): Promise<Array<LogWithDecodedArgs<ArgsType>>> {
|
||||
const filter = filterUtils.getFilter(address, eventName, indexFilterValues, abi, subscriptionOpts);
|
||||
const logs = await this._web3Wrapper.getLogsAsync(filter);
|
||||
const logsWithDecodedArguments = _.map(logs, this._tryToDecodeLogOrNoop.bind(this));
|
||||
return logsWithDecodedArguments;
|
||||
}
|
||||
protected _tryToDecodeLogOrNoop<ArgsType extends ContractEventArgs>(
|
||||
log: Web3.LogEntry): LogWithDecodedArgs<ArgsType>|RawLog {
|
||||
if (_.isUndefined(this._abiDecoder)) {
|
||||
throw new Error(InternalZeroExError.NoAbiDecoder);
|
||||
}
|
||||
const logWithDecodedArgs = this._abiDecoder.tryToDecodeLogOrNoop(log);
|
||||
return logWithDecodedArgs;
|
||||
}
|
||||
protected async _instantiateContractIfExistsAsync<ContractType extends Web3.ContractInstance>(
|
||||
artifact: Artifact, addressIfExists?: string): Promise<ContractType> {
|
||||
const contractInstance =
|
||||
await this._web3Wrapper.getContractInstanceFromArtifactAsync<ContractType>(artifact, addressIfExists);
|
||||
return contractInstance;
|
||||
}
|
||||
private _onLogStateChanged<ArgsType extends ContractEventArgs>(removed: boolean, log: Web3.LogEntry): void {
|
||||
_.forEach(this._filters, (filter: Web3.FilterObject, filterToken: string) => {
|
||||
if (filterUtils.matchesFilter(log, filter)) {
|
||||
const decodedLog = this._tryToDecodeLogOrNoop(log) as LogWithDecodedArgs<ArgsType>;
|
||||
const logEvent = {
|
||||
...decodedLog,
|
||||
removed,
|
||||
};
|
||||
this._filterCallbacks[filterToken](null, logEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
private _startBlockAndLogStream(): void {
|
||||
this._blockAndLogStreamer = new BlockAndLogStreamer(
|
||||
this._web3Wrapper.getBlockAsync.bind(this._web3Wrapper),
|
||||
this._web3Wrapper.getLogsAsync.bind(this._web3Wrapper),
|
||||
);
|
||||
const catchAllLogFilter = {};
|
||||
this._blockAndLogStreamer.addLogFilter(catchAllLogFilter);
|
||||
this._blockAndLogStreamInterval = intervalUtils.setAsyncExcludingInterval(
|
||||
this._reconcileBlockAsync.bind(this), constants.DEFAULT_BLOCK_POLLING_INTERVAL,
|
||||
);
|
||||
let removed = false;
|
||||
this._onLogAddedSubscriptionToken = this._blockAndLogStreamer.subscribeToOnLogAdded(
|
||||
this._onLogStateChanged.bind(this, removed),
|
||||
);
|
||||
removed = true;
|
||||
this._onLogRemovedSubscriptionToken = this._blockAndLogStreamer.subscribeToOnLogRemoved(
|
||||
this._onLogStateChanged.bind(this, removed),
|
||||
);
|
||||
}
|
||||
private _stopBlockAndLogStream(): void {
|
||||
(this._blockAndLogStreamer as BlockAndLogStreamer).unsubscribeFromOnLogAdded(
|
||||
this._onLogAddedSubscriptionToken as string);
|
||||
(this._blockAndLogStreamer as BlockAndLogStreamer).unsubscribeFromOnLogRemoved(
|
||||
this._onLogRemovedSubscriptionToken as string);
|
||||
intervalUtils.clearAsyncExcludingInterval(this._blockAndLogStreamInterval);
|
||||
delete this._blockAndLogStreamer;
|
||||
}
|
||||
private async _reconcileBlockAsync(): Promise<void> {
|
||||
try {
|
||||
const latestBlock = await this._web3Wrapper.getBlockAsync(BlockParamLiteral.Latest);
|
||||
// We need to coerce to Block type cause Web3.Block includes types for mempool blocks
|
||||
if (!_.isUndefined(this._blockAndLogStreamer)) {
|
||||
// If we clear the interval while fetching the block - this._blockAndLogStreamer will be undefined
|
||||
this._blockAndLogStreamer.reconcileNewBlock(latestBlock as any as Block);
|
||||
}
|
||||
} catch (err) {
|
||||
const filterTokens = _.keys(this._filterCallbacks);
|
||||
_.each(filterTokens, filterToken => {
|
||||
this._unsubscribe(filterToken, err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
87
packages/0x.js/src/contract_wrappers/ether_token_wrapper.ts
Normal file
87
packages/0x.js/src/contract_wrappers/ether_token_wrapper.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import * as _ from 'lodash';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {Web3Wrapper} from '../web3_wrapper';
|
||||
import {ContractWrapper} from './contract_wrapper';
|
||||
import {TokenWrapper} from './token_wrapper';
|
||||
import {EtherTokenContract, ZeroExError} from '../types';
|
||||
import {assert} from '../utils/assert';
|
||||
import {artifacts} from '../artifacts';
|
||||
|
||||
/**
|
||||
* This class includes all the functionality related to interacting with a wrapped Ether ERC20 token contract.
|
||||
* The caller can convert ETH into the equivalent number of wrapped ETH ERC20 tokens and back.
|
||||
*/
|
||||
export class EtherTokenWrapper extends ContractWrapper {
|
||||
private _etherTokenContractIfExists?: EtherTokenContract;
|
||||
private _tokenWrapper: TokenWrapper;
|
||||
private _contractAddressIfExists?: string;
|
||||
constructor(web3Wrapper: Web3Wrapper, tokenWrapper: TokenWrapper, contractAddressIfExists?: string) {
|
||||
super(web3Wrapper);
|
||||
this._tokenWrapper = tokenWrapper;
|
||||
this._contractAddressIfExists = contractAddressIfExists;
|
||||
}
|
||||
/**
|
||||
* Deposit ETH into the Wrapped ETH smart contract and issues the equivalent number of wrapped ETH tokens
|
||||
* to the depositor address. These wrapped ETH tokens can be used in 0x trades and are redeemable for 1-to-1
|
||||
* for ETH.
|
||||
* @param amountInWei Amount of ETH in Wei the caller wishes to deposit.
|
||||
* @param depositor The hex encoded user Ethereum address that would like to make the deposit.
|
||||
* @return Transaction hash.
|
||||
*/
|
||||
public async depositAsync(amountInWei: BigNumber, depositor: string): Promise<string> {
|
||||
assert.isValidBaseUnitAmount('amountInWei', amountInWei);
|
||||
await assert.isSenderAddressAsync('depositor', depositor, this._web3Wrapper);
|
||||
|
||||
const ethBalanceInWei = await this._web3Wrapper.getBalanceInWeiAsync(depositor);
|
||||
assert.assert(ethBalanceInWei.gte(amountInWei), ZeroExError.InsufficientEthBalanceForDeposit);
|
||||
|
||||
const wethContract = await this._getEtherTokenContractAsync();
|
||||
const txHash = await wethContract.deposit.sendTransactionAsync({
|
||||
from: depositor,
|
||||
value: amountInWei,
|
||||
});
|
||||
return txHash;
|
||||
}
|
||||
/**
|
||||
* Withdraw ETH to the withdrawer's address from the wrapped ETH smart contract in exchange for the
|
||||
* equivalent number of wrapped ETH tokens.
|
||||
* @param amountInWei Amount of ETH in Wei the caller wishes to withdraw.
|
||||
* @param withdrawer The hex encoded user Ethereum address that would like to make the withdrawl.
|
||||
* @return Transaction hash.
|
||||
*/
|
||||
public async withdrawAsync(amountInWei: BigNumber, withdrawer: string): Promise<string> {
|
||||
assert.isValidBaseUnitAmount('amountInWei', amountInWei);
|
||||
await assert.isSenderAddressAsync('withdrawer', withdrawer, this._web3Wrapper);
|
||||
|
||||
const wethContractAddress = await this.getContractAddressAsync();
|
||||
const WETHBalanceInBaseUnits = await this._tokenWrapper.getBalanceAsync(wethContractAddress, withdrawer);
|
||||
assert.assert(WETHBalanceInBaseUnits.gte(amountInWei), ZeroExError.InsufficientWEthBalanceForWithdrawal);
|
||||
|
||||
const wethContract = await this._getEtherTokenContractAsync();
|
||||
const txHash = await wethContract.withdraw.sendTransactionAsync(amountInWei, {
|
||||
from: withdrawer,
|
||||
});
|
||||
return txHash;
|
||||
}
|
||||
/**
|
||||
* Retrieves the Wrapped Ether token contract address
|
||||
* @return The Wrapped Ether token contract address
|
||||
*/
|
||||
public async getContractAddressAsync(): Promise<string> {
|
||||
const wethContract = await this._getEtherTokenContractAsync();
|
||||
return wethContract.address;
|
||||
}
|
||||
private _invalidateContractInstance(): void {
|
||||
delete this._etherTokenContractIfExists;
|
||||
}
|
||||
private async _getEtherTokenContractAsync(): Promise<EtherTokenContract> {
|
||||
if (!_.isUndefined(this._etherTokenContractIfExists)) {
|
||||
return this._etherTokenContractIfExists;
|
||||
}
|
||||
const contractInstance = await this._instantiateContractIfExistsAsync<EtherTokenContract>(
|
||||
artifacts.EtherTokenArtifact, this._contractAddressIfExists,
|
||||
);
|
||||
this._etherTokenContractIfExists = contractInstance as EtherTokenContract;
|
||||
return this._etherTokenContractIfExists;
|
||||
}
|
||||
}
|
866
packages/0x.js/src/contract_wrappers/exchange_wrapper.ts
Normal file
866
packages/0x.js/src/contract_wrappers/exchange_wrapper.ts
Normal file
@@ -0,0 +1,866 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as Web3 from 'web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {schemas} from '0x-json-schemas';
|
||||
import {Web3Wrapper} from '../web3_wrapper';
|
||||
import {
|
||||
ECSignature,
|
||||
ExchangeContract,
|
||||
ExchangeContractErrCodes,
|
||||
ExchangeContractErrs,
|
||||
ZeroExError,
|
||||
OrderValues,
|
||||
OrderAddresses,
|
||||
Order,
|
||||
SignedOrder,
|
||||
ExchangeEvents,
|
||||
SubscriptionOpts,
|
||||
IndexedFilterValues,
|
||||
OrderCancellationRequest,
|
||||
OrderFillRequest,
|
||||
LogErrorContractEventArgs,
|
||||
LogFillContractEventArgs,
|
||||
LogCancelContractEventArgs,
|
||||
LogWithDecodedArgs,
|
||||
MethodOpts,
|
||||
ValidateOrderFillableOpts,
|
||||
OrderTransactionOpts,
|
||||
RawLog,
|
||||
EventCallback,
|
||||
ExchangeContractEventArgs,
|
||||
DecodedLogArgs,
|
||||
} from '../types';
|
||||
import {assert} from '../utils/assert';
|
||||
import {utils} from '../utils/utils';
|
||||
import {OrderValidationUtils} from '../utils/order_validation_utils';
|
||||
import {ContractWrapper} from './contract_wrapper';
|
||||
import {TokenWrapper} from './token_wrapper';
|
||||
import {decorators} from '../utils/decorators';
|
||||
import {AbiDecoder} from '../utils/abi_decoder';
|
||||
import {ExchangeTransferSimulator} from '../utils/exchange_transfer_simulator';
|
||||
import {artifacts} from '../artifacts';
|
||||
|
||||
const SHOULD_VALIDATE_BY_DEFAULT = true;
|
||||
|
||||
/**
|
||||
* This class includes all the functionality related to calling methods and subscribing to
|
||||
* events of the 0x Exchange smart contract.
|
||||
*/
|
||||
export class ExchangeWrapper extends ContractWrapper {
|
||||
private _exchangeContractIfExists?: ExchangeContract;
|
||||
private _orderValidationUtils: OrderValidationUtils;
|
||||
private _tokenWrapper: TokenWrapper;
|
||||
private _exchangeContractErrCodesToMsg = {
|
||||
[ExchangeContractErrCodes.ERROR_FILL_EXPIRED]: ExchangeContractErrs.OrderFillExpired,
|
||||
[ExchangeContractErrCodes.ERROR_CANCEL_EXPIRED]: ExchangeContractErrs.OrderFillExpired,
|
||||
[ExchangeContractErrCodes.ERROR_FILL_NO_VALUE]: ExchangeContractErrs.OrderRemainingFillAmountZero,
|
||||
[ExchangeContractErrCodes.ERROR_CANCEL_NO_VALUE]: ExchangeContractErrs.OrderRemainingFillAmountZero,
|
||||
[ExchangeContractErrCodes.ERROR_FILL_TRUNCATION]: ExchangeContractErrs.OrderFillRoundingError,
|
||||
[ExchangeContractErrCodes.ERROR_FILL_BALANCE_ALLOWANCE]: ExchangeContractErrs.FillBalanceAllowanceError,
|
||||
};
|
||||
private _contractAddressIfExists?: string;
|
||||
private static _getOrderAddressesAndValues(order: Order): [OrderAddresses, OrderValues] {
|
||||
const orderAddresses: OrderAddresses = [
|
||||
order.maker,
|
||||
order.taker,
|
||||
order.makerTokenAddress,
|
||||
order.takerTokenAddress,
|
||||
order.feeRecipient,
|
||||
];
|
||||
const orderValues: OrderValues = [
|
||||
order.makerTokenAmount,
|
||||
order.takerTokenAmount,
|
||||
order.makerFee,
|
||||
order.takerFee,
|
||||
order.expirationUnixTimestampSec,
|
||||
order.salt,
|
||||
];
|
||||
return [orderAddresses, orderValues];
|
||||
}
|
||||
constructor(web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder,
|
||||
tokenWrapper: TokenWrapper, contractAddressIfExists?: string) {
|
||||
super(web3Wrapper, abiDecoder);
|
||||
this._tokenWrapper = tokenWrapper;
|
||||
this._orderValidationUtils = new OrderValidationUtils(tokenWrapper, this);
|
||||
this._contractAddressIfExists = contractAddressIfExists;
|
||||
}
|
||||
/**
|
||||
* Returns the unavailable takerAmount of an order. Unavailable amount is defined as the total
|
||||
* amount that has been filled or cancelled. The remaining takerAmount can be calculated by
|
||||
* subtracting the unavailable amount from the total order takerAmount.
|
||||
* @param orderHash The hex encoded orderHash for which you would like to retrieve the
|
||||
* unavailable takerAmount.
|
||||
* @param methodOpts Optional arguments this method accepts.
|
||||
* @return The amount of the order (in taker tokens) that has either been filled or canceled.
|
||||
*/
|
||||
public async getUnavailableTakerAmountAsync(orderHash: string,
|
||||
methodOpts?: MethodOpts): Promise<BigNumber> {
|
||||
assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema);
|
||||
|
||||
const exchangeContract = await this._getExchangeContractAsync();
|
||||
const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
|
||||
let unavailableTakerTokenAmount = await exchangeContract.getUnavailableTakerTokenAmount.callAsync(
|
||||
orderHash, defaultBlock,
|
||||
);
|
||||
// Wrap BigNumbers returned from web3 with our own (later) version of BigNumber
|
||||
unavailableTakerTokenAmount = new BigNumber(unavailableTakerTokenAmount);
|
||||
return unavailableTakerTokenAmount;
|
||||
}
|
||||
/**
|
||||
* Retrieve the takerAmount of an order that has already been filled.
|
||||
* @param orderHash The hex encoded orderHash for which you would like to retrieve the filled takerAmount.
|
||||
* @param methodOpts Optional arguments this method accepts.
|
||||
* @return The amount of the order (in taker tokens) that has already been filled.
|
||||
*/
|
||||
public async getFilledTakerAmountAsync(orderHash: string, methodOpts?: MethodOpts): Promise<BigNumber> {
|
||||
assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema);
|
||||
|
||||
const exchangeContract = await this._getExchangeContractAsync();
|
||||
const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
|
||||
let fillAmountInBaseUnits = await exchangeContract.filled.callAsync(orderHash, defaultBlock);
|
||||
// Wrap BigNumbers returned from web3 with our own (later) version of BigNumber
|
||||
fillAmountInBaseUnits = new BigNumber(fillAmountInBaseUnits);
|
||||
return fillAmountInBaseUnits;
|
||||
}
|
||||
/**
|
||||
* Retrieve the takerAmount of an order that has been cancelled.
|
||||
* @param orderHash The hex encoded orderHash for which you would like to retrieve the
|
||||
* cancelled takerAmount.
|
||||
* @param methodOpts Optional arguments this method accepts.
|
||||
* @return The amount of the order (in taker tokens) that has been cancelled.
|
||||
*/
|
||||
public async getCanceledTakerAmountAsync(orderHash: string, methodOpts?: MethodOpts): Promise<BigNumber> {
|
||||
assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema);
|
||||
|
||||
const exchangeContract = await this._getExchangeContractAsync();
|
||||
const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
|
||||
let cancelledAmountInBaseUnits = await exchangeContract.cancelled.callAsync(orderHash, defaultBlock);
|
||||
// Wrap BigNumbers returned from web3 with our own (later) version of BigNumber
|
||||
cancelledAmountInBaseUnits = new BigNumber(cancelledAmountInBaseUnits);
|
||||
return cancelledAmountInBaseUnits;
|
||||
}
|
||||
/**
|
||||
* Fills a signed order with an amount denominated in baseUnits of the taker token.
|
||||
* Since the order in which transactions are included in the next block is indeterminate, race-conditions
|
||||
* could arise where a users balance or allowance changes before the fillOrder executes. Because of this,
|
||||
* we allow you to specify `shouldThrowOnInsufficientBalanceOrAllowance`.
|
||||
* If false, the smart contract will not throw if the parties
|
||||
* do not have sufficient balances/allowances, preserving gas costs. Setting it to true forgoes this check
|
||||
* and causes the smart contract to throw (using all the gas supplied) instead.
|
||||
* @param signedOrder An object that conforms to the SignedOrder interface.
|
||||
* @param fillTakerTokenAmount The amount of the order (in taker tokens baseUnits) that
|
||||
* you wish to fill.
|
||||
* @param shouldThrowOnInsufficientBalanceOrAllowance Whether or not you wish for the contract call to throw
|
||||
* if upon execution the tokens cannot be transferred.
|
||||
* @param takerAddress The user Ethereum address who would like to fill this order.
|
||||
* Must be available via the supplied Web3.Provider
|
||||
* passed to 0x.js.
|
||||
* @param orderTransactionOpts Optional arguments this method accepts.
|
||||
* @return Transaction hash.
|
||||
*/
|
||||
@decorators.contractCallErrorHandler
|
||||
public async fillOrderAsync(signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber,
|
||||
shouldThrowOnInsufficientBalanceOrAllowance: boolean,
|
||||
takerAddress: string,
|
||||
orderTransactionOpts?: OrderTransactionOpts): Promise<string> {
|
||||
assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
|
||||
assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount);
|
||||
assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance);
|
||||
await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
|
||||
|
||||
const exchangeInstance = await this._getExchangeContractAsync();
|
||||
const shouldValidate = _.isUndefined(orderTransactionOpts) ?
|
||||
SHOULD_VALIDATE_BY_DEFAULT :
|
||||
orderTransactionOpts.shouldValidate;
|
||||
if (shouldValidate) {
|
||||
const zrxTokenAddress = await this.getZRXTokenAddressAsync();
|
||||
const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
|
||||
await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
|
||||
exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress);
|
||||
}
|
||||
|
||||
const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(signedOrder);
|
||||
|
||||
const gas = await exchangeInstance.fillOrder.estimateGasAsync(
|
||||
orderAddresses,
|
||||
orderValues,
|
||||
fillTakerTokenAmount,
|
||||
shouldThrowOnInsufficientBalanceOrAllowance,
|
||||
signedOrder.ecSignature.v,
|
||||
signedOrder.ecSignature.r,
|
||||
signedOrder.ecSignature.s,
|
||||
{
|
||||
from: takerAddress,
|
||||
},
|
||||
);
|
||||
const txHash: string = await exchangeInstance.fillOrder.sendTransactionAsync(
|
||||
orderAddresses,
|
||||
orderValues,
|
||||
fillTakerTokenAmount,
|
||||
shouldThrowOnInsufficientBalanceOrAllowance,
|
||||
signedOrder.ecSignature.v,
|
||||
signedOrder.ecSignature.r,
|
||||
signedOrder.ecSignature.s,
|
||||
{
|
||||
from: takerAddress,
|
||||
gas,
|
||||
},
|
||||
);
|
||||
return txHash;
|
||||
}
|
||||
/**
|
||||
* Sequentially and atomically fills signedOrders up to the specified takerTokenFillAmount.
|
||||
* If the fill amount is reached - it succeeds and does not fill the rest of the orders.
|
||||
* If fill amount is not reached - it fills as much of the fill amount as possible and succeeds.
|
||||
* @param signedOrders The array of signedOrders that you would like to fill until
|
||||
* takerTokenFillAmount is reached.
|
||||
* @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill.
|
||||
* @param shouldThrowOnInsufficientBalanceOrAllowance Whether or not you wish for the contract call to throw if
|
||||
* upon execution any of the tokens cannot be transferred.
|
||||
* If set to false, the call will continue to fill subsequent
|
||||
* signedOrders even when some cannot be filled.
|
||||
* @param takerAddress The user Ethereum address who would like to fill these
|
||||
* orders. Must be available via the supplied Web3.Provider
|
||||
* passed to 0x.js.
|
||||
* @param orderTransactionOpts Optional arguments this method accepts.
|
||||
* @return Transaction hash.
|
||||
*/
|
||||
@decorators.contractCallErrorHandler
|
||||
public async fillOrdersUpToAsync(signedOrders: SignedOrder[], fillTakerTokenAmount: BigNumber,
|
||||
shouldThrowOnInsufficientBalanceOrAllowance: boolean,
|
||||
takerAddress: string,
|
||||
orderTransactionOpts?: OrderTransactionOpts): Promise<string> {
|
||||
assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
|
||||
const takerTokenAddresses = _.map(signedOrders, signedOrder => signedOrder.takerTokenAddress);
|
||||
assert.hasAtMostOneUniqueValue(takerTokenAddresses,
|
||||
ExchangeContractErrs.MultipleTakerTokensInFillUpToDisallowed);
|
||||
const exchangeContractAddresses = _.map(signedOrders, signedOrder => signedOrder.exchangeContractAddress);
|
||||
assert.hasAtMostOneUniqueValue(exchangeContractAddresses,
|
||||
ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress);
|
||||
assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount);
|
||||
assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance);
|
||||
await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
|
||||
|
||||
const shouldValidate = _.isUndefined(orderTransactionOpts) ?
|
||||
SHOULD_VALIDATE_BY_DEFAULT :
|
||||
orderTransactionOpts.shouldValidate;
|
||||
if (shouldValidate) {
|
||||
const zrxTokenAddress = await this.getZRXTokenAddressAsync();
|
||||
const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
|
||||
for (const signedOrder of signedOrders) {
|
||||
await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
|
||||
exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress);
|
||||
}
|
||||
}
|
||||
|
||||
if (_.isEmpty(signedOrders)) {
|
||||
throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
|
||||
}
|
||||
|
||||
const orderAddressesValuesAndSignatureArray = _.map(signedOrders, signedOrder => {
|
||||
return [
|
||||
...ExchangeWrapper._getOrderAddressesAndValues(signedOrder),
|
||||
signedOrder.ecSignature.v,
|
||||
signedOrder.ecSignature.r,
|
||||
signedOrder.ecSignature.s,
|
||||
];
|
||||
});
|
||||
// We use _.unzip<any> because _.unzip doesn't type check if values have different types :'(
|
||||
const [orderAddressesArray, orderValuesArray, vArray, rArray, sArray] = _.unzip<any>(
|
||||
orderAddressesValuesAndSignatureArray,
|
||||
);
|
||||
|
||||
const exchangeInstance = await this._getExchangeContractAsync();
|
||||
const gas = await exchangeInstance.fillOrdersUpTo.estimateGasAsync(
|
||||
orderAddressesArray,
|
||||
orderValuesArray,
|
||||
fillTakerTokenAmount,
|
||||
shouldThrowOnInsufficientBalanceOrAllowance,
|
||||
vArray,
|
||||
rArray,
|
||||
sArray,
|
||||
{
|
||||
from: takerAddress,
|
||||
},
|
||||
);
|
||||
const txHash = await exchangeInstance.fillOrdersUpTo.sendTransactionAsync(
|
||||
orderAddressesArray,
|
||||
orderValuesArray,
|
||||
fillTakerTokenAmount,
|
||||
shouldThrowOnInsufficientBalanceOrAllowance,
|
||||
vArray,
|
||||
rArray,
|
||||
sArray,
|
||||
{
|
||||
from: takerAddress,
|
||||
gas,
|
||||
},
|
||||
);
|
||||
return txHash;
|
||||
}
|
||||
/**
|
||||
* Batch version of fillOrderAsync.
|
||||
* Executes multiple fills atomically in a single transaction.
|
||||
* If shouldThrowOnInsufficientBalanceOrAllowance is set to false, it will continue filling subsequent orders even
|
||||
* when earlier ones fail.
|
||||
* When shouldThrowOnInsufficientBalanceOrAllowance is set to true, if any fill fails, the entire batch fails.
|
||||
* @param orderFillRequests An array of objects that conform to the
|
||||
* OrderFillRequest interface.
|
||||
* @param shouldThrowOnInsufficientBalanceOrAllowance Whether or not you wish for the contract call to throw
|
||||
* if upon execution any of the tokens cannot be
|
||||
* transferred. If set to false, the call will continue to
|
||||
* fill subsequent signedOrders even when some
|
||||
* cannot be filled.
|
||||
* @param takerAddress The user Ethereum address who would like to fill
|
||||
* these orders. Must be available via the supplied
|
||||
* Web3.Provider passed to 0x.js.
|
||||
* @param orderTransactionOpts Optional arguments this method accepts.
|
||||
* @return Transaction hash.
|
||||
*/
|
||||
@decorators.contractCallErrorHandler
|
||||
public async batchFillOrdersAsync(orderFillRequests: OrderFillRequest[],
|
||||
shouldThrowOnInsufficientBalanceOrAllowance: boolean,
|
||||
takerAddress: string,
|
||||
orderTransactionOpts?: OrderTransactionOpts): Promise<string> {
|
||||
assert.doesConformToSchema('orderFillRequests', orderFillRequests, schemas.orderFillRequestsSchema);
|
||||
const exchangeContractAddresses = _.map(
|
||||
orderFillRequests,
|
||||
orderFillRequest => orderFillRequest.signedOrder.exchangeContractAddress,
|
||||
);
|
||||
assert.hasAtMostOneUniqueValue(exchangeContractAddresses,
|
||||
ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress);
|
||||
assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance);
|
||||
await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
|
||||
const shouldValidate = _.isUndefined(orderTransactionOpts) ?
|
||||
SHOULD_VALIDATE_BY_DEFAULT :
|
||||
orderTransactionOpts.shouldValidate;
|
||||
if (shouldValidate) {
|
||||
const zrxTokenAddress = await this.getZRXTokenAddressAsync();
|
||||
const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
|
||||
for (const orderFillRequest of orderFillRequests) {
|
||||
await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
|
||||
exchangeTradeEmulator, orderFillRequest.signedOrder, orderFillRequest.takerTokenFillAmount,
|
||||
takerAddress, zrxTokenAddress,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (_.isEmpty(orderFillRequests)) {
|
||||
throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
|
||||
}
|
||||
|
||||
const orderAddressesValuesAmountsAndSignatureArray = _.map(orderFillRequests, orderFillRequest => {
|
||||
return [
|
||||
...ExchangeWrapper._getOrderAddressesAndValues(orderFillRequest.signedOrder),
|
||||
orderFillRequest.takerTokenFillAmount,
|
||||
orderFillRequest.signedOrder.ecSignature.v,
|
||||
orderFillRequest.signedOrder.ecSignature.r,
|
||||
orderFillRequest.signedOrder.ecSignature.s,
|
||||
];
|
||||
});
|
||||
// We use _.unzip<any> because _.unzip doesn't type check if values have different types :'(
|
||||
const [orderAddressesArray, orderValuesArray, fillTakerTokenAmounts, vArray, rArray, sArray] = _.unzip<any>(
|
||||
orderAddressesValuesAmountsAndSignatureArray,
|
||||
);
|
||||
|
||||
const exchangeInstance = await this._getExchangeContractAsync();
|
||||
const gas = await exchangeInstance.batchFillOrders.estimateGasAsync(
|
||||
orderAddressesArray,
|
||||
orderValuesArray,
|
||||
fillTakerTokenAmounts,
|
||||
shouldThrowOnInsufficientBalanceOrAllowance,
|
||||
vArray,
|
||||
rArray,
|
||||
sArray,
|
||||
{
|
||||
from: takerAddress,
|
||||
},
|
||||
);
|
||||
const txHash = await exchangeInstance.batchFillOrders.sendTransactionAsync(
|
||||
orderAddressesArray,
|
||||
orderValuesArray,
|
||||
fillTakerTokenAmounts,
|
||||
shouldThrowOnInsufficientBalanceOrAllowance,
|
||||
vArray,
|
||||
rArray,
|
||||
sArray,
|
||||
{
|
||||
from: takerAddress,
|
||||
gas,
|
||||
},
|
||||
);
|
||||
return txHash;
|
||||
}
|
||||
/**
|
||||
* Attempts to fill a specific amount of an order. If the entire amount specified cannot be filled,
|
||||
* the fill order is abandoned.
|
||||
* @param signedOrder An object that conforms to the SignedOrder interface. The
|
||||
* signedOrder you wish to fill.
|
||||
* @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill.
|
||||
* @param takerAddress The user Ethereum address who would like to fill this order.
|
||||
* Must be available via the supplied Web3.Provider passed to 0x.js.
|
||||
* @param orderTransactionOpts Optional arguments this method accepts.
|
||||
* @return Transaction hash.
|
||||
*/
|
||||
@decorators.contractCallErrorHandler
|
||||
public async fillOrKillOrderAsync(signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber,
|
||||
takerAddress: string,
|
||||
orderTransactionOpts?: OrderTransactionOpts): Promise<string> {
|
||||
assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
|
||||
assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount);
|
||||
await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
|
||||
|
||||
const exchangeInstance = await this._getExchangeContractAsync();
|
||||
|
||||
const shouldValidate = _.isUndefined(orderTransactionOpts) ?
|
||||
SHOULD_VALIDATE_BY_DEFAULT :
|
||||
orderTransactionOpts.shouldValidate;
|
||||
if (shouldValidate) {
|
||||
const zrxTokenAddress = await this.getZRXTokenAddressAsync();
|
||||
const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
|
||||
await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync(
|
||||
exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress);
|
||||
}
|
||||
|
||||
const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(signedOrder);
|
||||
|
||||
const gas = await exchangeInstance.fillOrKillOrder.estimateGasAsync(
|
||||
orderAddresses,
|
||||
orderValues,
|
||||
fillTakerTokenAmount,
|
||||
signedOrder.ecSignature.v,
|
||||
signedOrder.ecSignature.r,
|
||||
signedOrder.ecSignature.s,
|
||||
{
|
||||
from: takerAddress,
|
||||
},
|
||||
);
|
||||
const txHash = await exchangeInstance.fillOrKillOrder.sendTransactionAsync(
|
||||
orderAddresses,
|
||||
orderValues,
|
||||
fillTakerTokenAmount,
|
||||
signedOrder.ecSignature.v,
|
||||
signedOrder.ecSignature.r,
|
||||
signedOrder.ecSignature.s,
|
||||
{
|
||||
from: takerAddress,
|
||||
gas,
|
||||
},
|
||||
);
|
||||
return txHash;
|
||||
}
|
||||
/**
|
||||
* Batch version of fillOrKill. Allows a taker to specify a batch of orders that will either be atomically
|
||||
* filled (each to the specified fillAmount) or aborted.
|
||||
* @param orderFillRequests An array of objects that conform to the OrderFillRequest interface.
|
||||
* @param takerAddress The user Ethereum address who would like to fill there orders.
|
||||
* Must be available via the supplied Web3.Provider passed to 0x.js.
|
||||
* @param orderTransactionOpts Optional arguments this method accepts.
|
||||
* @return Transaction hash.
|
||||
*/
|
||||
@decorators.contractCallErrorHandler
|
||||
public async batchFillOrKillAsync(orderFillRequests: OrderFillRequest[],
|
||||
takerAddress: string,
|
||||
orderTransactionOpts?: OrderTransactionOpts): Promise<string> {
|
||||
assert.doesConformToSchema('orderFillRequests', orderFillRequests,
|
||||
schemas.orderFillRequestsSchema);
|
||||
const exchangeContractAddresses = _.map(
|
||||
orderFillRequests,
|
||||
orderFillRequest => orderFillRequest.signedOrder.exchangeContractAddress,
|
||||
);
|
||||
assert.hasAtMostOneUniqueValue(exchangeContractAddresses,
|
||||
ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress);
|
||||
await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
|
||||
if (_.isEmpty(orderFillRequests)) {
|
||||
throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
|
||||
}
|
||||
const exchangeInstance = await this._getExchangeContractAsync();
|
||||
|
||||
const shouldValidate = _.isUndefined(orderTransactionOpts) ?
|
||||
SHOULD_VALIDATE_BY_DEFAULT :
|
||||
orderTransactionOpts.shouldValidate;
|
||||
if (shouldValidate) {
|
||||
const zrxTokenAddress = await this.getZRXTokenAddressAsync();
|
||||
const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
|
||||
for (const orderFillRequest of orderFillRequests) {
|
||||
await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync(
|
||||
exchangeTradeEmulator, orderFillRequest.signedOrder, orderFillRequest.takerTokenFillAmount,
|
||||
takerAddress, zrxTokenAddress,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const orderAddressesValuesAndTakerTokenFillAmounts = _.map(orderFillRequests, request => {
|
||||
return [
|
||||
...ExchangeWrapper._getOrderAddressesAndValues(request.signedOrder),
|
||||
request.takerTokenFillAmount,
|
||||
request.signedOrder.ecSignature.v,
|
||||
request.signedOrder.ecSignature.r,
|
||||
request.signedOrder.ecSignature.s,
|
||||
];
|
||||
});
|
||||
|
||||
// We use _.unzip<any> because _.unzip doesn't type check if values have different types :'(
|
||||
const [orderAddresses, orderValues, fillTakerTokenAmounts, vParams, rParams, sParams] =
|
||||
_.unzip<any>(orderAddressesValuesAndTakerTokenFillAmounts);
|
||||
|
||||
const gas = await exchangeInstance.batchFillOrKillOrders.estimateGasAsync(
|
||||
orderAddresses,
|
||||
orderValues,
|
||||
fillTakerTokenAmounts,
|
||||
vParams,
|
||||
rParams,
|
||||
sParams,
|
||||
{
|
||||
from: takerAddress,
|
||||
},
|
||||
);
|
||||
const txHash = await exchangeInstance.batchFillOrKillOrders.sendTransactionAsync(
|
||||
orderAddresses,
|
||||
orderValues,
|
||||
fillTakerTokenAmounts,
|
||||
vParams,
|
||||
rParams,
|
||||
sParams,
|
||||
{
|
||||
from: takerAddress,
|
||||
gas,
|
||||
},
|
||||
);
|
||||
return txHash;
|
||||
}
|
||||
/**
|
||||
* Cancel a given fill amount of an order. Cancellations are cumulative.
|
||||
* @param order An object that conforms to the Order or SignedOrder interface.
|
||||
* The order you would like to cancel.
|
||||
* @param cancelTakerTokenAmount The amount (specified in taker tokens) that you would like to cancel.
|
||||
* @param transactionOpts Optional arguments this method accepts.
|
||||
* @return Transaction hash.
|
||||
*/
|
||||
@decorators.contractCallErrorHandler
|
||||
public async cancelOrderAsync(order: Order|SignedOrder,
|
||||
cancelTakerTokenAmount: BigNumber,
|
||||
orderTransactionOpts?: OrderTransactionOpts): Promise<string> {
|
||||
assert.doesConformToSchema('order', order, schemas.orderSchema);
|
||||
assert.isValidBaseUnitAmount('takerTokenCancelAmount', cancelTakerTokenAmount);
|
||||
await assert.isSenderAddressAsync('order.maker', order.maker, this._web3Wrapper);
|
||||
|
||||
const exchangeInstance = await this._getExchangeContractAsync();
|
||||
|
||||
const shouldValidate = _.isUndefined(orderTransactionOpts) ?
|
||||
SHOULD_VALIDATE_BY_DEFAULT :
|
||||
orderTransactionOpts.shouldValidate;
|
||||
if (shouldValidate) {
|
||||
const orderHash = utils.getOrderHashHex(order);
|
||||
const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash);
|
||||
await this._orderValidationUtils.validateCancelOrderThrowIfInvalidAsync(
|
||||
order, cancelTakerTokenAmount, unavailableTakerTokenAmount);
|
||||
}
|
||||
|
||||
const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(order);
|
||||
const gas = await exchangeInstance.cancelOrder.estimateGasAsync(
|
||||
orderAddresses,
|
||||
orderValues,
|
||||
cancelTakerTokenAmount,
|
||||
{
|
||||
from: order.maker,
|
||||
},
|
||||
);
|
||||
const txHash = await exchangeInstance.cancelOrder.sendTransactionAsync(
|
||||
orderAddresses,
|
||||
orderValues,
|
||||
cancelTakerTokenAmount,
|
||||
{
|
||||
from: order.maker,
|
||||
gas,
|
||||
},
|
||||
);
|
||||
return txHash;
|
||||
}
|
||||
/**
|
||||
* Batch version of cancelOrderAsync. Atomically cancels multiple orders in a single transaction.
|
||||
* All orders must be from the same maker.
|
||||
* @param orderCancellationRequests An array of objects that conform to the OrderCancellationRequest
|
||||
* interface.
|
||||
* @param transactionOpts Optional arguments this method accepts.
|
||||
* @return Transaction hash.
|
||||
*/
|
||||
@decorators.contractCallErrorHandler
|
||||
public async batchCancelOrdersAsync(orderCancellationRequests: OrderCancellationRequest[],
|
||||
orderTransactionOpts?: OrderTransactionOpts): Promise<string> {
|
||||
assert.doesConformToSchema('orderCancellationRequests', orderCancellationRequests,
|
||||
schemas.orderCancellationRequestsSchema);
|
||||
const exchangeContractAddresses = _.map(
|
||||
orderCancellationRequests,
|
||||
orderCancellationRequest => orderCancellationRequest.order.exchangeContractAddress,
|
||||
);
|
||||
assert.hasAtMostOneUniqueValue(exchangeContractAddresses,
|
||||
ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress);
|
||||
const makers = _.map(orderCancellationRequests, cancellationRequest => cancellationRequest.order.maker);
|
||||
assert.hasAtMostOneUniqueValue(makers, ExchangeContractErrs.MultipleMakersInSingleCancelBatchDisallowed);
|
||||
const maker = makers[0];
|
||||
await assert.isSenderAddressAsync('maker', maker, this._web3Wrapper);
|
||||
const shouldValidate = _.isUndefined(orderTransactionOpts) ?
|
||||
SHOULD_VALIDATE_BY_DEFAULT :
|
||||
orderTransactionOpts.shouldValidate;
|
||||
if (shouldValidate) {
|
||||
for (const orderCancellationRequest of orderCancellationRequests) {
|
||||
const orderHash = utils.getOrderHashHex(orderCancellationRequest.order);
|
||||
const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash);
|
||||
await this._orderValidationUtils.validateCancelOrderThrowIfInvalidAsync(
|
||||
orderCancellationRequest.order, orderCancellationRequest.takerTokenCancelAmount,
|
||||
unavailableTakerTokenAmount,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
if (_.isEmpty(orderCancellationRequests)) {
|
||||
throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
|
||||
}
|
||||
const exchangeInstance = await this._getExchangeContractAsync();
|
||||
const orderAddressesValuesAndTakerTokenCancelAmounts = _.map(orderCancellationRequests, cancellationRequest => {
|
||||
return [
|
||||
...ExchangeWrapper._getOrderAddressesAndValues(cancellationRequest.order),
|
||||
cancellationRequest.takerTokenCancelAmount,
|
||||
];
|
||||
});
|
||||
// We use _.unzip<any> because _.unzip doesn't type check if values have different types :'(
|
||||
const [orderAddresses, orderValues, cancelTakerTokenAmounts] =
|
||||
_.unzip<any>(orderAddressesValuesAndTakerTokenCancelAmounts);
|
||||
const gas = await exchangeInstance.batchCancelOrders.estimateGasAsync(
|
||||
orderAddresses,
|
||||
orderValues,
|
||||
cancelTakerTokenAmounts,
|
||||
{
|
||||
from: maker,
|
||||
},
|
||||
);
|
||||
const txHash = await exchangeInstance.batchCancelOrders.sendTransactionAsync(
|
||||
orderAddresses,
|
||||
orderValues,
|
||||
cancelTakerTokenAmounts,
|
||||
{
|
||||
from: maker,
|
||||
gas,
|
||||
},
|
||||
);
|
||||
return txHash;
|
||||
}
|
||||
/**
|
||||
* Subscribe to an event type emitted by the Exchange contract.
|
||||
* @param eventName The exchange contract event you would like to subscribe to.
|
||||
* @param indexFilterValues An object where the keys are indexed args returned by the event and
|
||||
* the value is the value you are interested in. E.g `{maker: aUserAddressHex}`
|
||||
* @param callback Callback that gets called when a log is added/removed
|
||||
* @return Subscription token used later to unsubscribe
|
||||
*/
|
||||
public async subscribeAsync<ArgsType extends ExchangeContractEventArgs>(
|
||||
eventName: ExchangeEvents, indexFilterValues: IndexedFilterValues,
|
||||
callback: EventCallback<ArgsType>): Promise<string> {
|
||||
assert.doesBelongToStringEnum('eventName', eventName, ExchangeEvents);
|
||||
assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
|
||||
assert.isFunction('callback', callback);
|
||||
const exchangeContractAddress = await this.getContractAddressAsync();
|
||||
const subscriptionToken = this._subscribe<ArgsType>(
|
||||
exchangeContractAddress, eventName, indexFilterValues, artifacts.ExchangeArtifact.abi, callback,
|
||||
);
|
||||
return subscriptionToken;
|
||||
}
|
||||
/**
|
||||
* Cancel a subscription
|
||||
* @param subscriptionToken Subscription token returned by `subscribe()`
|
||||
*/
|
||||
public unsubscribe(subscriptionToken: string): void {
|
||||
this._unsubscribe(subscriptionToken);
|
||||
}
|
||||
/**
|
||||
* Gets historical logs without creating a subscription
|
||||
* @param eventName The exchange contract event you would like to subscribe to.
|
||||
* @param subscriptionOpts Subscriptions options that let you configure the subscription.
|
||||
* @param indexFilterValues An object where the keys are indexed args returned by the event and
|
||||
* the value is the value you are interested in. E.g `{_from: aUserAddressHex}`
|
||||
* @return Array of logs that match the parameters
|
||||
*/
|
||||
public async getLogsAsync<ArgsType extends ExchangeContractEventArgs>(
|
||||
eventName: ExchangeEvents, subscriptionOpts: SubscriptionOpts, indexFilterValues: IndexedFilterValues,
|
||||
): Promise<Array<LogWithDecodedArgs<ArgsType>>> {
|
||||
assert.doesBelongToStringEnum('eventName', eventName, ExchangeEvents);
|
||||
assert.doesConformToSchema('subscriptionOpts', subscriptionOpts, schemas.subscriptionOptsSchema);
|
||||
assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
|
||||
const exchangeContractAddress = await this.getContractAddressAsync();
|
||||
const logs = await this._getLogsAsync<ArgsType>(
|
||||
exchangeContractAddress, eventName, subscriptionOpts, indexFilterValues, artifacts.ExchangeArtifact.abi,
|
||||
);
|
||||
return logs;
|
||||
}
|
||||
/**
|
||||
* Retrieves the Ethereum address of the Exchange contract deployed on the network
|
||||
* that the user-passed web3 provider is connected to.
|
||||
* @returns The Ethereum address of the Exchange contract being used.
|
||||
*/
|
||||
public async getContractAddressAsync(): Promise<string> {
|
||||
const exchangeInstance = await this._getExchangeContractAsync();
|
||||
const exchangeAddress = exchangeInstance.address;
|
||||
return exchangeAddress;
|
||||
}
|
||||
/**
|
||||
* Checks if order is still fillable and throws an error otherwise. Useful for orderbook
|
||||
* pruning where you want to remove stale orders without knowing who the taker will be.
|
||||
* @param signedOrder An object that conforms to the SignedOrder interface. The
|
||||
* signedOrder you wish to validate.
|
||||
* @param opts An object that conforms to the ValidateOrderFillableOpts
|
||||
* interface. Allows specifying a specific fillTakerTokenAmount
|
||||
* to validate for.
|
||||
*/
|
||||
public async validateOrderFillableOrThrowAsync(
|
||||
signedOrder: SignedOrder, opts?: ValidateOrderFillableOpts,
|
||||
): Promise<void> {
|
||||
assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
|
||||
const zrxTokenAddress = await this.getZRXTokenAddressAsync();
|
||||
const expectedFillTakerTokenAmount = !_.isUndefined(opts) ? opts.expectedFillTakerTokenAmount : undefined;
|
||||
const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
|
||||
await this._orderValidationUtils.validateOrderFillableOrThrowAsync(
|
||||
exchangeTradeEmulator, signedOrder, zrxTokenAddress, expectedFillTakerTokenAmount,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Checks if order fill will succeed and throws an error otherwise.
|
||||
* @param signedOrder An object that conforms to the SignedOrder interface. The
|
||||
* signedOrder you wish to fill.
|
||||
* @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill.
|
||||
* @param takerAddress The user Ethereum address who would like to fill this order.
|
||||
* Must be available via the supplied Web3.Provider passed to 0x.js.
|
||||
*/
|
||||
public async validateFillOrderThrowIfInvalidAsync(signedOrder: SignedOrder,
|
||||
fillTakerTokenAmount: BigNumber,
|
||||
takerAddress: string): Promise<void> {
|
||||
assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
|
||||
assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount);
|
||||
await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
|
||||
const zrxTokenAddress = await this.getZRXTokenAddressAsync();
|
||||
const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
|
||||
await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync(
|
||||
exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress);
|
||||
}
|
||||
/**
|
||||
* Checks if cancelling a given order will succeed and throws an informative error if it won't.
|
||||
* @param order An object that conforms to the Order or SignedOrder interface.
|
||||
* The order you would like to cancel.
|
||||
* @param cancelTakerTokenAmount The amount (specified in taker tokens) that you would like to cancel.
|
||||
*/
|
||||
public async validateCancelOrderThrowIfInvalidAsync(
|
||||
order: Order, cancelTakerTokenAmount: BigNumber): Promise<void> {
|
||||
assert.doesConformToSchema('order', order, schemas.orderSchema);
|
||||
assert.isValidBaseUnitAmount('cancelTakerTokenAmount', cancelTakerTokenAmount);
|
||||
const orderHash = utils.getOrderHashHex(order);
|
||||
const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash);
|
||||
await this._orderValidationUtils.validateCancelOrderThrowIfInvalidAsync(
|
||||
order, cancelTakerTokenAmount, unavailableTakerTokenAmount);
|
||||
}
|
||||
/**
|
||||
* Checks if calling fillOrKill on a given order will succeed and throws an informative error if it won't.
|
||||
* @param signedOrder An object that conforms to the SignedOrder interface. The
|
||||
* signedOrder you wish to fill.
|
||||
* @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill.
|
||||
* @param takerAddress The user Ethereum address who would like to fill this order.
|
||||
* Must be available via the supplied Web3.Provider passed to 0x.js.
|
||||
*/
|
||||
public async validateFillOrKillOrderThrowIfInvalidAsync(signedOrder: SignedOrder,
|
||||
fillTakerTokenAmount: BigNumber,
|
||||
takerAddress: string): Promise<void> {
|
||||
assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
|
||||
assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount);
|
||||
await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
|
||||
const zrxTokenAddress = await this.getZRXTokenAddressAsync();
|
||||
const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper);
|
||||
await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync(
|
||||
exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress);
|
||||
}
|
||||
/**
|
||||
* Checks if rounding error will be > 0.1% when computing makerTokenAmount by doing:
|
||||
* `(fillTakerTokenAmount * makerTokenAmount) / takerTokenAmount`.
|
||||
* 0x Protocol does not accept any trades that result in large rounding errors. This means that tokens with few or
|
||||
* no decimals can only be filled in quantities and ratios that avoid large rounding errors.
|
||||
* @param fillTakerTokenAmount The amount of the order (in taker tokens baseUnits) that you wish to fill.
|
||||
* @param takerTokenAmount The order size on the taker side
|
||||
* @param makerTokenAmount The order size on the maker side
|
||||
*/
|
||||
public async isRoundingErrorAsync(fillTakerTokenAmount: BigNumber,
|
||||
takerTokenAmount: BigNumber,
|
||||
makerTokenAmount: BigNumber): Promise<boolean> {
|
||||
assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount);
|
||||
assert.isValidBaseUnitAmount('takerTokenAmount', takerTokenAmount);
|
||||
assert.isValidBaseUnitAmount('makerTokenAmount', makerTokenAmount);
|
||||
const exchangeInstance = await this._getExchangeContractAsync();
|
||||
const isRoundingError = await exchangeInstance.isRoundingError.callAsync(
|
||||
fillTakerTokenAmount, takerTokenAmount, makerTokenAmount,
|
||||
);
|
||||
return isRoundingError;
|
||||
}
|
||||
/**
|
||||
* Checks if logs contain LogError, which is emmited by Exchange contract on transaction failure.
|
||||
* @param logs Transaction logs as returned by `zeroEx.awaitTransactionMinedAsync`
|
||||
*/
|
||||
public throwLogErrorsAsErrors(logs: Array<LogWithDecodedArgs<DecodedLogArgs>|Web3.LogEntry>): void {
|
||||
const errLog = _.find(logs, {
|
||||
event: ExchangeEvents.LogError,
|
||||
}) as LogWithDecodedArgs<LogErrorContractEventArgs>|undefined;
|
||||
if (!_.isUndefined(errLog)) {
|
||||
const logArgs = errLog.args;
|
||||
const errCode = logArgs.errorId.toNumber();
|
||||
const errMessage = this._exchangeContractErrCodesToMsg[errCode];
|
||||
throw new Error(errMessage);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the ZRX token address used by the exchange contract.
|
||||
* @return Address of ZRX token
|
||||
*/
|
||||
public async getZRXTokenAddressAsync(): Promise<string> {
|
||||
const exchangeInstance = await this._getExchangeContractAsync();
|
||||
const ZRXtokenAddress = await exchangeInstance.ZRX_TOKEN_CONTRACT.callAsync();
|
||||
return ZRXtokenAddress;
|
||||
}
|
||||
private async _invalidateContractInstancesAsync(): Promise<void> {
|
||||
this.unsubscribeAll();
|
||||
delete this._exchangeContractIfExists;
|
||||
}
|
||||
private async _isValidSignatureUsingContractCallAsync(dataHex: string, ecSignature: ECSignature,
|
||||
signerAddressHex: string): Promise<boolean> {
|
||||
assert.isHexString('dataHex', dataHex);
|
||||
assert.doesConformToSchema('ecSignature', ecSignature, schemas.ecSignatureSchema);
|
||||
assert.isETHAddressHex('signerAddressHex', signerAddressHex);
|
||||
|
||||
const exchangeInstance = await this._getExchangeContractAsync();
|
||||
|
||||
const isValidSignature = await exchangeInstance.isValidSignature.callAsync(
|
||||
signerAddressHex,
|
||||
dataHex,
|
||||
ecSignature.v,
|
||||
ecSignature.r,
|
||||
ecSignature.s,
|
||||
);
|
||||
return isValidSignature;
|
||||
}
|
||||
private async _getOrderHashHexUsingContractCallAsync(order: Order|SignedOrder): Promise<string> {
|
||||
const exchangeInstance = await this._getExchangeContractAsync();
|
||||
const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(order);
|
||||
const orderHashHex = await exchangeInstance.getOrderHash.callAsync(orderAddresses, orderValues);
|
||||
return orderHashHex;
|
||||
}
|
||||
private async _getExchangeContractAsync(): Promise<ExchangeContract> {
|
||||
if (!_.isUndefined(this._exchangeContractIfExists)) {
|
||||
return this._exchangeContractIfExists;
|
||||
}
|
||||
const contractInstance = await this._instantiateContractIfExistsAsync<ExchangeContract>(
|
||||
artifacts.ExchangeArtifact, this._contractAddressIfExists,
|
||||
);
|
||||
this._exchangeContractIfExists = contractInstance as ExchangeContract;
|
||||
return this._exchangeContractIfExists;
|
||||
}
|
||||
private async _getTokenTransferProxyAddressAsync(): Promise<string> {
|
||||
const exchangeInstance = await this._getExchangeContractAsync();
|
||||
const tokenTransferProxyAddress = await exchangeInstance.TOKEN_TRANSFER_PROXY_CONTRACT.callAsync();
|
||||
const tokenTransferProxyAddressLowerCase = tokenTransferProxyAddress.toLowerCase();
|
||||
return tokenTransferProxyAddressLowerCase;
|
||||
}
|
||||
}
|
122
packages/0x.js/src/contract_wrappers/token_registry_wrapper.ts
Normal file
122
packages/0x.js/src/contract_wrappers/token_registry_wrapper.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import * as _ from 'lodash';
|
||||
import {Web3Wrapper} from '../web3_wrapper';
|
||||
import {assert} from '../utils/assert';
|
||||
import {Token, TokenRegistryContract, TokenMetadata} from '../types';
|
||||
import {constants} from '../utils/constants';
|
||||
import {ContractWrapper} from './contract_wrapper';
|
||||
import {artifacts} from '../artifacts';
|
||||
|
||||
/**
|
||||
* This class includes all the functionality related to interacting with the 0x Token Registry smart contract.
|
||||
*/
|
||||
export class TokenRegistryWrapper extends ContractWrapper {
|
||||
private _tokenRegistryContractIfExists?: TokenRegistryContract;
|
||||
private _contractAddressIfExists?: string;
|
||||
constructor(web3Wrapper: Web3Wrapper, contractAddressIfExists?: string) {
|
||||
super(web3Wrapper);
|
||||
this._contractAddressIfExists = contractAddressIfExists;
|
||||
}
|
||||
/**
|
||||
* Retrieves all the tokens currently listed in the Token Registry smart contract
|
||||
* @return An array of objects that conform to the Token interface.
|
||||
*/
|
||||
public async getTokensAsync(): Promise<Token[]> {
|
||||
const tokenRegistryContract = await this._getTokenRegistryContractAsync();
|
||||
|
||||
const addresses = await this.getTokenAddressesAsync();
|
||||
const tokenPromises: Array<Promise<Token|undefined>> = _.map(
|
||||
addresses,
|
||||
(address: string) => (this.getTokenIfExistsAsync(address)),
|
||||
);
|
||||
const tokens = await Promise.all(tokenPromises);
|
||||
return tokens as Token[];
|
||||
}
|
||||
/**
|
||||
* Retrieves all the addresses of the tokens currently listed in the Token Registry smart contract
|
||||
* @return An array of token addresses.
|
||||
*/
|
||||
public async getTokenAddressesAsync(): Promise<string[]> {
|
||||
const tokenRegistryContract = await this._getTokenRegistryContractAsync();
|
||||
const addresses = await tokenRegistryContract.getTokenAddresses.callAsync();
|
||||
return addresses;
|
||||
}
|
||||
/**
|
||||
* Retrieves a token by address currently listed in the Token Registry smart contract
|
||||
* @return An object that conforms to the Token interface or undefined if token not found.
|
||||
*/
|
||||
public async getTokenIfExistsAsync(address: string): Promise<Token|undefined> {
|
||||
assert.isETHAddressHex('address', address);
|
||||
|
||||
const tokenRegistryContract = await this._getTokenRegistryContractAsync();
|
||||
const metadata = await tokenRegistryContract.getTokenMetaData.callAsync(address);
|
||||
const token = this._createTokenFromMetadata(metadata);
|
||||
return token;
|
||||
}
|
||||
public async getTokenAddressBySymbolIfExistsAsync(symbol: string): Promise<string|undefined> {
|
||||
assert.isString('symbol', symbol);
|
||||
const tokenRegistryContract = await this._getTokenRegistryContractAsync();
|
||||
const addressIfExists = await tokenRegistryContract.getTokenAddressBySymbol.callAsync(symbol);
|
||||
if (addressIfExists === constants.NULL_ADDRESS) {
|
||||
return undefined;
|
||||
}
|
||||
return addressIfExists;
|
||||
}
|
||||
public async getTokenAddressByNameIfExistsAsync(name: string): Promise<string|undefined> {
|
||||
assert.isString('name', name);
|
||||
const tokenRegistryContract = await this._getTokenRegistryContractAsync();
|
||||
const addressIfExists = await tokenRegistryContract.getTokenAddressByName.callAsync(name);
|
||||
if (addressIfExists === constants.NULL_ADDRESS) {
|
||||
return undefined;
|
||||
}
|
||||
return addressIfExists;
|
||||
}
|
||||
public async getTokenBySymbolIfExistsAsync(symbol: string): Promise<Token|undefined> {
|
||||
assert.isString('symbol', symbol);
|
||||
const tokenRegistryContract = await this._getTokenRegistryContractAsync();
|
||||
const metadata = await tokenRegistryContract.getTokenBySymbol.callAsync(symbol);
|
||||
const token = this._createTokenFromMetadata(metadata);
|
||||
return token;
|
||||
}
|
||||
public async getTokenByNameIfExistsAsync(name: string): Promise<Token|undefined> {
|
||||
assert.isString('name', name);
|
||||
const tokenRegistryContract = await this._getTokenRegistryContractAsync();
|
||||
const metadata = await tokenRegistryContract.getTokenByName.callAsync(name);
|
||||
const token = this._createTokenFromMetadata(metadata);
|
||||
return token;
|
||||
}
|
||||
/**
|
||||
* Retrieves the Ethereum address of the TokenRegistry contract deployed on the network
|
||||
* that the user-passed web3 provider is connected to.
|
||||
* @returns The Ethereum address of the TokenRegistry contract being used.
|
||||
*/
|
||||
public async getContractAddressAsync(): Promise<string> {
|
||||
const tokenRegistryInstance = await this._getTokenRegistryContractAsync();
|
||||
const tokenRegistryAddress = tokenRegistryInstance.address;
|
||||
return tokenRegistryAddress;
|
||||
}
|
||||
private _createTokenFromMetadata(metadata: TokenMetadata): Token|undefined {
|
||||
if (metadata[0] === constants.NULL_ADDRESS) {
|
||||
return undefined;
|
||||
}
|
||||
const token = {
|
||||
address: metadata[0],
|
||||
name: metadata[1],
|
||||
symbol: metadata[2],
|
||||
decimals: metadata[3].toNumber(),
|
||||
};
|
||||
return token;
|
||||
}
|
||||
private _invalidateContractInstance(): void {
|
||||
delete this._tokenRegistryContractIfExists;
|
||||
}
|
||||
private async _getTokenRegistryContractAsync(): Promise<TokenRegistryContract> {
|
||||
if (!_.isUndefined(this._tokenRegistryContractIfExists)) {
|
||||
return this._tokenRegistryContractIfExists;
|
||||
}
|
||||
const contractInstance = await this._instantiateContractIfExistsAsync<TokenRegistryContract>(
|
||||
artifacts.TokenRegistryArtifact, this._contractAddressIfExists,
|
||||
);
|
||||
this._tokenRegistryContractIfExists = contractInstance as TokenRegistryContract;
|
||||
return this._tokenRegistryContractIfExists;
|
||||
}
|
||||
}
|
@@ -0,0 +1,60 @@
|
||||
import * as _ from 'lodash';
|
||||
import {Web3Wrapper} from '../web3_wrapper';
|
||||
import {ContractWrapper} from './contract_wrapper';
|
||||
import {artifacts} from '../artifacts';
|
||||
import {TokenTransferProxyContract} from '../types';
|
||||
|
||||
/**
|
||||
* This class includes the functionality related to interacting with the TokenTransferProxy contract.
|
||||
*/
|
||||
export class TokenTransferProxyWrapper extends ContractWrapper {
|
||||
private _tokenTransferProxyContractIfExists?: TokenTransferProxyContract;
|
||||
private _tokenTransferProxyContractAddressFetcher: () => Promise<string>;
|
||||
constructor(web3Wrapper: Web3Wrapper, tokenTransferProxyContractAddressFetcher: () => Promise<string>) {
|
||||
super(web3Wrapper);
|
||||
this._tokenTransferProxyContractAddressFetcher = tokenTransferProxyContractAddressFetcher;
|
||||
}
|
||||
/**
|
||||
* Check if the Exchange contract address is authorized by the TokenTransferProxy contract.
|
||||
* @param exchangeContractAddress The hex encoded address of the Exchange contract to call.
|
||||
* @return Whether the exchangeContractAddress is authorized.
|
||||
*/
|
||||
public async isAuthorizedAsync(exchangeContractAddress: string): Promise<boolean> {
|
||||
const tokenTransferProxyContractInstance = await this._getTokenTransferProxyContractAsync();
|
||||
const isAuthorized = await tokenTransferProxyContractInstance.authorized.callAsync(exchangeContractAddress);
|
||||
return isAuthorized;
|
||||
}
|
||||
/**
|
||||
* Get the list of all Exchange contract addresses authorized by the TokenTransferProxy contract.
|
||||
* @return The list of authorized addresses.
|
||||
*/
|
||||
public async getAuthorizedAddressesAsync(): Promise<string[]> {
|
||||
const tokenTransferProxyContractInstance = await this._getTokenTransferProxyContractAsync();
|
||||
const authorizedAddresses = await tokenTransferProxyContractInstance.getAuthorizedAddresses.callAsync();
|
||||
return authorizedAddresses;
|
||||
}
|
||||
/**
|
||||
* Retrieves the Ethereum address of the TokenTransferProxy contract deployed on the network
|
||||
* that the user-passed web3 provider is connected to.
|
||||
* @returns The Ethereum address of the TokenTransferProxy contract being used.
|
||||
*/
|
||||
public async getContractAddressAsync(): Promise<string> {
|
||||
const proxyInstance = await this._getTokenTransferProxyContractAsync();
|
||||
const proxyAddress = proxyInstance.address;
|
||||
return proxyAddress;
|
||||
}
|
||||
private _invalidateContractInstance(): void {
|
||||
delete this._tokenTransferProxyContractIfExists;
|
||||
}
|
||||
private async _getTokenTransferProxyContractAsync(): Promise<TokenTransferProxyContract> {
|
||||
if (!_.isUndefined(this._tokenTransferProxyContractIfExists)) {
|
||||
return this._tokenTransferProxyContractIfExists;
|
||||
}
|
||||
const contractAddress = await this._tokenTransferProxyContractAddressFetcher();
|
||||
const contractInstance = await this._instantiateContractIfExistsAsync<TokenTransferProxyContract>(
|
||||
artifacts.TokenTransferProxyArtifact, contractAddress,
|
||||
);
|
||||
this._tokenTransferProxyContractIfExists = contractInstance as TokenTransferProxyContract;
|
||||
return this._tokenTransferProxyContractIfExists;
|
||||
}
|
||||
}
|
313
packages/0x.js/src/contract_wrappers/token_wrapper.ts
Normal file
313
packages/0x.js/src/contract_wrappers/token_wrapper.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import * as _ from 'lodash';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {schemas} from '0x-json-schemas';
|
||||
import {Web3Wrapper} from '../web3_wrapper';
|
||||
import {assert} from '../utils/assert';
|
||||
import {constants} from '../utils/constants';
|
||||
import {ContractWrapper} from './contract_wrapper';
|
||||
import {AbiDecoder} from '../utils/abi_decoder';
|
||||
import {artifacts} from '../artifacts';
|
||||
import {
|
||||
TokenContract,
|
||||
ZeroExError,
|
||||
TokenEvents,
|
||||
IndexedFilterValues,
|
||||
SubscriptionOpts,
|
||||
MethodOpts,
|
||||
LogWithDecodedArgs,
|
||||
EventCallback,
|
||||
TokenContractEventArgs,
|
||||
} from '../types';
|
||||
|
||||
const ALLOWANCE_TO_ZERO_GAS_AMOUNT = 47275;
|
||||
|
||||
/**
|
||||
* This class includes all the functionality related to interacting with ERC20 token contracts.
|
||||
* All ERC20 method calls are supported, along with some convenience methods for getting/setting allowances
|
||||
* to the 0x Proxy smart contract.
|
||||
*/
|
||||
export class TokenWrapper extends ContractWrapper {
|
||||
public UNLIMITED_ALLOWANCE_IN_BASE_UNITS = constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS;
|
||||
private _tokenContractsByAddress: {[address: string]: TokenContract};
|
||||
private _tokenTransferProxyContractAddressFetcher: () => Promise<string>;
|
||||
constructor(web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder,
|
||||
tokenTransferProxyContractAddressFetcher: () => Promise<string>) {
|
||||
super(web3Wrapper, abiDecoder);
|
||||
this._tokenContractsByAddress = {};
|
||||
this._tokenTransferProxyContractAddressFetcher = tokenTransferProxyContractAddressFetcher;
|
||||
}
|
||||
/**
|
||||
* Retrieves an owner's ERC20 token balance.
|
||||
* @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
|
||||
* @param ownerAddress The hex encoded user Ethereum address whose balance you would like to check.
|
||||
* @param methodOpts Optional arguments this method accepts.
|
||||
* @return The owner's ERC20 token balance in base units.
|
||||
*/
|
||||
public async getBalanceAsync(tokenAddress: string, ownerAddress: string,
|
||||
methodOpts?: MethodOpts): Promise<BigNumber> {
|
||||
assert.isETHAddressHex('ownerAddress', ownerAddress);
|
||||
assert.isETHAddressHex('tokenAddress', tokenAddress);
|
||||
|
||||
const tokenContract = await this._getTokenContractAsync(tokenAddress);
|
||||
const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
|
||||
let balance = await tokenContract.balanceOf.callAsync(ownerAddress, defaultBlock);
|
||||
// Wrap BigNumbers returned from web3 with our own (later) version of BigNumber
|
||||
balance = new BigNumber(balance);
|
||||
return balance;
|
||||
}
|
||||
/**
|
||||
* Sets the spender's allowance to a specified number of baseUnits on behalf of the owner address.
|
||||
* Equivalent to the ERC20 spec method `approve`.
|
||||
* @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
|
||||
* @param ownerAddress The hex encoded user Ethereum address who would like to set an allowance
|
||||
* for spenderAddress.
|
||||
* @param spenderAddress The hex encoded user Ethereum address who will be able to spend the set allowance.
|
||||
* @param amountInBaseUnits The allowance amount you would like to set.
|
||||
* @return Transaction hash.
|
||||
*/
|
||||
public async setAllowanceAsync(tokenAddress: string, ownerAddress: string, spenderAddress: string,
|
||||
amountInBaseUnits: BigNumber): Promise<string> {
|
||||
await assert.isSenderAddressAsync('ownerAddress', ownerAddress, this._web3Wrapper);
|
||||
assert.isETHAddressHex('spenderAddress', spenderAddress);
|
||||
assert.isETHAddressHex('tokenAddress', tokenAddress);
|
||||
assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits);
|
||||
|
||||
const tokenContract = await this._getTokenContractAsync(tokenAddress);
|
||||
// Hack: for some reason default estimated gas amount causes `base fee exceeds gas limit` exception
|
||||
// on testrpc. Probably related to https://github.com/ethereumjs/testrpc/issues/294
|
||||
// TODO: Debug issue in testrpc and submit a PR, then remove this hack
|
||||
const networkIdIfExists = await this._web3Wrapper.getNetworkIdIfExistsAsync();
|
||||
const gas = networkIdIfExists === constants.TESTRPC_NETWORK_ID ? ALLOWANCE_TO_ZERO_GAS_AMOUNT : undefined;
|
||||
const txHash = await tokenContract.approve.sendTransactionAsync(spenderAddress, amountInBaseUnits, {
|
||||
from: ownerAddress,
|
||||
gas,
|
||||
});
|
||||
return txHash;
|
||||
}
|
||||
/**
|
||||
* Sets the spender's allowance to an unlimited number of baseUnits on behalf of the owner address.
|
||||
* Equivalent to the ERC20 spec method `approve`.
|
||||
* Setting an unlimited allowance will lower the gas cost for filling orders involving tokens that forego updating
|
||||
* allowances set to the max amount (e.g ZRX, WETH)
|
||||
* @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
|
||||
* @param ownerAddress The hex encoded user Ethereum address who would like to set an allowance
|
||||
* for spenderAddress.
|
||||
* @param spenderAddress The hex encoded user Ethereum address who will be able to spend the set allowance.
|
||||
* @return Transaction hash.
|
||||
*/
|
||||
public async setUnlimitedAllowanceAsync(tokenAddress: string, ownerAddress: string,
|
||||
spenderAddress: string): Promise<string> {
|
||||
const txHash = await this.setAllowanceAsync(
|
||||
tokenAddress, ownerAddress, spenderAddress, this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
|
||||
);
|
||||
return txHash;
|
||||
}
|
||||
/**
|
||||
* Retrieves the owners allowance in baseUnits set to the spender's address.
|
||||
* @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
|
||||
* @param ownerAddress The hex encoded user Ethereum address whose allowance to spenderAddress
|
||||
* you would like to retrieve.
|
||||
* @param spenderAddress The hex encoded user Ethereum address who can spend the allowance you are fetching.
|
||||
* @param methodOpts Optional arguments this method accepts.
|
||||
*/
|
||||
public async getAllowanceAsync(tokenAddress: string, ownerAddress: string,
|
||||
spenderAddress: string, methodOpts?: MethodOpts): Promise<BigNumber> {
|
||||
assert.isETHAddressHex('ownerAddress', ownerAddress);
|
||||
assert.isETHAddressHex('tokenAddress', tokenAddress);
|
||||
|
||||
const tokenContract = await this._getTokenContractAsync(tokenAddress);
|
||||
const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
|
||||
let allowanceInBaseUnits = await tokenContract.allowance.callAsync(ownerAddress, spenderAddress, defaultBlock);
|
||||
// Wrap BigNumbers returned from web3 with our own (later) version of BigNumber
|
||||
allowanceInBaseUnits = new BigNumber(allowanceInBaseUnits);
|
||||
return allowanceInBaseUnits;
|
||||
}
|
||||
/**
|
||||
* Retrieves the owner's allowance in baseUnits set to the 0x proxy contract.
|
||||
* @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
|
||||
* @param ownerAddress The hex encoded user Ethereum address whose proxy contract allowance we are retrieving.
|
||||
* @param methodOpts Optional arguments this method accepts.
|
||||
*/
|
||||
public async getProxyAllowanceAsync(tokenAddress: string, ownerAddress: string,
|
||||
methodOpts?: MethodOpts): Promise<BigNumber> {
|
||||
assert.isETHAddressHex('ownerAddress', ownerAddress);
|
||||
assert.isETHAddressHex('tokenAddress', tokenAddress);
|
||||
|
||||
const proxyAddress = await this._getTokenTransferProxyAddressAsync();
|
||||
const allowanceInBaseUnits = await this.getAllowanceAsync(tokenAddress, ownerAddress, proxyAddress, methodOpts);
|
||||
return allowanceInBaseUnits;
|
||||
}
|
||||
/**
|
||||
* Sets the 0x proxy contract's allowance to a specified number of a tokens' baseUnits on behalf
|
||||
* of an owner address.
|
||||
* @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
|
||||
* @param ownerAddress The hex encoded user Ethereum address who is setting an allowance
|
||||
* for the Proxy contract.
|
||||
* @param amountInBaseUnits The allowance amount specified in baseUnits.
|
||||
* @return Transaction hash.
|
||||
*/
|
||||
public async setProxyAllowanceAsync(tokenAddress: string, ownerAddress: string,
|
||||
amountInBaseUnits: BigNumber): Promise<string> {
|
||||
assert.isETHAddressHex('ownerAddress', ownerAddress);
|
||||
assert.isETHAddressHex('tokenAddress', tokenAddress);
|
||||
assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits);
|
||||
|
||||
const proxyAddress = await this._getTokenTransferProxyAddressAsync();
|
||||
const txHash = await this.setAllowanceAsync(tokenAddress, ownerAddress, proxyAddress, amountInBaseUnits);
|
||||
return txHash;
|
||||
}
|
||||
/**
|
||||
* Sets the 0x proxy contract's allowance to a unlimited number of a tokens' baseUnits on behalf
|
||||
* of an owner address.
|
||||
* Setting an unlimited allowance will lower the gas cost for filling orders involving tokens that forego updating
|
||||
* allowances set to the max amount (e.g ZRX, WETH)
|
||||
* @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
|
||||
* @param ownerAddress The hex encoded user Ethereum address who is setting an allowance
|
||||
* for the Proxy contract.
|
||||
* @return Transaction hash.
|
||||
*/
|
||||
public async setUnlimitedProxyAllowanceAsync(tokenAddress: string, ownerAddress: string): Promise<string> {
|
||||
const txHash = await this.setProxyAllowanceAsync(
|
||||
tokenAddress, ownerAddress, this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
|
||||
);
|
||||
return txHash;
|
||||
}
|
||||
/**
|
||||
* Transfers `amountInBaseUnits` ERC20 tokens from `fromAddress` to `toAddress`.
|
||||
* @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
|
||||
* @param fromAddress The hex encoded user Ethereum address that will send the funds.
|
||||
* @param toAddress The hex encoded user Ethereum address that will receive the funds.
|
||||
* @param amountInBaseUnits The amount (specified in baseUnits) of the token to transfer.
|
||||
* @return Transaction hash.
|
||||
*/
|
||||
public async transferAsync(tokenAddress: string, fromAddress: string, toAddress: string,
|
||||
amountInBaseUnits: BigNumber): Promise<string> {
|
||||
assert.isETHAddressHex('tokenAddress', tokenAddress);
|
||||
await assert.isSenderAddressAsync('fromAddress', fromAddress, this._web3Wrapper);
|
||||
assert.isETHAddressHex('toAddress', toAddress);
|
||||
assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits);
|
||||
|
||||
const tokenContract = await this._getTokenContractAsync(tokenAddress);
|
||||
|
||||
const fromAddressBalance = await this.getBalanceAsync(tokenAddress, fromAddress);
|
||||
if (fromAddressBalance.lessThan(amountInBaseUnits)) {
|
||||
throw new Error(ZeroExError.InsufficientBalanceForTransfer);
|
||||
}
|
||||
|
||||
const txHash = await tokenContract.transfer.sendTransactionAsync(toAddress, amountInBaseUnits, {
|
||||
from: fromAddress,
|
||||
});
|
||||
return txHash;
|
||||
}
|
||||
/**
|
||||
* Transfers `amountInBaseUnits` ERC20 tokens from `fromAddress` to `toAddress`.
|
||||
* Requires the fromAddress to have sufficient funds and to have approved an allowance of
|
||||
* `amountInBaseUnits` to `senderAddress`.
|
||||
* @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
|
||||
* @param fromAddress The hex encoded user Ethereum address whose funds are being sent.
|
||||
* @param toAddress The hex encoded user Ethereum address that will receive the funds.
|
||||
* @param senderAddress The hex encoded user Ethereum address whose initiates the fund transfer. The
|
||||
* `fromAddress` must have set an allowance to the `senderAddress`
|
||||
* before this call.
|
||||
* @param amountInBaseUnits The amount (specified in baseUnits) of the token to transfer.
|
||||
* @return Transaction hash.
|
||||
*/
|
||||
public async transferFromAsync(tokenAddress: string, fromAddress: string, toAddress: string,
|
||||
senderAddress: string, amountInBaseUnits: BigNumber):
|
||||
Promise<string> {
|
||||
assert.isETHAddressHex('tokenAddress', tokenAddress);
|
||||
assert.isETHAddressHex('fromAddress', fromAddress);
|
||||
assert.isETHAddressHex('toAddress', toAddress);
|
||||
await assert.isSenderAddressAsync('senderAddress', senderAddress, this._web3Wrapper);
|
||||
assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits);
|
||||
|
||||
const tokenContract = await this._getTokenContractAsync(tokenAddress);
|
||||
|
||||
const fromAddressAllowance = await this.getAllowanceAsync(tokenAddress, fromAddress, senderAddress);
|
||||
if (fromAddressAllowance.lessThan(amountInBaseUnits)) {
|
||||
throw new Error(ZeroExError.InsufficientAllowanceForTransfer);
|
||||
}
|
||||
|
||||
const fromAddressBalance = await this.getBalanceAsync(tokenAddress, fromAddress);
|
||||
if (fromAddressBalance.lessThan(amountInBaseUnits)) {
|
||||
throw new Error(ZeroExError.InsufficientBalanceForTransfer);
|
||||
}
|
||||
|
||||
const txHash = await tokenContract.transferFrom.sendTransactionAsync(
|
||||
fromAddress, toAddress, amountInBaseUnits,
|
||||
{
|
||||
from: senderAddress,
|
||||
},
|
||||
);
|
||||
return txHash;
|
||||
}
|
||||
/**
|
||||
* Subscribe to an event type emitted by the Token contract.
|
||||
* @param tokenAddress The hex encoded address where the ERC20 token is deployed.
|
||||
* @param eventName The token contract event you would like to subscribe to.
|
||||
* @param indexFilterValues An object where the keys are indexed args returned by the event and
|
||||
* the value is the value you are interested in. E.g `{maker: aUserAddressHex}`
|
||||
* @param callback Callback that gets called when a log is added/removed
|
||||
* @return Subscription token used later to unsubscribe
|
||||
*/
|
||||
public subscribe<ArgsType extends TokenContractEventArgs>(
|
||||
tokenAddress: string, eventName: TokenEvents, indexFilterValues: IndexedFilterValues,
|
||||
callback: EventCallback<ArgsType>): string {
|
||||
assert.isETHAddressHex('tokenAddress', tokenAddress);
|
||||
assert.doesBelongToStringEnum('eventName', eventName, TokenEvents);
|
||||
assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
|
||||
assert.isFunction('callback', callback);
|
||||
const subscriptionToken = this._subscribe<ArgsType>(
|
||||
tokenAddress, eventName, indexFilterValues, artifacts.TokenArtifact.abi, callback,
|
||||
);
|
||||
return subscriptionToken;
|
||||
}
|
||||
/**
|
||||
* Cancel a subscription
|
||||
* @param subscriptionToken Subscription token returned by `subscribe()`
|
||||
*/
|
||||
public unsubscribe(subscriptionToken: string): void {
|
||||
this._unsubscribe(subscriptionToken);
|
||||
}
|
||||
/**
|
||||
* Gets historical logs without creating a subscription
|
||||
* @param tokenAddress An address of the token that emmited the logs.
|
||||
* @param eventName The token contract event you would like to subscribe to.
|
||||
* @param subscriptionOpts Subscriptions options that let you configure the subscription.
|
||||
* @param indexFilterValues An object where the keys are indexed args returned by the event and
|
||||
* the value is the value you are interested in. E.g `{_from: aUserAddressHex}`
|
||||
* @return Array of logs that match the parameters
|
||||
*/
|
||||
public async getLogsAsync<ArgsType extends TokenContractEventArgs>(
|
||||
tokenAddress: string, eventName: TokenEvents, subscriptionOpts: SubscriptionOpts,
|
||||
indexFilterValues: IndexedFilterValues): Promise<Array<LogWithDecodedArgs<ArgsType>>> {
|
||||
assert.isETHAddressHex('tokenAddress', tokenAddress);
|
||||
assert.doesBelongToStringEnum('eventName', eventName, TokenEvents);
|
||||
assert.doesConformToSchema('subscriptionOpts', subscriptionOpts, schemas.subscriptionOptsSchema);
|
||||
assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
|
||||
const logs = await this._getLogsAsync<ArgsType>(
|
||||
tokenAddress, eventName, subscriptionOpts, indexFilterValues, artifacts.TokenArtifact.abi,
|
||||
);
|
||||
return logs;
|
||||
}
|
||||
private _invalidateContractInstancesAsync(): void {
|
||||
this.unsubscribeAll();
|
||||
this._tokenContractsByAddress = {};
|
||||
}
|
||||
private async _getTokenContractAsync(tokenAddress: string): Promise<TokenContract> {
|
||||
let tokenContract = this._tokenContractsByAddress[tokenAddress];
|
||||
if (!_.isUndefined(tokenContract)) {
|
||||
return tokenContract;
|
||||
}
|
||||
const contractInstance = await this._instantiateContractIfExistsAsync<TokenContract>(
|
||||
artifacts.TokenArtifact, tokenAddress,
|
||||
);
|
||||
tokenContract = contractInstance as TokenContract;
|
||||
this._tokenContractsByAddress[tokenAddress] = tokenContract;
|
||||
return tokenContract;
|
||||
}
|
||||
private async _getTokenTransferProxyAddressAsync(): Promise<string> {
|
||||
const tokenTransferProxyContractAddress = await this._tokenTransferProxyContractAddressFetcher();
|
||||
return tokenTransferProxyContractAddress;
|
||||
}
|
||||
}
|
80
packages/0x.js/src/globals.d.ts
vendored
Normal file
80
packages/0x.js/src/globals.d.ts
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
/// <reference types='chai-typescript-typings' />
|
||||
/// <reference types='chai-as-promised-typescript-typings' />
|
||||
declare module 'web3_beta';
|
||||
declare module 'chai-bignumber';
|
||||
declare module 'dirty-chai';
|
||||
declare module 'request-promise-native';
|
||||
declare module 'web3-provider-engine';
|
||||
declare module 'web3-provider-engine/subproviders/rpc';
|
||||
|
||||
// HACK: In order to merge the bignumber declaration added by chai-bignumber to the chai Assertion
|
||||
// interface we must use `namespace` as the Chai definitelyTyped definition does. Since we otherwise
|
||||
// disallow `namespace`, we disable tslint for the following.
|
||||
/* tslint:disable */
|
||||
declare namespace Chai {
|
||||
interface Assertion {
|
||||
bignumber: Assertion;
|
||||
// HACK: In order to comply with chai-as-promised we make eventually a `PromisedAssertion` not an `Assertion`
|
||||
eventually: PromisedAssertion;
|
||||
}
|
||||
}
|
||||
/* tslint:enable */
|
||||
|
||||
declare module '*.json' {
|
||||
const json: any;
|
||||
/* tslint:disable */
|
||||
export default json;
|
||||
/* tslint:enable */
|
||||
}
|
||||
|
||||
// find-version declarations
|
||||
declare function findVersions(version: string): string[];
|
||||
declare module 'find-versions' {
|
||||
export = findVersions;
|
||||
}
|
||||
|
||||
// compare-version declarations
|
||||
declare function compareVersions(firstVersion: string, secondVersion: string): number;
|
||||
declare module 'compare-versions' {
|
||||
export = compareVersions;
|
||||
}
|
||||
|
||||
// es6-promisify declarations
|
||||
declare function promisify(original: any, settings?: any): ((...arg: any[]) => Promise<any>);
|
||||
declare module 'es6-promisify' {
|
||||
export = promisify;
|
||||
}
|
||||
|
||||
declare module 'ethereumjs-abi' {
|
||||
const soliditySHA3: (argTypes: string[], args: any[]) => Buffer;
|
||||
}
|
||||
|
||||
// truffle-hdwallet-provider declarations
|
||||
declare module 'truffle-hdwallet-provider' {
|
||||
import * as Web3 from 'web3';
|
||||
class HDWalletProvider implements Web3.Provider {
|
||||
constructor(mnemonic: string, rpcUrl: string);
|
||||
public sendAsync(
|
||||
payload: Web3.JSONRPCRequestPayload,
|
||||
callback: (err: Error, result: Web3.JSONRPCResponsePayload) => void,
|
||||
): void;
|
||||
}
|
||||
export = HDWalletProvider;
|
||||
}
|
||||
|
||||
// abi-decoder declarations
|
||||
interface DecodedLogArg {
|
||||
}
|
||||
interface DecodedLog {
|
||||
name: string;
|
||||
events: DecodedLogArg[];
|
||||
}
|
||||
declare module 'abi-decoder' {
|
||||
import * as Web3 from 'web3';
|
||||
const addABI: (abi: Web3.AbiDefinition) => void;
|
||||
const decodeLogs: (logs: Web3.LogEntry[]) => DecodedLog[];
|
||||
}
|
||||
|
||||
declare module 'web3/lib/solidity/coder' {
|
||||
const decodeParams: (types: string[], data: string) => any[];
|
||||
}
|
23
packages/0x.js/src/globalsAugment.d.ts
vendored
Normal file
23
packages/0x.js/src/globalsAugment.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
// HACK: This module overrides the Chai namespace so that we can use BigNumber types inside.
|
||||
// Source: https://github.com/Microsoft/TypeScript/issues/7352#issuecomment-191547232
|
||||
declare global {
|
||||
// HACK: In order to merge the bignumber declaration added by chai-bignumber to the chai Assertion
|
||||
// interface we must use `namespace` as the Chai definitelyTyped definition does. Since we otherwise
|
||||
// disallow `namespace`, we disable tslint for the following.
|
||||
/* tslint:disable */
|
||||
namespace Chai {
|
||||
interface NumberComparer {
|
||||
(value: number|BigNumber, message?: string): Assertion;
|
||||
}
|
||||
interface NumericComparison {
|
||||
greaterThan: NumberComparer;
|
||||
}
|
||||
}
|
||||
/* tslint:enable */
|
||||
interface DecodedLogArg {
|
||||
name: string;
|
||||
value: string|BigNumber;
|
||||
}
|
||||
}
|
45
packages/0x.js/src/index.ts
Normal file
45
packages/0x.js/src/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export {ZeroEx} from './0x';
|
||||
|
||||
export {
|
||||
Order,
|
||||
SignedOrder,
|
||||
ECSignature,
|
||||
ZeroExError,
|
||||
EventCallback,
|
||||
EventCallbackAsync,
|
||||
EventCallbackSync,
|
||||
ExchangeContractErrs,
|
||||
ContractEvent,
|
||||
Token,
|
||||
ExchangeEvents,
|
||||
TokenEvents,
|
||||
IndexedFilterValues,
|
||||
SubscriptionOpts,
|
||||
BlockParam,
|
||||
OrderCancellationRequest,
|
||||
OrderFillRequest,
|
||||
LogErrorContractEventArgs,
|
||||
LogCancelContractEventArgs,
|
||||
LogFillContractEventArgs,
|
||||
ExchangeContractEventArgs,
|
||||
TransferContractEventArgs,
|
||||
ApprovalContractEventArgs,
|
||||
TokenContractEventArgs,
|
||||
ContractEventArgs,
|
||||
ContractEventArg,
|
||||
Web3Provider,
|
||||
ZeroExConfig,
|
||||
TransactionReceipt,
|
||||
TransactionReceiptWithDecodedLogs,
|
||||
LogWithDecodedArgs,
|
||||
MethodOpts,
|
||||
OrderTransactionOpts,
|
||||
FilterObject,
|
||||
LogEvent,
|
||||
DecodedLogEvent,
|
||||
EventWatcherCallback,
|
||||
OnOrderStateChangeCallback,
|
||||
OrderStateValid,
|
||||
OrderStateInvalid,
|
||||
OrderState,
|
||||
} from './types';
|
88
packages/0x.js/src/order_watcher/event_watcher.ts
Normal file
88
packages/0x.js/src/order_watcher/event_watcher.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import * as Web3 from 'web3';
|
||||
import * as _ from 'lodash';
|
||||
import {Web3Wrapper} from '../web3_wrapper';
|
||||
import {
|
||||
BlockParamLiteral,
|
||||
EventCallback,
|
||||
EventWatcherCallback,
|
||||
ZeroExError,
|
||||
} from '../types';
|
||||
import {AbiDecoder} from '../utils/abi_decoder';
|
||||
import {intervalUtils} from '../utils/interval_utils';
|
||||
import {assert} from '../utils/assert';
|
||||
import {utils} from '../utils/utils';
|
||||
|
||||
const DEFAULT_EVENT_POLLING_INTERVAL = 200;
|
||||
|
||||
enum LogEventState {
|
||||
Removed,
|
||||
Added,
|
||||
}
|
||||
|
||||
/*
|
||||
* The EventWatcher watches for blockchain events at the specified block confirmation
|
||||
* depth.
|
||||
*/
|
||||
export class EventWatcher {
|
||||
private _web3Wrapper: Web3Wrapper;
|
||||
private _pollingIntervalMs: number;
|
||||
private _intervalIdIfExists?: NodeJS.Timer;
|
||||
private _lastEvents: Web3.LogEntry[] = [];
|
||||
constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number) {
|
||||
this._web3Wrapper = web3Wrapper;
|
||||
this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ?
|
||||
DEFAULT_EVENT_POLLING_INTERVAL :
|
||||
pollingIntervalMs;
|
||||
}
|
||||
public subscribe(callback: EventWatcherCallback): void {
|
||||
assert.isFunction('callback', callback);
|
||||
if (!_.isUndefined(this._intervalIdIfExists)) {
|
||||
throw new Error(ZeroExError.SubscriptionAlreadyPresent);
|
||||
}
|
||||
this._intervalIdIfExists = intervalUtils.setAsyncExcludingInterval(
|
||||
this._pollForBlockchainEventsAsync.bind(this, callback), this._pollingIntervalMs,
|
||||
);
|
||||
}
|
||||
public unsubscribe(): void {
|
||||
this._lastEvents = [];
|
||||
if (!_.isUndefined(this._intervalIdIfExists)) {
|
||||
intervalUtils.clearAsyncExcludingInterval(this._intervalIdIfExists);
|
||||
delete this._intervalIdIfExists;
|
||||
}
|
||||
}
|
||||
private async _pollForBlockchainEventsAsync(callback: EventWatcherCallback): Promise<void> {
|
||||
const pendingEvents = await this._getEventsAsync();
|
||||
if (pendingEvents.length === 0) {
|
||||
// HACK: Sometimes when node rebuilds the pending block we get back the empty result.
|
||||
// We don't want to emit a lot of removal events and bring them back after a couple of miliseconds,
|
||||
// that's why we just ignore those cases.
|
||||
return;
|
||||
}
|
||||
const removedEvents = _.differenceBy(this._lastEvents, pendingEvents, JSON.stringify);
|
||||
const newEvents = _.differenceBy(pendingEvents, this._lastEvents, JSON.stringify);
|
||||
await this._emitDifferencesAsync(removedEvents, LogEventState.Removed, callback);
|
||||
await this._emitDifferencesAsync(newEvents, LogEventState.Added, callback);
|
||||
this._lastEvents = pendingEvents;
|
||||
}
|
||||
private async _getEventsAsync(): Promise<Web3.LogEntry[]> {
|
||||
const eventFilter = {
|
||||
fromBlock: BlockParamLiteral.Pending,
|
||||
toBlock: BlockParamLiteral.Pending,
|
||||
};
|
||||
const events = await this._web3Wrapper.getLogsAsync(eventFilter);
|
||||
return events;
|
||||
}
|
||||
private async _emitDifferencesAsync(
|
||||
logs: Web3.LogEntry[], logEventState: LogEventState, callback: EventWatcherCallback,
|
||||
): Promise<void> {
|
||||
for (const log of logs) {
|
||||
const logEvent = {
|
||||
removed: logEventState === LogEventState.Removed,
|
||||
...log,
|
||||
};
|
||||
if (!_.isUndefined(this._intervalIdIfExists)) {
|
||||
await callback(logEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
232
packages/0x.js/src/order_watcher/order_state_watcher.ts
Normal file
232
packages/0x.js/src/order_watcher/order_state_watcher.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import * as _ from 'lodash';
|
||||
import {schemas} from '0x-json-schemas';
|
||||
import {ZeroEx} from '../0x';
|
||||
import {EventWatcher} from './event_watcher';
|
||||
import {assert} from '../utils/assert';
|
||||
import {utils} from '../utils/utils';
|
||||
import {artifacts} from '../artifacts';
|
||||
import {AbiDecoder} from '../utils/abi_decoder';
|
||||
import {OrderStateUtils} from '../utils/order_state_utils';
|
||||
import {
|
||||
LogEvent,
|
||||
OrderState,
|
||||
SignedOrder,
|
||||
Web3Provider,
|
||||
BlockParamLiteral,
|
||||
LogWithDecodedArgs,
|
||||
ContractEventArgs,
|
||||
OnOrderStateChangeCallback,
|
||||
OrderStateWatcherConfig,
|
||||
ApprovalContractEventArgs,
|
||||
TransferContractEventArgs,
|
||||
LogFillContractEventArgs,
|
||||
LogCancelContractEventArgs,
|
||||
ExchangeEvents,
|
||||
TokenEvents,
|
||||
ZeroExError,
|
||||
} from '../types';
|
||||
import {Web3Wrapper} from '../web3_wrapper';
|
||||
import {TokenWrapper} from '../contract_wrappers/token_wrapper';
|
||||
import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper';
|
||||
import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store';
|
||||
import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store';
|
||||
|
||||
const DEFAULT_NUM_CONFIRMATIONS = 0;
|
||||
|
||||
interface DependentOrderHashes {
|
||||
[makerAddress: string]: {
|
||||
[makerToken: string]: Set<string>,
|
||||
};
|
||||
}
|
||||
|
||||
interface OrderByOrderHash {
|
||||
[orderHash: string]: SignedOrder;
|
||||
}
|
||||
|
||||
/**
|
||||
* This class includes all the functionality related to watching a set of orders
|
||||
* for potential changes in order validity/fillability. The orderWatcher notifies
|
||||
* the subscriber of these changes so that a final decison can be made on whether
|
||||
* the order should be deemed invalid.
|
||||
*/
|
||||
export class OrderStateWatcher {
|
||||
private _orderByOrderHash: OrderByOrderHash = {};
|
||||
private _dependentOrderHashes: DependentOrderHashes = {};
|
||||
private _callbackIfExistsAsync?: OnOrderStateChangeCallback;
|
||||
private _eventWatcher: EventWatcher;
|
||||
private _web3Wrapper: Web3Wrapper;
|
||||
private _abiDecoder: AbiDecoder;
|
||||
private _orderStateUtils: OrderStateUtils;
|
||||
private _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore;
|
||||
private _balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore;
|
||||
constructor(
|
||||
web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, token: TokenWrapper, exchange: ExchangeWrapper,
|
||||
config?: OrderStateWatcherConfig,
|
||||
) {
|
||||
this._abiDecoder = abiDecoder;
|
||||
this._web3Wrapper = web3Wrapper;
|
||||
const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs;
|
||||
this._eventWatcher = new EventWatcher(web3Wrapper, eventPollingIntervalMs);
|
||||
this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore(token);
|
||||
this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore(exchange);
|
||||
this._orderStateUtils = new OrderStateUtils(
|
||||
this._balanceAndProxyAllowanceLazyStore, this._orderFilledCancelledLazyStore,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Add an order to the orderStateWatcher. Before the order is added, it's
|
||||
* signature is verified.
|
||||
* @param signedOrder The order you wish to start watching.
|
||||
*/
|
||||
public addOrder(signedOrder: SignedOrder): void {
|
||||
assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
assert.isValidSignature(orderHash, signedOrder.ecSignature, signedOrder.maker);
|
||||
this._orderByOrderHash[orderHash] = signedOrder;
|
||||
this.addToDependentOrderHashes(signedOrder, orderHash);
|
||||
}
|
||||
/**
|
||||
* Removes an order from the orderStateWatcher
|
||||
* @param orderHash The orderHash of the order you wish to stop watching.
|
||||
*/
|
||||
public removeOrder(orderHash: string): void {
|
||||
assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema);
|
||||
const signedOrder = this._orderByOrderHash[orderHash];
|
||||
if (_.isUndefined(signedOrder)) {
|
||||
return; // noop
|
||||
}
|
||||
delete this._orderByOrderHash[orderHash];
|
||||
this.removeFromDependentOrderHashes(signedOrder.maker, signedOrder.makerTokenAddress, orderHash);
|
||||
}
|
||||
/**
|
||||
* Starts an orderStateWatcher subscription. The callback will be called every time a watched order's
|
||||
* backing blockchain state has changed. This is a call-to-action for the caller to re-validate the order.
|
||||
* @param callback Receives the orderHash of the order that should be re-validated, together
|
||||
* with all the order-relevant blockchain state needed to re-validate the order.
|
||||
*/
|
||||
public subscribe(callback: OnOrderStateChangeCallback): void {
|
||||
assert.isFunction('callback', callback);
|
||||
if (!_.isUndefined(this._callbackIfExistsAsync)) {
|
||||
throw new Error(ZeroExError.SubscriptionAlreadyPresent);
|
||||
}
|
||||
this._callbackIfExistsAsync = callback;
|
||||
this._eventWatcher.subscribe(this._onEventWatcherCallbackAsync.bind(this));
|
||||
}
|
||||
/**
|
||||
* Ends an orderStateWatcher subscription.
|
||||
*/
|
||||
public unsubscribe(): void {
|
||||
if (_.isUndefined(this._callbackIfExistsAsync)) {
|
||||
throw new Error(ZeroExError.SubscriptionNotFound);
|
||||
}
|
||||
this._balanceAndProxyAllowanceLazyStore.deleteAll();
|
||||
this._orderFilledCancelledLazyStore.deleteAll();
|
||||
delete this._callbackIfExistsAsync;
|
||||
this._eventWatcher.unsubscribe();
|
||||
}
|
||||
private async _onEventWatcherCallbackAsync(log: LogEvent): Promise<void> {
|
||||
const maybeDecodedLog = this._abiDecoder.tryToDecodeLogOrNoop(log);
|
||||
const isLogDecoded = !_.isUndefined((maybeDecodedLog as LogWithDecodedArgs<any>).event);
|
||||
if (!isLogDecoded) {
|
||||
return; // noop
|
||||
}
|
||||
const decodedLog = maybeDecodedLog as LogWithDecodedArgs<ContractEventArgs>;
|
||||
let makerToken: string;
|
||||
let makerAddress: string;
|
||||
let orderHashesSet: Set<string>;
|
||||
switch (decodedLog.event) {
|
||||
case TokenEvents.Approval:
|
||||
{
|
||||
// Invalidate cache
|
||||
const args = decodedLog.args as ApprovalContractEventArgs;
|
||||
this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(decodedLog.address, args._owner);
|
||||
// Revalidate orders
|
||||
makerToken = decodedLog.address;
|
||||
makerAddress = args._owner;
|
||||
orderHashesSet = _.get(this._dependentOrderHashes, [makerAddress, makerToken]);
|
||||
if (!_.isUndefined(orderHashesSet)) {
|
||||
const orderHashes = Array.from(orderHashesSet);
|
||||
await this._emitRevalidateOrdersAsync(orderHashes);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TokenEvents.Transfer:
|
||||
{
|
||||
// Invalidate cache
|
||||
const args = decodedLog.args as TransferContractEventArgs;
|
||||
this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._from);
|
||||
this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._to);
|
||||
// Revalidate orders
|
||||
makerToken = decodedLog.address;
|
||||
makerAddress = args._from;
|
||||
orderHashesSet = _.get(this._dependentOrderHashes, [makerAddress, makerToken]);
|
||||
if (!_.isUndefined(orderHashesSet)) {
|
||||
const orderHashes = Array.from(orderHashesSet);
|
||||
await this._emitRevalidateOrdersAsync(orderHashes);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ExchangeEvents.LogFill:
|
||||
{
|
||||
// Invalidate cache
|
||||
const args = decodedLog.args as LogFillContractEventArgs;
|
||||
this._orderFilledCancelledLazyStore.deleteFilledTakerAmount(args.orderHash);
|
||||
// Revalidate orders
|
||||
const orderHash = args.orderHash;
|
||||
const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]);
|
||||
if (isOrderWatched) {
|
||||
await this._emitRevalidateOrdersAsync([orderHash]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ExchangeEvents.LogCancel:
|
||||
{
|
||||
// Invalidate cache
|
||||
const args = decodedLog.args as LogCancelContractEventArgs;
|
||||
this._orderFilledCancelledLazyStore.deleteCancelledTakerAmount(args.orderHash);
|
||||
// Revalidate orders
|
||||
const orderHash = args.orderHash;
|
||||
const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]);
|
||||
if (isOrderWatched) {
|
||||
await this._emitRevalidateOrdersAsync([orderHash]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ExchangeEvents.LogError:
|
||||
return; // noop
|
||||
|
||||
default:
|
||||
throw utils.spawnSwitchErr('decodedLog.event', decodedLog.event);
|
||||
}
|
||||
}
|
||||
private async _emitRevalidateOrdersAsync(orderHashes: string[]): Promise<void> {
|
||||
for (const orderHash of orderHashes) {
|
||||
const signedOrder = this._orderByOrderHash[orderHash] as SignedOrder;
|
||||
// Most of these calls will never reach the network because the data is fetched from stores
|
||||
// and only updated when cache is invalidated
|
||||
const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder);
|
||||
if (_.isUndefined(this._callbackIfExistsAsync)) {
|
||||
break; // Unsubscribe was called
|
||||
}
|
||||
await this._callbackIfExistsAsync(orderState);
|
||||
}
|
||||
}
|
||||
private addToDependentOrderHashes(signedOrder: SignedOrder, orderHash: string) {
|
||||
if (_.isUndefined(this._dependentOrderHashes[signedOrder.maker])) {
|
||||
this._dependentOrderHashes[signedOrder.maker] = {};
|
||||
}
|
||||
if (_.isUndefined(this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress])) {
|
||||
this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress] = new Set();
|
||||
}
|
||||
this._dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress].add(orderHash);
|
||||
}
|
||||
private removeFromDependentOrderHashes(makerAddress: string, makerTokenAddress: string, orderHash: string) {
|
||||
this._dependentOrderHashes[makerAddress][makerTokenAddress].delete(orderHash);
|
||||
if (this._dependentOrderHashes[makerAddress][makerTokenAddress].size === 0) {
|
||||
delete this._dependentOrderHashes[makerAddress][makerTokenAddress];
|
||||
}
|
||||
if (_.isEmpty(this._dependentOrderHashes[makerAddress])) {
|
||||
delete this._dependentOrderHashes[makerAddress];
|
||||
}
|
||||
}
|
||||
}
|
23
packages/0x.js/src/schemas/zero_ex_config_schema.ts
Normal file
23
packages/0x.js/src/schemas/zero_ex_config_schema.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export const zeroExConfigSchema = {
|
||||
id: '/ZeroExConfig',
|
||||
properties: {
|
||||
gasPrice: {$ref: '/Number'},
|
||||
exchangeContractAddress: {$ref: '/Address'},
|
||||
tokenRegistryContractAddress: {$ref: '/Address'},
|
||||
etherTokenContractAddress: {$ref: '/Address'},
|
||||
orderWatcherConfig: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
pollingIntervalMs: {
|
||||
type: 'number',
|
||||
minimum: 0,
|
||||
},
|
||||
numConfirmations: {
|
||||
type: 'number',
|
||||
minimum: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
type: 'object',
|
||||
};
|
@@ -0,0 +1,82 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as Web3 from 'web3';
|
||||
import {BigNumber} from 'bignumber.js';
|
||||
import {TokenWrapper} from '../contract_wrappers/token_wrapper';
|
||||
import {BlockParamLiteral} from '../types';
|
||||
|
||||
/**
|
||||
* Copy on read store for balances/proxyAllowances of tokens/accounts
|
||||
*/
|
||||
export class BalanceAndProxyAllowanceLazyStore {
|
||||
private token: TokenWrapper;
|
||||
private balance: {
|
||||
[tokenAddress: string]: {
|
||||
[userAddress: string]: BigNumber,
|
||||
},
|
||||
};
|
||||
private proxyAllowance: {
|
||||
[tokenAddress: string]: {
|
||||
[userAddress: string]: BigNumber,
|
||||
},
|
||||
};
|
||||
constructor(token: TokenWrapper) {
|
||||
this.token = token;
|
||||
this.balance = {};
|
||||
this.proxyAllowance = {};
|
||||
}
|
||||
public async getBalanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> {
|
||||
if (_.isUndefined(this.balance[tokenAddress]) || _.isUndefined(this.balance[tokenAddress][userAddress])) {
|
||||
const methodOpts = {
|
||||
defaultBlock: BlockParamLiteral.Pending,
|
||||
};
|
||||
const balance = await this.token.getBalanceAsync(tokenAddress, userAddress, methodOpts);
|
||||
this.setBalance(tokenAddress, userAddress, balance);
|
||||
}
|
||||
const cachedBalance = this.balance[tokenAddress][userAddress];
|
||||
return cachedBalance;
|
||||
}
|
||||
public setBalance(tokenAddress: string, userAddress: string, balance: BigNumber): void {
|
||||
if (_.isUndefined(this.balance[tokenAddress])) {
|
||||
this.balance[tokenAddress] = {};
|
||||
}
|
||||
this.balance[tokenAddress][userAddress] = balance;
|
||||
}
|
||||
public deleteBalance(tokenAddress: string, userAddress: string): void {
|
||||
if (!_.isUndefined(this.balance[tokenAddress])) {
|
||||
delete this.balance[tokenAddress][userAddress];
|
||||
if (_.isEmpty(this.balance[tokenAddress])) {
|
||||
delete this.balance[tokenAddress];
|
||||
}
|
||||
}
|
||||
}
|
||||
public async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> {
|
||||
if (_.isUndefined(this.proxyAllowance[tokenAddress]) ||
|
||||
_.isUndefined(this.proxyAllowance[tokenAddress][userAddress])) {
|
||||
const methodOpts = {
|
||||
defaultBlock: BlockParamLiteral.Pending,
|
||||
};
|
||||
const proxyAllowance = await this.token.getProxyAllowanceAsync(tokenAddress, userAddress, methodOpts);
|
||||
this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance);
|
||||
}
|
||||
const cachedProxyAllowance = this.proxyAllowance[tokenAddress][userAddress];
|
||||
return cachedProxyAllowance;
|
||||
}
|
||||
public setProxyAllowance(tokenAddress: string, userAddress: string, proxyAllowance: BigNumber): void {
|
||||
if (_.isUndefined(this.proxyAllowance[tokenAddress])) {
|
||||
this.proxyAllowance[tokenAddress] = {};
|
||||
}
|
||||
this.proxyAllowance[tokenAddress][userAddress] = proxyAllowance;
|
||||
}
|
||||
public deleteProxyAllowance(tokenAddress: string, userAddress: string): void {
|
||||
if (!_.isUndefined(this.proxyAllowance[tokenAddress])) {
|
||||
delete this.proxyAllowance[tokenAddress][userAddress];
|
||||
if (_.isEmpty(this.proxyAllowance[tokenAddress])) {
|
||||
delete this.proxyAllowance[tokenAddress];
|
||||
}
|
||||
}
|
||||
}
|
||||
public deleteAll(): void {
|
||||
this.balance = {};
|
||||
this.proxyAllowance = {};
|
||||
}
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as Web3 from 'web3';
|
||||
import {BigNumber} from 'bignumber.js';
|
||||
import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper';
|
||||
import {BlockParamLiteral} from '../types';
|
||||
|
||||
/**
|
||||
* Copy on read store for filled/cancelled taker amounts
|
||||
*/
|
||||
export class OrderFilledCancelledLazyStore {
|
||||
private exchange: ExchangeWrapper;
|
||||
private filledTakerAmount: {
|
||||
[orderHash: string]: BigNumber,
|
||||
};
|
||||
private cancelledTakerAmount: {
|
||||
[orderHash: string]: BigNumber,
|
||||
};
|
||||
constructor(exchange: ExchangeWrapper) {
|
||||
this.exchange = exchange;
|
||||
this.filledTakerAmount = {};
|
||||
this.cancelledTakerAmount = {};
|
||||
}
|
||||
public async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber> {
|
||||
if (_.isUndefined(this.filledTakerAmount[orderHash])) {
|
||||
const methodOpts = {
|
||||
defaultBlock: BlockParamLiteral.Pending,
|
||||
};
|
||||
const filledTakerAmount = await this.exchange.getFilledTakerAmountAsync(orderHash, methodOpts);
|
||||
this.setFilledTakerAmount(orderHash, filledTakerAmount);
|
||||
}
|
||||
const cachedFilled = this.filledTakerAmount[orderHash];
|
||||
return cachedFilled;
|
||||
}
|
||||
public setFilledTakerAmount(orderHash: string, filledTakerAmount: BigNumber): void {
|
||||
this.filledTakerAmount[orderHash] = filledTakerAmount;
|
||||
}
|
||||
public deleteFilledTakerAmount(orderHash: string): void {
|
||||
delete this.filledTakerAmount[orderHash];
|
||||
}
|
||||
public async getCancelledTakerAmountAsync(orderHash: string): Promise<BigNumber> {
|
||||
if (_.isUndefined(this.cancelledTakerAmount[orderHash])) {
|
||||
const methodOpts = {
|
||||
defaultBlock: BlockParamLiteral.Pending,
|
||||
};
|
||||
const cancelledTakerAmount = await this.exchange.getCanceledTakerAmountAsync(orderHash, methodOpts);
|
||||
this.setCancelledTakerAmount(orderHash, cancelledTakerAmount);
|
||||
}
|
||||
const cachedCancelled = this.cancelledTakerAmount[orderHash];
|
||||
return cachedCancelled;
|
||||
}
|
||||
public setCancelledTakerAmount(orderHash: string, cancelledTakerAmount: BigNumber): void {
|
||||
this.cancelledTakerAmount[orderHash] = cancelledTakerAmount;
|
||||
}
|
||||
public deleteCancelledTakerAmount(orderHash: string): void {
|
||||
delete this.cancelledTakerAmount[orderHash];
|
||||
}
|
||||
public deleteAll(): void {
|
||||
this.filledTakerAmount = {};
|
||||
this.cancelledTakerAmount = {};
|
||||
}
|
||||
}
|
24
packages/0x.js/src/subproviders/empty_wallet_subprovider.ts
Normal file
24
packages/0x.js/src/subproviders/empty_wallet_subprovider.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {JSONRPCPayload} from '../types';
|
||||
|
||||
/*
|
||||
* This class implements the web3-provider-engine subprovider interface and returns
|
||||
* that the provider has no addresses when queried.
|
||||
* Source: https://github.com/MetaMask/provider-engine/blob/master/subproviders/subprovider.js
|
||||
*/
|
||||
export class EmptyWalletSubProvider {
|
||||
public handleRequest(payload: JSONRPCPayload, next: () => void, end: (err: Error|null, result: any) => void) {
|
||||
switch (payload.method) {
|
||||
case 'eth_accounts':
|
||||
end(null, []);
|
||||
return;
|
||||
|
||||
default:
|
||||
next();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Required to implement this method despite not needing it for this subprovider
|
||||
public setEngine(engine: any) {
|
||||
// noop
|
||||
}
|
||||
}
|
525
packages/0x.js/src/types.ts
Normal file
525
packages/0x.js/src/types.ts
Normal file
@@ -0,0 +1,525 @@
|
||||
import * as Web3 from 'web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
export enum ZeroExError {
|
||||
ContractDoesNotExist = 'CONTRACT_DOES_NOT_EXIST',
|
||||
ExchangeContractDoesNotExist = 'EXCHANGE_CONTRACT_DOES_NOT_EXIST',
|
||||
UnhandledError = 'UNHANDLED_ERROR',
|
||||
UserHasNoAssociatedAddress = 'USER_HAS_NO_ASSOCIATED_ADDRESSES',
|
||||
InvalidSignature = 'INVALID_SIGNATURE',
|
||||
ContractNotDeployedOnNetwork = 'CONTRACT_NOT_DEPLOYED_ON_NETWORK',
|
||||
InsufficientAllowanceForTransfer = 'INSUFFICIENT_ALLOWANCE_FOR_TRANSFER',
|
||||
InsufficientBalanceForTransfer = 'INSUFFICIENT_BALANCE_FOR_TRANSFER',
|
||||
InsufficientEthBalanceForDeposit = 'INSUFFICIENT_ETH_BALANCE_FOR_DEPOSIT',
|
||||
InsufficientWEthBalanceForWithdrawal = 'INSUFFICIENT_WETH_BALANCE_FOR_WITHDRAWAL',
|
||||
InvalidJump = 'INVALID_JUMP',
|
||||
OutOfGas = 'OUT_OF_GAS',
|
||||
NoNetworkId = 'NO_NETWORK_ID',
|
||||
SubscriptionNotFound = 'SUBSCRIPTION_NOT_FOUND',
|
||||
SubscriptionAlreadyPresent = 'SUBSCRIPTION_ALREADY_PRESENT',
|
||||
TransactionMiningTimeout = 'TRANSACTION_MINING_TIMEOUT',
|
||||
}
|
||||
|
||||
export enum InternalZeroExError {
|
||||
NoAbiDecoder = 'NO_ABI_DECODER',
|
||||
ZrxNotInTokenRegistry = 'ZRX_NOT_IN_TOKEN_REGISTRY',
|
||||
}
|
||||
|
||||
/**
|
||||
* Elliptic Curve signature
|
||||
*/
|
||||
export interface ECSignature {
|
||||
v: number;
|
||||
r: string;
|
||||
s: string;
|
||||
}
|
||||
|
||||
export type OrderAddresses = [string, string, string, string, string];
|
||||
|
||||
export type OrderValues = [BigNumber, BigNumber, BigNumber,
|
||||
BigNumber, BigNumber, BigNumber];
|
||||
|
||||
export type LogEvent = Web3.LogEntryEvent;
|
||||
export type DecodedLogEvent<ArgsType> = Web3.DecodedLogEntryEvent<ArgsType>;
|
||||
|
||||
export type EventCallbackAsync<ArgsType> = (err: null|Error, log?: DecodedLogEvent<ArgsType>) => Promise<void>;
|
||||
export type EventCallbackSync<ArgsType> = (err: null|Error, log?: DecodedLogEvent<ArgsType>) => void;
|
||||
export type EventCallback<ArgsType> = EventCallbackSync<ArgsType>|EventCallbackAsync<ArgsType>;
|
||||
|
||||
export type EventWatcherCallbackSync = (log: LogEvent) => void;
|
||||
export type EventWatcherCallbackAsync = (log: LogEvent) => Promise<void>;
|
||||
export type EventWatcherCallback = EventWatcherCallbackSync|EventWatcherCallbackAsync;
|
||||
|
||||
export interface ExchangeContract extends Web3.ContractInstance {
|
||||
isValidSignature: {
|
||||
callAsync: (signerAddressHex: string, dataHex: string, v: number, r: string, s: string,
|
||||
txOpts?: TxOpts) => Promise<boolean>;
|
||||
};
|
||||
ZRX_TOKEN_CONTRACT: {
|
||||
callAsync: () => Promise<string>;
|
||||
};
|
||||
TOKEN_TRANSFER_PROXY_CONTRACT: {
|
||||
callAsync: () => Promise<string>;
|
||||
};
|
||||
getUnavailableTakerTokenAmount: {
|
||||
callAsync: (orderHash: string, defaultBlock?: Web3.BlockParam) => Promise<BigNumber>;
|
||||
};
|
||||
isRoundingError: {
|
||||
callAsync: (takerTokenFillAmount: BigNumber, takerTokenAmount: BigNumber,
|
||||
makerTokenAmount: BigNumber, txOpts?: TxOpts) => Promise<boolean>;
|
||||
};
|
||||
fillOrder: {
|
||||
sendTransactionAsync: (orderAddresses: OrderAddresses, orderValues: OrderValues,
|
||||
fillTakerTokenAmount: BigNumber,
|
||||
shouldThrowOnInsufficientBalanceOrAllowance: boolean,
|
||||
v: number, r: string, s: string, txOpts?: TxOpts) => Promise<string>;
|
||||
estimateGasAsync: (orderAddresses: OrderAddresses, orderValues: OrderValues,
|
||||
fillTakerTokenAmount: BigNumber,
|
||||
shouldThrowOnInsufficientBalanceOrAllowance: boolean,
|
||||
v: number, r: string, s: string, txOpts?: TxOpts) => Promise<number>;
|
||||
};
|
||||
batchFillOrders: {
|
||||
sendTransactionAsync: (orderAddresses: OrderAddresses[], orderValues: OrderValues[],
|
||||
fillTakerTokenAmounts: BigNumber[],
|
||||
shouldThrowOnInsufficientBalanceOrAllowance: boolean,
|
||||
v: number[], r: string[], s: string[], txOpts?: TxOpts) => Promise<string>;
|
||||
estimateGasAsync: (orderAddresses: OrderAddresses[], orderValues: OrderValues[],
|
||||
fillTakerTokenAmounts: BigNumber[],
|
||||
shouldThrowOnInsufficientBalanceOrAllowance: boolean,
|
||||
v: number[], r: string[], s: string[], txOpts?: TxOpts) => Promise<number>;
|
||||
};
|
||||
fillOrdersUpTo: {
|
||||
sendTransactionAsync: (orderAddresses: OrderAddresses[], orderValues: OrderValues[],
|
||||
fillTakerTokenAmount: BigNumber,
|
||||
shouldThrowOnInsufficientBalanceOrAllowance: boolean,
|
||||
v: number[], r: string[], s: string[], txOpts?: TxOpts) => Promise<string>;
|
||||
estimateGasAsync: (orderAddresses: OrderAddresses[], orderValues: OrderValues[],
|
||||
fillTakerTokenAmount: BigNumber,
|
||||
shouldThrowOnInsufficientBalanceOrAllowance: boolean,
|
||||
v: number[], r: string[], s: string[], txOpts?: TxOpts) => Promise<number>;
|
||||
};
|
||||
cancelOrder: {
|
||||
sendTransactionAsync: (orderAddresses: OrderAddresses, orderValues: OrderValues,
|
||||
cancelTakerTokenAmount: BigNumber, txOpts?: TxOpts) => Promise<string>;
|
||||
estimateGasAsync: (orderAddresses: OrderAddresses, orderValues: OrderValues,
|
||||
cancelTakerTokenAmount: BigNumber,
|
||||
txOpts?: TxOpts) => Promise<number>;
|
||||
};
|
||||
batchCancelOrders: {
|
||||
sendTransactionAsync: (orderAddresses: OrderAddresses[], orderValues: OrderValues[],
|
||||
cancelTakerTokenAmounts: BigNumber[], txOpts?: TxOpts) => Promise<string>;
|
||||
estimateGasAsync: (orderAddresses: OrderAddresses[], orderValues: OrderValues[],
|
||||
cancelTakerTokenAmounts: BigNumber[],
|
||||
txOpts?: TxOpts) => Promise<number>;
|
||||
};
|
||||
fillOrKillOrder: {
|
||||
sendTransactionAsync: (orderAddresses: OrderAddresses, orderValues: OrderValues,
|
||||
fillTakerTokenAmount: BigNumber,
|
||||
v: number, r: string, s: string, txOpts?: TxOpts) => Promise<string>;
|
||||
estimateGasAsync: (orderAddresses: OrderAddresses, orderValues: OrderValues,
|
||||
fillTakerTokenAmount: BigNumber,
|
||||
v: number, r: string, s: string, txOpts?: TxOpts) => Promise<number>;
|
||||
};
|
||||
batchFillOrKillOrders: {
|
||||
sendTransactionAsync: (orderAddresses: OrderAddresses[], orderValues: OrderValues[],
|
||||
fillTakerTokenAmounts: BigNumber[],
|
||||
v: number[], r: string[], s: string[], txOpts: TxOpts) => Promise<string>;
|
||||
estimateGasAsync: (orderAddresses: OrderAddresses[], orderValues: OrderValues[],
|
||||
fillTakerTokenAmounts: BigNumber[],
|
||||
v: number[], r: string[], s: string[], txOpts?: TxOpts) => Promise<number>;
|
||||
};
|
||||
filled: {
|
||||
callAsync: (orderHash: string, defaultBlock?: Web3.BlockParam) => Promise<BigNumber>;
|
||||
};
|
||||
cancelled: {
|
||||
callAsync: (orderHash: string, defaultBlock?: Web3.BlockParam) => Promise<BigNumber>;
|
||||
};
|
||||
getOrderHash: {
|
||||
callAsync: (orderAddresses: OrderAddresses, orderValues: OrderValues) => Promise<string>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TokenContract extends Web3.ContractInstance {
|
||||
balanceOf: {
|
||||
callAsync: (address: string, defaultBlock?: Web3.BlockParam) => Promise<BigNumber>;
|
||||
};
|
||||
allowance: {
|
||||
callAsync: (ownerAddress: string, allowedAddress: string,
|
||||
defaultBlock?: Web3.BlockParam) => Promise<BigNumber>;
|
||||
};
|
||||
transfer: {
|
||||
sendTransactionAsync: (toAddress: string, amountInBaseUnits: BigNumber,
|
||||
txOpts?: TxOpts) => Promise<string>;
|
||||
};
|
||||
transferFrom: {
|
||||
sendTransactionAsync: (fromAddress: string, toAddress: string, amountInBaseUnits: BigNumber,
|
||||
txOpts?: TxOpts) => Promise<string>;
|
||||
};
|
||||
approve: {
|
||||
sendTransactionAsync: (proxyAddress: string, amountInBaseUnits: BigNumber,
|
||||
txOpts?: TxOpts) => Promise<string>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TokenRegistryContract extends Web3.ContractInstance {
|
||||
getTokenMetaData: {
|
||||
callAsync: (address: string) => Promise<TokenMetadata>;
|
||||
};
|
||||
getTokenAddresses: {
|
||||
callAsync: () => Promise<string[]>;
|
||||
};
|
||||
getTokenAddressBySymbol: {
|
||||
callAsync: (symbol: string) => Promise<string>;
|
||||
};
|
||||
getTokenAddressByName: {
|
||||
callAsync: (name: string) => Promise<string>;
|
||||
};
|
||||
getTokenBySymbol: {
|
||||
callAsync: (symbol: string) => Promise<TokenMetadata>;
|
||||
};
|
||||
getTokenByName: {
|
||||
callAsync: (name: string) => Promise<TokenMetadata>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EtherTokenContract extends Web3.ContractInstance {
|
||||
deposit: {
|
||||
sendTransactionAsync: (txOpts: TxOpts) => Promise<string>;
|
||||
};
|
||||
withdraw: {
|
||||
sendTransactionAsync: (amount: BigNumber, txOpts: TxOpts) => Promise<string>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TokenTransferProxyContract extends Web3.ContractInstance {
|
||||
getAuthorizedAddresses: {
|
||||
callAsync: () => Promise<string[]>;
|
||||
};
|
||||
authorized: {
|
||||
callAsync: (address: string) => Promise<boolean>;
|
||||
};
|
||||
}
|
||||
|
||||
export enum SolidityTypes {
|
||||
Address = 'address',
|
||||
Uint256 = 'uint256',
|
||||
Uint8 = 'uint8',
|
||||
Uint = 'uint',
|
||||
}
|
||||
|
||||
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 enum ExchangeContractErrs {
|
||||
OrderFillExpired = 'ORDER_FILL_EXPIRED',
|
||||
OrderCancelExpired = 'ORDER_CANCEL_EXPIRED',
|
||||
OrderCancelAmountZero = 'ORDER_CANCEL_AMOUNT_ZERO',
|
||||
OrderAlreadyCancelledOrFilled = 'ORDER_ALREADY_CANCELLED_OR_FILLED',
|
||||
OrderFillAmountZero = 'ORDER_FILL_AMOUNT_ZERO',
|
||||
OrderRemainingFillAmountZero = 'ORDER_REMAINING_FILL_AMOUNT_ZERO',
|
||||
OrderFillRoundingError = 'ORDER_FILL_ROUNDING_ERROR',
|
||||
FillBalanceAllowanceError = 'FILL_BALANCE_ALLOWANCE_ERROR',
|
||||
InsufficientTakerBalance = 'INSUFFICIENT_TAKER_BALANCE',
|
||||
InsufficientTakerAllowance = 'INSUFFICIENT_TAKER_ALLOWANCE',
|
||||
InsufficientMakerBalance = 'INSUFFICIENT_MAKER_BALANCE',
|
||||
InsufficientMakerAllowance = 'INSUFFICIENT_MAKER_ALLOWANCE',
|
||||
InsufficientTakerFeeBalance = 'INSUFFICIENT_TAKER_FEE_BALANCE',
|
||||
InsufficientTakerFeeAllowance = 'INSUFFICIENT_TAKER_FEE_ALLOWANCE',
|
||||
InsufficientMakerFeeBalance = 'INSUFFICIENT_MAKER_FEE_BALANCE',
|
||||
InsufficientMakerFeeAllowance = 'INSUFFICIENT_MAKER_FEE_ALLOWANCE',
|
||||
TransactionSenderIsNotFillOrderTaker = 'TRANSACTION_SENDER_IS_NOT_FILL_ORDER_TAKER',
|
||||
MultipleMakersInSingleCancelBatchDisallowed = 'MULTIPLE_MAKERS_IN_SINGLE_CANCEL_BATCH_DISALLOWED',
|
||||
InsufficientRemainingFillAmount = 'INSUFFICIENT_REMAINING_FILL_AMOUNT',
|
||||
MultipleTakerTokensInFillUpToDisallowed = 'MULTIPLE_TAKER_TOKENS_IN_FILL_UP_TO_DISALLOWED',
|
||||
BatchOrdersMustHaveSameExchangeAddress = 'BATCH_ORDERS_MUST_HAVE_SAME_EXCHANGE_ADDRESS',
|
||||
BatchOrdersMustHaveAtLeastOneItem = 'BATCH_ORDERS_MUST_HAVE_AT_LEAST_ONE_ITEM',
|
||||
}
|
||||
|
||||
export type RawLog = Web3.LogEntry;
|
||||
|
||||
export interface ContractEvent {
|
||||
logIndex: number;
|
||||
transactionIndex: number;
|
||||
transactionHash: string;
|
||||
blockHash: string;
|
||||
blockNumber: number;
|
||||
address: string;
|
||||
type: string;
|
||||
event: string;
|
||||
args: ContractEventArgs;
|
||||
}
|
||||
|
||||
export interface LogFillContractEventArgs {
|
||||
maker: string;
|
||||
taker: string;
|
||||
feeRecipient: string;
|
||||
makerToken: string;
|
||||
takerToken: string;
|
||||
filledMakerTokenAmount: BigNumber;
|
||||
filledTakerTokenAmount: BigNumber;
|
||||
paidMakerFee: BigNumber;
|
||||
paidTakerFee: BigNumber;
|
||||
tokens: string;
|
||||
orderHash: string;
|
||||
}
|
||||
export interface LogCancelContractEventArgs {
|
||||
maker: string;
|
||||
feeRecipient: string;
|
||||
makerToken: string;
|
||||
takerToken: string;
|
||||
cancelledMakerTokenAmount: BigNumber;
|
||||
cancelledTakerTokenAmount: BigNumber;
|
||||
tokens: string;
|
||||
orderHash: string;
|
||||
}
|
||||
export interface LogErrorContractEventArgs {
|
||||
errorId: BigNumber;
|
||||
orderHash: string;
|
||||
}
|
||||
export type ExchangeContractEventArgs = LogFillContractEventArgs|LogCancelContractEventArgs|LogErrorContractEventArgs;
|
||||
export interface TransferContractEventArgs {
|
||||
_from: string;
|
||||
_to: string;
|
||||
_value: BigNumber;
|
||||
}
|
||||
export interface ApprovalContractEventArgs {
|
||||
_owner: string;
|
||||
_spender: string;
|
||||
_value: BigNumber;
|
||||
}
|
||||
export type TokenContractEventArgs = TransferContractEventArgs|ApprovalContractEventArgs;
|
||||
export type ContractEventArgs = ExchangeContractEventArgs|TokenContractEventArgs;
|
||||
export type ContractEventArg = string|BigNumber;
|
||||
|
||||
export interface Order {
|
||||
maker: string;
|
||||
taker: string;
|
||||
makerFee: BigNumber;
|
||||
takerFee: BigNumber;
|
||||
makerTokenAmount: BigNumber;
|
||||
takerTokenAmount: BigNumber;
|
||||
makerTokenAddress: string;
|
||||
takerTokenAddress: string;
|
||||
salt: BigNumber;
|
||||
exchangeContractAddress: string;
|
||||
feeRecipient: string;
|
||||
expirationUnixTimestampSec: BigNumber;
|
||||
}
|
||||
|
||||
export interface SignedOrder extends Order {
|
||||
ecSignature: ECSignature;
|
||||
}
|
||||
|
||||
// [address, name, symbol, decimals, ipfsHash, swarmHash]
|
||||
export type TokenMetadata = [string, string, string, BigNumber, string, string];
|
||||
|
||||
export interface Token {
|
||||
name: string;
|
||||
address: string;
|
||||
symbol: string;
|
||||
decimals: number;
|
||||
}
|
||||
|
||||
export interface TxOpts {
|
||||
from: string;
|
||||
gas?: number;
|
||||
value?: BigNumber;
|
||||
}
|
||||
|
||||
export interface TokenAddressBySymbol {
|
||||
[symbol: string]: string;
|
||||
}
|
||||
|
||||
export enum ExchangeEvents {
|
||||
LogFill = 'LogFill',
|
||||
LogCancel = 'LogCancel',
|
||||
LogError = 'LogError',
|
||||
}
|
||||
|
||||
export enum TokenEvents {
|
||||
Transfer = 'Transfer',
|
||||
Approval = 'Approval',
|
||||
}
|
||||
|
||||
export type ContractEvents = TokenEvents|ExchangeEvents;
|
||||
|
||||
export interface IndexedFilterValues {
|
||||
[index: string]: ContractEventArg;
|
||||
}
|
||||
|
||||
export enum BlockParamLiteral {
|
||||
Latest = 'latest',
|
||||
Earliest = 'earliest',
|
||||
Pending = 'pending',
|
||||
}
|
||||
|
||||
export type BlockParam = BlockParamLiteral|number;
|
||||
|
||||
export interface SubscriptionOpts {
|
||||
fromBlock: BlockParam;
|
||||
toBlock: BlockParam;
|
||||
}
|
||||
|
||||
export type DoneCallback = (err?: Error) => void;
|
||||
|
||||
export interface OrderCancellationRequest {
|
||||
order: Order|SignedOrder;
|
||||
takerTokenCancelAmount: BigNumber;
|
||||
}
|
||||
|
||||
export interface OrderFillRequest {
|
||||
signedOrder: SignedOrder;
|
||||
takerTokenFillAmount: BigNumber;
|
||||
}
|
||||
|
||||
export type AsyncMethod = (...args: any[]) => Promise<any>;
|
||||
|
||||
/**
|
||||
* We re-export the `Web3.Provider` type specified in the Web3 Typescript typings
|
||||
* since it is the type of the `provider` argument to the `ZeroEx` constructor.
|
||||
* It is however a `Web3` library type, not a native `0x.js` type.
|
||||
*/
|
||||
export type Web3Provider = Web3.Provider;
|
||||
|
||||
export interface ExchangeContractByAddress {
|
||||
[address: string]: ExchangeContract;
|
||||
}
|
||||
|
||||
export interface JSONRPCPayload {
|
||||
params: any[];
|
||||
method: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* eventPollingIntervalMs: How often to poll the Ethereum node for new events
|
||||
*/
|
||||
export interface OrderStateWatcherConfig {
|
||||
eventPollingIntervalMs?: number;
|
||||
}
|
||||
|
||||
/*
|
||||
* gasPrice: Gas price to use with every transaction
|
||||
* exchangeContractAddress: The address of an exchange contract to use
|
||||
* tokenRegistryContractAddress: The address of a token registry contract to use
|
||||
* etherTokenContractAddress: The address of an ether token contract to use
|
||||
* orderWatcherConfig: All the configs related to the orderWatcher
|
||||
*/
|
||||
export interface ZeroExConfig {
|
||||
gasPrice?: BigNumber; // Gas price to use with every transaction
|
||||
exchangeContractAddress?: string;
|
||||
tokenRegistryContractAddress?: string;
|
||||
etherTokenContractAddress?: string;
|
||||
orderWatcherConfig?: OrderStateWatcherConfig;
|
||||
}
|
||||
|
||||
export enum AbiType {
|
||||
Function = 'function',
|
||||
Constructor = 'constructor',
|
||||
Event = 'event',
|
||||
Fallback = 'fallback',
|
||||
}
|
||||
|
||||
export interface DecodedLogArgs {
|
||||
[argName: string]: ContractEventArg;
|
||||
}
|
||||
|
||||
export interface LogWithDecodedArgs<ArgsType> extends Web3.DecodedLogEntry<ArgsType> {}
|
||||
|
||||
export interface TransactionReceiptWithDecodedLogs extends TransactionReceipt {
|
||||
logs: Array<LogWithDecodedArgs<DecodedLogArgs>|Web3.LogEntry>;
|
||||
}
|
||||
|
||||
export interface Artifact {
|
||||
abi: any;
|
||||
networks: {[networkId: number]: {
|
||||
address: string;
|
||||
}};
|
||||
}
|
||||
|
||||
/*
|
||||
* expectedFillTakerTokenAmount: If specified, the validation method will ensure that the
|
||||
* supplied order maker has a sufficient allowance/balance to fill this amount of the order's
|
||||
* takerTokenAmount. If not specified, the validation method ensures that the maker has a sufficient
|
||||
* allowance/balance to fill the entire remaining order amount.
|
||||
*/
|
||||
export interface ValidateOrderFillableOpts {
|
||||
expectedFillTakerTokenAmount?: BigNumber;
|
||||
}
|
||||
|
||||
/*
|
||||
* defaultBlock: The block up to which to query the blockchain state. Setting this to a historical block number
|
||||
* let's the user query the blockchain's state at an arbitrary point in time. In order for this to work, the
|
||||
* backing Ethereum node must keep the entire historical state of the chain (e.g setting `--pruning=archive`
|
||||
* flag when running Parity).
|
||||
*/
|
||||
export interface MethodOpts {
|
||||
defaultBlock?: Web3.BlockParam;
|
||||
}
|
||||
|
||||
/*
|
||||
* shouldValidate: Flag indicating whether the library should make attempts to validate a transaction before
|
||||
* broadcasting it. For example, order has a valid signature, maker has sufficient funds, etc.
|
||||
*/
|
||||
export interface OrderTransactionOpts {
|
||||
shouldValidate: boolean;
|
||||
}
|
||||
|
||||
export type FilterObject = Web3.FilterObject;
|
||||
|
||||
export enum TradeSide {
|
||||
Maker = 'maker',
|
||||
Taker = 'taker',
|
||||
}
|
||||
|
||||
export enum TransferType {
|
||||
Trade = 'trade',
|
||||
Fee = 'fee',
|
||||
}
|
||||
|
||||
export interface OrderRelevantState {
|
||||
makerBalance: BigNumber;
|
||||
makerProxyAllowance: BigNumber;
|
||||
makerFeeBalance: BigNumber;
|
||||
makerFeeProxyAllowance: BigNumber;
|
||||
filledTakerTokenAmount: BigNumber;
|
||||
canceledTakerTokenAmount: BigNumber;
|
||||
remainingFillableMakerTokenAmount: BigNumber;
|
||||
}
|
||||
|
||||
export interface OrderStateValid {
|
||||
isValid: true;
|
||||
orderHash: string;
|
||||
orderRelevantState: OrderRelevantState;
|
||||
}
|
||||
|
||||
export interface OrderStateInvalid {
|
||||
isValid: false;
|
||||
orderHash: string;
|
||||
error: ExchangeContractErrs;
|
||||
}
|
||||
|
||||
export type OrderState = OrderStateValid|OrderStateInvalid;
|
||||
|
||||
export type OnOrderStateChangeCallbackSync = (orderState: OrderState) => void;
|
||||
export type OnOrderStateChangeCallbackAsync = (orderState: OrderState) => Promise<void>;
|
||||
export type OnOrderStateChangeCallback = OnOrderStateChangeCallbackAsync|OnOrderStateChangeCallbackSync;
|
||||
|
||||
export interface TransactionReceipt {
|
||||
blockHash: string;
|
||||
blockNumber: number;
|
||||
transactionHash: string;
|
||||
transactionIndex: number;
|
||||
from: string;
|
||||
to: string;
|
||||
status: null|0|1;
|
||||
cumulativeGasUsed: number;
|
||||
gasUsed: number;
|
||||
contractAddress: string|null;
|
||||
logs: Web3.LogEntry[];
|
||||
}
|
68
packages/0x.js/src/utils/abi_decoder.ts
Normal file
68
packages/0x.js/src/utils/abi_decoder.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import * as Web3 from 'web3';
|
||||
import * as _ from 'lodash';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {AbiType, DecodedLogArgs, LogWithDecodedArgs, RawLog, SolidityTypes, ContractEventArgs} from '../types';
|
||||
import * as SolidityCoder from 'web3/lib/solidity/coder';
|
||||
|
||||
export class AbiDecoder {
|
||||
private savedABIs: Web3.AbiDefinition[] = [];
|
||||
private methodIds: {[signatureHash: string]: Web3.EventAbi} = {};
|
||||
constructor(abiArrays: Web3.AbiDefinition[][]) {
|
||||
_.map(abiArrays, this.addABI.bind(this));
|
||||
}
|
||||
// This method can only decode logs from the 0x & ERC20 smart contracts
|
||||
public tryToDecodeLogOrNoop<ArgsType extends ContractEventArgs>(
|
||||
log: Web3.LogEntry): LogWithDecodedArgs<ArgsType>|RawLog {
|
||||
const methodId = log.topics[0];
|
||||
const event = this.methodIds[methodId];
|
||||
if (_.isUndefined(event)) {
|
||||
return log;
|
||||
}
|
||||
const logData = log.data;
|
||||
const decodedParams: DecodedLogArgs = {};
|
||||
let dataIndex = 0;
|
||||
let topicsIndex = 1;
|
||||
|
||||
const nonIndexedInputs = _.filter(event.inputs, input => !input.indexed);
|
||||
const dataTypes = _.map(nonIndexedInputs, input => input.type);
|
||||
const decodedData = SolidityCoder.decodeParams(dataTypes, logData.slice('0x'.length));
|
||||
|
||||
_.map(event.inputs, (param: Web3.EventParameter) => {
|
||||
// Indexed parameters are stored in topics. Non-indexed ones in decodedData
|
||||
let value = param.indexed ? log.topics[topicsIndex++] : decodedData[dataIndex++];
|
||||
if (param.type === SolidityTypes.Address) {
|
||||
value = this.padZeros(new BigNumber(value).toString(16));
|
||||
} else if (param.type === SolidityTypes.Uint256 ||
|
||||
param.type === SolidityTypes.Uint8 ||
|
||||
param.type === SolidityTypes.Uint ) {
|
||||
value = new BigNumber(value);
|
||||
}
|
||||
decodedParams[param.name] = value;
|
||||
});
|
||||
|
||||
return {
|
||||
...log,
|
||||
event: event.name,
|
||||
args: decodedParams,
|
||||
};
|
||||
}
|
||||
private addABI(abiArray: Web3.AbiDefinition[]): void {
|
||||
_.map(abiArray, (abi: Web3.AbiDefinition) => {
|
||||
if (abi.type === AbiType.Event) {
|
||||
const signature = `${abi.name}(${_.map(abi.inputs, input => input.type).join(',')})`;
|
||||
const signatureHash = new Web3().sha3(signature);
|
||||
this.methodIds[signatureHash] = abi;
|
||||
}
|
||||
});
|
||||
this.savedABIs = this.savedABIs.concat(abiArray);
|
||||
}
|
||||
private padZeros(address: string) {
|
||||
let formatted = address;
|
||||
if (_.startsWith(formatted, '0x')) {
|
||||
formatted = formatted.slice(2);
|
||||
}
|
||||
|
||||
formatted = _.padStart(formatted, 40, '0');
|
||||
return `0x${formatted}`;
|
||||
}
|
||||
}
|
101
packages/0x.js/src/utils/assert.ts
Normal file
101
packages/0x.js/src/utils/assert.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as Web3 from 'web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {SchemaValidator, Schema} from '0x-json-schemas';
|
||||
import {Web3Wrapper} from '../web3_wrapper';
|
||||
import {signatureUtils} from '../utils/signature_utils';
|
||||
import {ECSignature} from '../types';
|
||||
|
||||
const HEX_REGEX = /^0x[0-9A-F]*$/i;
|
||||
|
||||
export const assert = {
|
||||
isBigNumber(variableName: string, value: BigNumber): void {
|
||||
const isBigNumber = _.isObject(value) && (value as any).isBigNumber;
|
||||
this.assert(isBigNumber, this.typeAssertionMessage(variableName, 'BigNumber', value));
|
||||
},
|
||||
isValidBaseUnitAmount(variableName: string, value: BigNumber) {
|
||||
assert.isBigNumber(variableName, value);
|
||||
const hasDecimals = value.decimalPlaces() !== 0;
|
||||
this.assert(
|
||||
!hasDecimals, `${variableName} should be in baseUnits (no decimals), found value: ${value.toNumber()}`,
|
||||
);
|
||||
},
|
||||
isValidSignature(orderHash: string, ecSignature: ECSignature, signerAddress: string) {
|
||||
const isValidSignature = signatureUtils.isValidSignature(orderHash, ecSignature, signerAddress);
|
||||
this.assert(isValidSignature, `Expected order with hash '${orderHash}' to have a valid signature`);
|
||||
},
|
||||
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));
|
||||
},
|
||||
isFunction(variableName: string, value: any): void {
|
||||
this.assert(_.isFunction(value), this.typeAssertionMessage(variableName, 'function', value));
|
||||
},
|
||||
isHexString(variableName: string, value: string): void {
|
||||
this.assert(_.isString(value) && HEX_REGEX.test(value),
|
||||
this.typeAssertionMessage(variableName, 'HexString', value));
|
||||
},
|
||||
isETHAddressHex(variableName: string, value: string): void {
|
||||
const web3 = new Web3();
|
||||
this.assert(web3.isAddress(value), this.typeAssertionMessage(variableName, 'ETHAddressHex', value));
|
||||
this.assert(
|
||||
web3.isAddress(value) && value.toLowerCase() === value,
|
||||
`Checksummed addresses are not supported. Convert ${variableName} to lower case before passing`,
|
||||
);
|
||||
},
|
||||
doesBelongToStringEnum(variableName: string, value: string,
|
||||
stringEnum: any /* There is no base type for every string enum */): void {
|
||||
const doesBelongToStringEnum = !_.isUndefined(stringEnum[value]);
|
||||
const enumValues = _.keys(stringEnum);
|
||||
const enumValuesAsStrings = _.map(enumValues, enumValue => `'${enumValue}'`);
|
||||
const enumValuesAsString = enumValuesAsStrings.join(', ');
|
||||
assert.assert(
|
||||
doesBelongToStringEnum,
|
||||
`Expected ${variableName} to be one of: ${enumValuesAsString}, encountered: ${value}`,
|
||||
);
|
||||
},
|
||||
async isSenderAddressAsync(variableName: string, senderAddressHex: string,
|
||||
web3Wrapper: Web3Wrapper): Promise<void> {
|
||||
assert.isETHAddressHex(variableName, senderAddressHex);
|
||||
const isSenderAddressAvailable = await web3Wrapper.isSenderAddressAvailableAsync(senderAddressHex);
|
||||
assert.assert(isSenderAddressAvailable,
|
||||
`Specified ${variableName} ${senderAddressHex} isn't available through the supplied web3 provider`,
|
||||
);
|
||||
},
|
||||
async isUserAddressAvailableAsync(web3Wrapper: Web3Wrapper): Promise<void> {
|
||||
const availableAddresses = await web3Wrapper.getAvailableAddressesAsync();
|
||||
this.assert(!_.isEmpty(availableAddresses), 'No addresses were available on the provided web3 provider');
|
||||
},
|
||||
hasAtMostOneUniqueValue(value: any[], errMsg: string): void {
|
||||
this.assert(_.uniq(value).length <= 1, errMsg);
|
||||
},
|
||||
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));
|
||||
},
|
||||
isWeb3Provider(variableName: string, value: Web3.Provider): void {
|
||||
const isWeb3Provider = _.isFunction((value as any).send) || _.isFunction((value as any).sendAsync);
|
||||
this.assert(isWeb3Provider, this.typeAssertionMessage(variableName, 'Web3.Provider', value));
|
||||
},
|
||||
doesConformToSchema(variableName: string, value: any, schema: Schema): void {
|
||||
const schemaValidator = new SchemaValidator();
|
||||
const validationResult = schemaValidator.validate(value, schema);
|
||||
const hasValidationErrors = validationResult.errors.length > 0;
|
||||
const msg = `Expected ${variableName} to conform to schema ${schema.id}
|
||||
Encountered: ${JSON.stringify(value, null, '\t')}
|
||||
Validation errors: ${validationResult.errors.join(', ')}`;
|
||||
this.assert(!hasValidationErrors, msg);
|
||||
},
|
||||
assert(condition: boolean, message: string): void {
|
||||
if (!condition) {
|
||||
throw new Error(message);
|
||||
}
|
||||
},
|
||||
typeAssertionMessage(variableName: string, type: string, value: any): string {
|
||||
return `Expected ${variableName} to be of type ${type}, encountered: ${value}`;
|
||||
},
|
||||
};
|
11
packages/0x.js/src/utils/constants.ts
Normal file
11
packages/0x.js/src/utils/constants.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
export const constants = {
|
||||
NULL_ADDRESS: '0x0000000000000000000000000000000000000000',
|
||||
TESTRPC_NETWORK_ID: 50,
|
||||
MAX_DIGITS_IN_UNSIGNED_256_INT: 78,
|
||||
INVALID_JUMP_PATTERN: 'invalid JUMP at',
|
||||
OUT_OF_GAS_PATTERN: 'out of gas',
|
||||
UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1),
|
||||
DEFAULT_BLOCK_POLLING_INTERVAL: 1000,
|
||||
};
|
35
packages/0x.js/src/utils/decorators.ts
Normal file
35
packages/0x.js/src/utils/decorators.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import * as _ from 'lodash';
|
||||
import {constants} from './constants';
|
||||
import {AsyncMethod, ZeroExError} from '../types';
|
||||
|
||||
export const decorators = {
|
||||
/**
|
||||
* Source: https://stackoverflow.com/a/29837695/3546986
|
||||
*/
|
||||
contractCallErrorHandler(target: object,
|
||||
key: string|symbol,
|
||||
descriptor: TypedPropertyDescriptor<AsyncMethod>,
|
||||
): TypedPropertyDescriptor<AsyncMethod> {
|
||||
const originalMethod = (descriptor.value as AsyncMethod);
|
||||
|
||||
// Do not use arrow syntax here. Use a function expression in
|
||||
// order to use the correct value of `this` in this method
|
||||
// tslint:disable-next-line:only-arrow-functions
|
||||
descriptor.value = async function(...args: any[]) {
|
||||
try {
|
||||
const result = await originalMethod.apply(this, args);
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (_.includes(error.message, constants.INVALID_JUMP_PATTERN)) {
|
||||
throw new Error(ZeroExError.InvalidJump);
|
||||
}
|
||||
if (_.includes(error.message, constants.OUT_OF_GAS_PATTERN)) {
|
||||
throw new Error(ZeroExError.OutOfGas);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
},
|
||||
};
|
88
packages/0x.js/src/utils/exchange_transfer_simulator.ts
Normal file
88
packages/0x.js/src/utils/exchange_transfer_simulator.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import * as _ from 'lodash';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {ExchangeContractErrs, TradeSide, TransferType, BlockParamLiteral} from '../types';
|
||||
import {TokenWrapper} from '../contract_wrappers/token_wrapper';
|
||||
import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store';
|
||||
|
||||
enum FailureReason {
|
||||
Balance = 'balance',
|
||||
ProxyAllowance = 'proxyAllowance',
|
||||
}
|
||||
|
||||
const ERR_MSG_MAPPING = {
|
||||
[FailureReason.Balance]: {
|
||||
[TradeSide.Maker]: {
|
||||
[TransferType.Trade]: ExchangeContractErrs.InsufficientMakerBalance,
|
||||
[TransferType.Fee]: ExchangeContractErrs.InsufficientMakerFeeBalance,
|
||||
},
|
||||
[TradeSide.Taker]: {
|
||||
[TransferType.Trade]: ExchangeContractErrs.InsufficientTakerBalance,
|
||||
[TransferType.Fee]: ExchangeContractErrs.InsufficientTakerFeeBalance,
|
||||
},
|
||||
},
|
||||
[FailureReason.ProxyAllowance]: {
|
||||
[TradeSide.Maker]: {
|
||||
[TransferType.Trade]: ExchangeContractErrs.InsufficientMakerAllowance,
|
||||
[TransferType.Fee]: ExchangeContractErrs.InsufficientMakerFeeAllowance,
|
||||
},
|
||||
[TradeSide.Taker]: {
|
||||
[TransferType.Trade]: ExchangeContractErrs.InsufficientTakerAllowance,
|
||||
[TransferType.Fee]: ExchangeContractErrs.InsufficientTakerFeeAllowance,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export class ExchangeTransferSimulator {
|
||||
private store: BalanceAndProxyAllowanceLazyStore;
|
||||
private UNLIMITED_ALLOWANCE_IN_BASE_UNITS: BigNumber;
|
||||
constructor(token: TokenWrapper) {
|
||||
this.store = new BalanceAndProxyAllowanceLazyStore(token);
|
||||
this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS = token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS;
|
||||
}
|
||||
/**
|
||||
* Simulates transferFrom call performed by a proxy
|
||||
* @param tokenAddress Address of the token to be transferred
|
||||
* @param from Owner of the transferred tokens
|
||||
* @param to Recipient of the transferred tokens
|
||||
* @param amountInBaseUnits The amount of tokens being transferred
|
||||
* @param tradeSide Is Maker/Taker transferring
|
||||
* @param transferType Is it a fee payment or a value transfer
|
||||
*/
|
||||
public async transferFromAsync(tokenAddress: string, from: string, to: string,
|
||||
amountInBaseUnits: BigNumber, tradeSide: TradeSide,
|
||||
transferType: TransferType): Promise<void> {
|
||||
const balance = await this.store.getBalanceAsync(tokenAddress, from);
|
||||
const proxyAllowance = await this.store.getProxyAllowanceAsync(tokenAddress, from);
|
||||
if (proxyAllowance.lessThan(amountInBaseUnits)) {
|
||||
this.throwValidationError(FailureReason.ProxyAllowance, tradeSide, transferType);
|
||||
}
|
||||
if (balance.lessThan(amountInBaseUnits)) {
|
||||
this.throwValidationError(FailureReason.Balance, tradeSide, transferType);
|
||||
}
|
||||
await this.decreaseProxyAllowanceAsync(tokenAddress, from, amountInBaseUnits);
|
||||
await this.decreaseBalanceAsync(tokenAddress, from, amountInBaseUnits);
|
||||
await this.increaseBalanceAsync(tokenAddress, to, amountInBaseUnits);
|
||||
}
|
||||
private async decreaseProxyAllowanceAsync(tokenAddress: string, userAddress: string,
|
||||
amountInBaseUnits: BigNumber): Promise<void> {
|
||||
const proxyAllowance = await this.store.getProxyAllowanceAsync(tokenAddress, userAddress);
|
||||
if (!proxyAllowance.eq(this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) {
|
||||
this.store.setProxyAllowance(tokenAddress, userAddress, proxyAllowance.minus(amountInBaseUnits));
|
||||
}
|
||||
}
|
||||
private async increaseBalanceAsync(tokenAddress: string, userAddress: string,
|
||||
amountInBaseUnits: BigNumber): Promise<void> {
|
||||
const balance = await this.store.getBalanceAsync(tokenAddress, userAddress);
|
||||
this.store.setBalance(tokenAddress, userAddress, balance.plus(amountInBaseUnits));
|
||||
}
|
||||
private async decreaseBalanceAsync(tokenAddress: string, userAddress: string,
|
||||
amountInBaseUnits: BigNumber): Promise<void> {
|
||||
const balance = await this.store.getBalanceAsync(tokenAddress, userAddress);
|
||||
this.store.setBalance(tokenAddress, userAddress, balance.minus(amountInBaseUnits));
|
||||
}
|
||||
private throwValidationError(failureReason: FailureReason, tradeSide: TradeSide,
|
||||
transferType: TransferType): Promise<never> {
|
||||
const errMsg = ERR_MSG_MAPPING[failureReason][tradeSide][transferType];
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
}
|
82
packages/0x.js/src/utils/filter_utils.ts
Normal file
82
packages/0x.js/src/utils/filter_utils.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as Web3 from 'web3';
|
||||
import * as uuid from 'uuid/v4';
|
||||
import * as ethUtil from 'ethereumjs-util';
|
||||
import * as jsSHA3 from 'js-sha3';
|
||||
import {ContractEvents, IndexedFilterValues, SubscriptionOpts} from '../types';
|
||||
|
||||
const TOPIC_LENGTH = 32;
|
||||
|
||||
export const filterUtils = {
|
||||
generateUUID(): string {
|
||||
return uuid();
|
||||
},
|
||||
getFilter(address: string, eventName: ContractEvents,
|
||||
indexFilterValues: IndexedFilterValues, abi: Web3.ContractAbi,
|
||||
subscriptionOpts?: SubscriptionOpts): Web3.FilterObject {
|
||||
const eventAbi = _.find(abi, {name: eventName}) as Web3.EventAbi;
|
||||
const eventSignature = filterUtils.getEventSignatureFromAbiByName(eventAbi, eventName);
|
||||
const topicForEventSignature = ethUtil.addHexPrefix(jsSHA3.keccak256(eventSignature));
|
||||
const topicsForIndexedArgs = filterUtils.getTopicsForIndexedArgs(eventAbi, indexFilterValues);
|
||||
const topics = [topicForEventSignature, ...topicsForIndexedArgs];
|
||||
let filter: Web3.FilterObject = {
|
||||
address,
|
||||
topics,
|
||||
};
|
||||
if (!_.isUndefined(subscriptionOpts)) {
|
||||
filter = {
|
||||
...subscriptionOpts,
|
||||
...filter,
|
||||
};
|
||||
}
|
||||
return filter;
|
||||
},
|
||||
getEventSignatureFromAbiByName(eventAbi: Web3.EventAbi, eventName: ContractEvents): string {
|
||||
const types = _.map(eventAbi.inputs, 'type');
|
||||
const signature = `${eventAbi.name}(${types.join(',')})`;
|
||||
return signature;
|
||||
},
|
||||
getTopicsForIndexedArgs(abi: Web3.EventAbi, indexFilterValues: IndexedFilterValues): Array<string|null> {
|
||||
const topics: Array<string|null> = [];
|
||||
for (const eventInput of abi.inputs) {
|
||||
if (!eventInput.indexed) {
|
||||
continue;
|
||||
}
|
||||
if (_.isUndefined(indexFilterValues[eventInput.name])) {
|
||||
// Null is a wildcard topic in a JSON-RPC call
|
||||
topics.push(null);
|
||||
} else {
|
||||
const value = indexFilterValues[eventInput.name] as string;
|
||||
const buffer = ethUtil.toBuffer(value);
|
||||
const paddedBuffer = ethUtil.setLengthLeft(buffer, TOPIC_LENGTH);
|
||||
const topic = ethUtil.bufferToHex(paddedBuffer);
|
||||
topics.push(topic);
|
||||
}
|
||||
}
|
||||
return topics;
|
||||
},
|
||||
matchesFilter(log: Web3.LogEntry, filter: Web3.FilterObject): boolean {
|
||||
if (!_.isUndefined(filter.address) && log.address !== filter.address) {
|
||||
return false;
|
||||
}
|
||||
if (!_.isUndefined(filter.topics)) {
|
||||
return filterUtils.matchesTopics(log.topics, filter.topics);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
matchesTopics(logTopics: string[], filterTopics: Array<string[]|string|null>): boolean {
|
||||
const matchesTopic = _.zipWith(logTopics, filterTopics, filterUtils.matchesTopic.bind(filterUtils));
|
||||
const matchesTopics = _.every(matchesTopic);
|
||||
return matchesTopics;
|
||||
},
|
||||
matchesTopic(logTopic: string, filterTopic: string[]|string|null): boolean {
|
||||
if (_.isArray(filterTopic)) {
|
||||
return _.includes(filterTopic, logTopic);
|
||||
}
|
||||
if (_.isString(filterTopic)) {
|
||||
return filterTopic === logTopic;
|
||||
}
|
||||
// null topic is a wildcard
|
||||
return true;
|
||||
},
|
||||
};
|
20
packages/0x.js/src/utils/interval_utils.ts
Normal file
20
packages/0x.js/src/utils/interval_utils.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as _ from 'lodash';
|
||||
|
||||
export const intervalUtils = {
|
||||
setAsyncExcludingInterval(fn: () => Promise<void>, intervalMs: number) {
|
||||
let locked = false;
|
||||
const intervalId = setInterval(async () => {
|
||||
if (locked) {
|
||||
return;
|
||||
} else {
|
||||
locked = true;
|
||||
await fn();
|
||||
locked = false;
|
||||
}
|
||||
}, intervalMs);
|
||||
return intervalId;
|
||||
},
|
||||
clearAsyncExcludingInterval(intervalId: NodeJS.Timer): void {
|
||||
clearInterval(intervalId);
|
||||
},
|
||||
};
|
119
packages/0x.js/src/utils/order_state_utils.ts
Normal file
119
packages/0x.js/src/utils/order_state_utils.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as Web3 from 'web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {
|
||||
ExchangeContractErrs,
|
||||
SignedOrder,
|
||||
OrderRelevantState,
|
||||
MethodOpts,
|
||||
OrderState,
|
||||
OrderStateValid,
|
||||
OrderStateInvalid,
|
||||
} from '../types';
|
||||
import {ZeroEx} from '../0x';
|
||||
import {TokenWrapper} from '../contract_wrappers/token_wrapper';
|
||||
import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper';
|
||||
import {utils} from '../utils/utils';
|
||||
import {constants} from '../utils/constants';
|
||||
import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store';
|
||||
import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store';
|
||||
|
||||
export class OrderStateUtils {
|
||||
private balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore;
|
||||
private orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore;
|
||||
constructor(balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore,
|
||||
orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore) {
|
||||
this.balanceAndProxyAllowanceLazyStore = balanceAndProxyAllowanceLazyStore;
|
||||
this.orderFilledCancelledLazyStore = orderFilledCancelledLazyStore;
|
||||
}
|
||||
public async getOrderStateAsync(signedOrder: SignedOrder): Promise<OrderState> {
|
||||
const orderRelevantState = await this.getOrderRelevantStateAsync(signedOrder);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
try {
|
||||
this.validateIfOrderIsValid(signedOrder, orderRelevantState);
|
||||
const orderState: OrderStateValid = {
|
||||
isValid: true,
|
||||
orderHash,
|
||||
orderRelevantState,
|
||||
};
|
||||
return orderState;
|
||||
} catch (err) {
|
||||
const orderState: OrderStateInvalid = {
|
||||
isValid: false,
|
||||
orderHash,
|
||||
error: err.message,
|
||||
};
|
||||
return orderState;
|
||||
}
|
||||
}
|
||||
public async getOrderRelevantStateAsync(signedOrder: SignedOrder): Promise<OrderRelevantState> {
|
||||
// HACK: We access the private property here but otherwise the interface will be less nice.
|
||||
// If we pass it from the instantiator - there is no opportunity to get it there
|
||||
// because JS doesn't support async constructors.
|
||||
// Moreover - it's cached under the hood so it's equivalent to an async constructor.
|
||||
const exchange = (this.orderFilledCancelledLazyStore as any).exchange as ExchangeWrapper;
|
||||
const zrxTokenAddress = await exchange.getZRXTokenAddressAsync();
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
const makerBalance = await this.balanceAndProxyAllowanceLazyStore.getBalanceAsync(
|
||||
signedOrder.makerTokenAddress, signedOrder.maker,
|
||||
);
|
||||
const makerProxyAllowance = await this.balanceAndProxyAllowanceLazyStore.getProxyAllowanceAsync(
|
||||
signedOrder.makerTokenAddress, signedOrder.maker,
|
||||
);
|
||||
const makerFeeBalance = await this.balanceAndProxyAllowanceLazyStore.getBalanceAsync(
|
||||
zrxTokenAddress, signedOrder.maker,
|
||||
);
|
||||
const makerFeeProxyAllowance = await this.balanceAndProxyAllowanceLazyStore.getProxyAllowanceAsync(
|
||||
zrxTokenAddress, signedOrder.maker,
|
||||
);
|
||||
const filledTakerTokenAmount = await this.orderFilledCancelledLazyStore.getFilledTakerAmountAsync(orderHash);
|
||||
const canceledTakerTokenAmount = await this.orderFilledCancelledLazyStore.getCancelledTakerAmountAsync(
|
||||
orderHash,
|
||||
);
|
||||
const unavailableTakerTokenAmount = await exchange.getUnavailableTakerAmountAsync(orderHash);
|
||||
const totalMakerTokenAmount = signedOrder.makerTokenAmount;
|
||||
const totalTakerTokenAmount = signedOrder.takerTokenAmount;
|
||||
const remainingTakerTokenAmount = totalTakerTokenAmount.minus(unavailableTakerTokenAmount);
|
||||
const remainingMakerTokenAmount = remainingTakerTokenAmount.times(totalMakerTokenAmount)
|
||||
.dividedToIntegerBy(totalTakerTokenAmount);
|
||||
const fillableMakerTokenAmount = BigNumber.min([makerProxyAllowance, makerBalance]);
|
||||
const remainingFillableMakerTokenAmount = BigNumber.min(fillableMakerTokenAmount, remainingMakerTokenAmount);
|
||||
// TODO: Handle edge case where maker token is ZRX with fee
|
||||
const orderRelevantState = {
|
||||
makerBalance,
|
||||
makerProxyAllowance,
|
||||
makerFeeBalance,
|
||||
makerFeeProxyAllowance,
|
||||
filledTakerTokenAmount,
|
||||
canceledTakerTokenAmount,
|
||||
remainingFillableMakerTokenAmount,
|
||||
};
|
||||
return orderRelevantState;
|
||||
}
|
||||
private validateIfOrderIsValid(signedOrder: SignedOrder, orderRelevantState: OrderRelevantState): void {
|
||||
const unavailableTakerTokenAmount = orderRelevantState.canceledTakerTokenAmount.add(
|
||||
orderRelevantState.filledTakerTokenAmount,
|
||||
);
|
||||
const availableTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount);
|
||||
if (availableTakerTokenAmount.eq(0)) {
|
||||
throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero);
|
||||
}
|
||||
|
||||
if (orderRelevantState.makerBalance.eq(0)) {
|
||||
throw new Error(ExchangeContractErrs.InsufficientMakerBalance);
|
||||
}
|
||||
if (orderRelevantState.makerProxyAllowance.eq(0)) {
|
||||
throw new Error(ExchangeContractErrs.InsufficientMakerAllowance);
|
||||
}
|
||||
if (!signedOrder.makerFee.eq(0)) {
|
||||
if (orderRelevantState.makerFeeBalance.eq(0)) {
|
||||
throw new Error(ExchangeContractErrs.InsufficientMakerFeeBalance);
|
||||
}
|
||||
if (orderRelevantState.makerFeeProxyAllowance.eq(0)) {
|
||||
throw new Error(ExchangeContractErrs.InsufficientMakerFeeAllowance);
|
||||
}
|
||||
}
|
||||
// TODO Add linear function solver when maker token is ZRX #badass
|
||||
// Return the max amount that's fillable
|
||||
}
|
||||
}
|
166
packages/0x.js/src/utils/order_validation_utils.ts
Normal file
166
packages/0x.js/src/utils/order_validation_utils.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import * as _ from 'lodash';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {ExchangeContractErrs, SignedOrder, Order, ZeroExError, TradeSide, TransferType} from '../types';
|
||||
import {ZeroEx} from '../0x';
|
||||
import {TokenWrapper} from '../contract_wrappers/token_wrapper';
|
||||
import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper';
|
||||
import {utils} from '../utils/utils';
|
||||
import {constants} from '../utils/constants';
|
||||
import {ExchangeTransferSimulator} from './exchange_transfer_simulator';
|
||||
|
||||
export class OrderValidationUtils {
|
||||
private tokenWrapper: TokenWrapper;
|
||||
private exchangeWrapper: ExchangeWrapper;
|
||||
constructor(tokenWrapper: TokenWrapper, exchangeWrapper: ExchangeWrapper) {
|
||||
this.tokenWrapper = tokenWrapper;
|
||||
this.exchangeWrapper = exchangeWrapper;
|
||||
}
|
||||
public async validateOrderFillableOrThrowAsync(
|
||||
exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder, zrxTokenAddress: string,
|
||||
expectedFillTakerTokenAmount?: BigNumber): Promise<void> {
|
||||
const orderHash = utils.getOrderHashHex(signedOrder);
|
||||
const unavailableTakerTokenAmount = await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHash);
|
||||
this.validateRemainingFillAmountNotZeroOrThrow(
|
||||
signedOrder.takerTokenAmount, unavailableTakerTokenAmount,
|
||||
);
|
||||
this.validateOrderNotExpiredOrThrow(signedOrder.expirationUnixTimestampSec);
|
||||
let fillTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount);
|
||||
if (!_.isUndefined(expectedFillTakerTokenAmount)) {
|
||||
fillTakerTokenAmount = expectedFillTakerTokenAmount;
|
||||
}
|
||||
const fillMakerTokenAmount = this.getPartialAmount(
|
||||
fillTakerTokenAmount,
|
||||
signedOrder.takerTokenAmount,
|
||||
signedOrder.makerTokenAmount,
|
||||
);
|
||||
await exchangeTradeEmulator.transferFromAsync(
|
||||
signedOrder.makerTokenAddress, signedOrder.maker, signedOrder.taker, fillMakerTokenAmount,
|
||||
TradeSide.Maker, TransferType.Trade,
|
||||
);
|
||||
const makerFeeAmount = this.getPartialAmount(
|
||||
fillTakerTokenAmount,
|
||||
signedOrder.takerTokenAmount,
|
||||
signedOrder.makerFee,
|
||||
);
|
||||
await exchangeTradeEmulator.transferFromAsync(
|
||||
zrxTokenAddress, signedOrder.maker, signedOrder.feeRecipient, makerFeeAmount,
|
||||
TradeSide.Maker, TransferType.Fee,
|
||||
);
|
||||
}
|
||||
public async validateFillOrderThrowIfInvalidAsync(
|
||||
exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder,
|
||||
fillTakerTokenAmount: BigNumber, takerAddress: string,
|
||||
zrxTokenAddress: string): Promise<BigNumber> {
|
||||
if (fillTakerTokenAmount.eq(0)) {
|
||||
throw new Error(ExchangeContractErrs.OrderFillAmountZero);
|
||||
}
|
||||
const orderHash = utils.getOrderHashHex(signedOrder);
|
||||
if (!ZeroEx.isValidSignature(orderHash, signedOrder.ecSignature, signedOrder.maker)) {
|
||||
throw new Error(ZeroExError.InvalidSignature);
|
||||
}
|
||||
const unavailableTakerTokenAmount = await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHash);
|
||||
this.validateRemainingFillAmountNotZeroOrThrow(
|
||||
signedOrder.takerTokenAmount, unavailableTakerTokenAmount,
|
||||
);
|
||||
if (signedOrder.taker !== constants.NULL_ADDRESS && signedOrder.taker !== takerAddress) {
|
||||
throw new Error(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker);
|
||||
}
|
||||
this.validateOrderNotExpiredOrThrow(signedOrder.expirationUnixTimestampSec);
|
||||
const remainingTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount);
|
||||
const filledTakerTokenAmount = remainingTakerTokenAmount.lessThan(fillTakerTokenAmount) ?
|
||||
remainingTakerTokenAmount :
|
||||
fillTakerTokenAmount;
|
||||
await this.validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
|
||||
exchangeTradeEmulator, signedOrder, filledTakerTokenAmount, takerAddress, zrxTokenAddress,
|
||||
);
|
||||
|
||||
const wouldRoundingErrorOccur = await this.exchangeWrapper.isRoundingErrorAsync(
|
||||
filledTakerTokenAmount, signedOrder.takerTokenAmount, signedOrder.makerTokenAmount,
|
||||
);
|
||||
if (wouldRoundingErrorOccur) {
|
||||
throw new Error(ExchangeContractErrs.OrderFillRoundingError);
|
||||
}
|
||||
return filledTakerTokenAmount;
|
||||
}
|
||||
public async validateFillOrKillOrderThrowIfInvalidAsync(
|
||||
exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder,
|
||||
fillTakerTokenAmount: BigNumber, takerAddress: string, zrxTokenAddress: string): Promise<void> {
|
||||
const filledTakerTokenAmount = await this.validateFillOrderThrowIfInvalidAsync(
|
||||
exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress,
|
||||
);
|
||||
if (filledTakerTokenAmount !== fillTakerTokenAmount) {
|
||||
throw new Error(ExchangeContractErrs.InsufficientRemainingFillAmount);
|
||||
}
|
||||
}
|
||||
public async validateCancelOrderThrowIfInvalidAsync(order: Order,
|
||||
cancelTakerTokenAmount: BigNumber,
|
||||
unavailableTakerTokenAmount: BigNumber,
|
||||
): Promise<void> {
|
||||
if (cancelTakerTokenAmount.eq(0)) {
|
||||
throw new Error(ExchangeContractErrs.OrderCancelAmountZero);
|
||||
}
|
||||
if (order.takerTokenAmount.eq(unavailableTakerTokenAmount)) {
|
||||
throw new Error(ExchangeContractErrs.OrderAlreadyCancelledOrFilled);
|
||||
}
|
||||
const currentUnixTimestampSec = utils.getCurrentUnixTimestamp();
|
||||
if (order.expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) {
|
||||
throw new Error(ExchangeContractErrs.OrderCancelExpired);
|
||||
}
|
||||
}
|
||||
public async validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
|
||||
exchangeTradeEmulator: ExchangeTransferSimulator, signedOrder: SignedOrder,
|
||||
fillTakerTokenAmount: BigNumber, senderAddress: string, zrxTokenAddress: string): Promise<void> {
|
||||
const fillMakerTokenAmount = this.getPartialAmount(
|
||||
fillTakerTokenAmount,
|
||||
signedOrder.takerTokenAmount,
|
||||
signedOrder.makerTokenAmount,
|
||||
);
|
||||
await exchangeTradeEmulator.transferFromAsync(
|
||||
signedOrder.makerTokenAddress, signedOrder.maker, senderAddress, fillMakerTokenAmount,
|
||||
TradeSide.Maker, TransferType.Trade,
|
||||
);
|
||||
await exchangeTradeEmulator.transferFromAsync(
|
||||
signedOrder.takerTokenAddress, senderAddress, signedOrder.maker, fillTakerTokenAmount,
|
||||
TradeSide.Taker, TransferType.Trade,
|
||||
);
|
||||
const makerFeeAmount = this.getPartialAmount(
|
||||
fillTakerTokenAmount,
|
||||
signedOrder.takerTokenAmount,
|
||||
signedOrder.makerFee,
|
||||
);
|
||||
await exchangeTradeEmulator.transferFromAsync(
|
||||
zrxTokenAddress, signedOrder.maker, signedOrder.feeRecipient, makerFeeAmount, TradeSide.Maker,
|
||||
TransferType.Fee,
|
||||
);
|
||||
const takerFeeAmount = this.getPartialAmount(
|
||||
fillTakerTokenAmount,
|
||||
signedOrder.takerTokenAmount,
|
||||
signedOrder.takerFee,
|
||||
);
|
||||
await exchangeTradeEmulator.transferFromAsync(
|
||||
zrxTokenAddress, senderAddress, signedOrder.feeRecipient, takerFeeAmount, TradeSide.Taker,
|
||||
TransferType.Fee,
|
||||
);
|
||||
}
|
||||
private validateRemainingFillAmountNotZeroOrThrow(
|
||||
takerTokenAmount: BigNumber, unavailableTakerTokenAmount: BigNumber,
|
||||
) {
|
||||
if (takerTokenAmount.eq(unavailableTakerTokenAmount)) {
|
||||
throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero);
|
||||
}
|
||||
}
|
||||
private validateOrderNotExpiredOrThrow(expirationUnixTimestampSec: BigNumber) {
|
||||
const currentUnixTimestampSec = utils.getCurrentUnixTimestamp();
|
||||
if (expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) {
|
||||
throw new Error(ExchangeContractErrs.OrderFillExpired);
|
||||
}
|
||||
}
|
||||
private getPartialAmount(numerator: BigNumber, denominator: BigNumber,
|
||||
target: BigNumber): BigNumber {
|
||||
const fillMakerTokenAmount = numerator
|
||||
.mul(target)
|
||||
.div(denominator)
|
||||
.round(0);
|
||||
return fillMakerTokenAmount;
|
||||
}
|
||||
}
|
44
packages/0x.js/src/utils/signature_utils.ts
Normal file
44
packages/0x.js/src/utils/signature_utils.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as ethUtil from 'ethereumjs-util';
|
||||
import {ECSignature} from '../types';
|
||||
|
||||
export const signatureUtils = {
|
||||
isValidSignature(data: string, signature: ECSignature, signerAddress: string): boolean {
|
||||
const dataBuff = ethUtil.toBuffer(data);
|
||||
const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff);
|
||||
try {
|
||||
const pubKey = ethUtil.ecrecover(
|
||||
msgHashBuff,
|
||||
signature.v,
|
||||
ethUtil.toBuffer(signature.r),
|
||||
ethUtil.toBuffer(signature.s));
|
||||
const retrievedAddress = ethUtil.bufferToHex(ethUtil.pubToAddress(pubKey));
|
||||
return retrievedAddress === signerAddress;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
parseSignatureHexAsVRS(signatureHex: string): ECSignature {
|
||||
const signatureBuffer = ethUtil.toBuffer(signatureHex);
|
||||
let v = signatureBuffer[0];
|
||||
if (v < 27) {
|
||||
v += 27;
|
||||
}
|
||||
const r = signatureBuffer.slice(1, 33);
|
||||
const s = signatureBuffer.slice(33, 65);
|
||||
const ecSignature: ECSignature = {
|
||||
v,
|
||||
r: ethUtil.bufferToHex(r),
|
||||
s: ethUtil.bufferToHex(s),
|
||||
};
|
||||
return ecSignature;
|
||||
},
|
||||
parseSignatureHexAsRSV(signatureHex: string): ECSignature {
|
||||
const {v, r, s} = ethUtil.fromRpcSig(signatureHex);
|
||||
const ecSignature: ECSignature = {
|
||||
v,
|
||||
r: ethUtil.bufferToHex(r),
|
||||
s: ethUtil.bufferToHex(s),
|
||||
};
|
||||
return ecSignature;
|
||||
},
|
||||
};
|
55
packages/0x.js/src/utils/utils.ts
Normal file
55
packages/0x.js/src/utils/utils.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as ethABI from 'ethereumjs-abi';
|
||||
import * as ethUtil from 'ethereumjs-util';
|
||||
import {Order, SignedOrder, SolidityTypes} from '../types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import BN = require('bn.js');
|
||||
|
||||
export const utils = {
|
||||
/**
|
||||
* Converts BigNumber instance to BN
|
||||
* The only reason we convert to BN is to remain compatible with `ethABI. soliditySHA3` that
|
||||
* expects values of Solidity type `uint` to be passed as type `BN`.
|
||||
* We do not use BN anywhere else in the codebase.
|
||||
*/
|
||||
bigNumberToBN(value: BigNumber) {
|
||||
return new BN(value.toString(), 10);
|
||||
},
|
||||
consoleLog(message: string): void {
|
||||
// tslint:disable-next-line: no-console
|
||||
console.log(message);
|
||||
},
|
||||
isParityNode(nodeVersion: string): boolean {
|
||||
return _.includes(nodeVersion, 'Parity');
|
||||
},
|
||||
isTestRpc(nodeVersion: string): boolean {
|
||||
return _.includes(nodeVersion, 'TestRPC');
|
||||
},
|
||||
spawnSwitchErr(name: string, value: any): Error {
|
||||
return new Error(`Unexpected switch value: ${value} encountered for ${name}`);
|
||||
},
|
||||
getOrderHashHex(order: Order|SignedOrder): string {
|
||||
const orderParts = [
|
||||
{value: order.exchangeContractAddress, 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;
|
||||
},
|
||||
getCurrentUnixTimestamp(): BigNumber {
|
||||
return new BigNumber(Date.now() / 1000);
|
||||
},
|
||||
};
|
172
packages/0x.js/src/web3_wrapper.ts
Normal file
172
packages/0x.js/src/web3_wrapper.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as Web3 from 'web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import promisify = require('es6-promisify');
|
||||
import {ZeroExError, Artifact, TransactionReceipt} from './types';
|
||||
import {Contract} from './contract';
|
||||
|
||||
export class Web3Wrapper {
|
||||
private web3: Web3;
|
||||
private defaults: Partial<Web3.TxData>;
|
||||
private networkIdIfExists?: number;
|
||||
private jsonRpcRequestId: number;
|
||||
constructor(provider: Web3.Provider, defaults?: Partial<Web3.TxData>) {
|
||||
if (_.isUndefined((provider as any).sendAsync)) {
|
||||
// Web3@1.0 provider doesn't support synchronous http requests,
|
||||
// so it only has an async `send` method, instead of a `send` and `sendAsync` in web3@0.x.x`
|
||||
// We re-assign the send method so that Web3@1.0 providers work with 0x.js
|
||||
(provider as any).sendAsync = (provider as any).send;
|
||||
}
|
||||
this.web3 = new Web3();
|
||||
this.web3.setProvider(provider);
|
||||
this.defaults = defaults || {};
|
||||
this.jsonRpcRequestId = 0;
|
||||
}
|
||||
public setProvider(provider: Web3.Provider) {
|
||||
delete this.networkIdIfExists;
|
||||
this.web3.setProvider(provider);
|
||||
}
|
||||
public isAddress(address: string): boolean {
|
||||
return this.web3.isAddress(address);
|
||||
}
|
||||
public async isSenderAddressAvailableAsync(senderAddress: string): Promise<boolean> {
|
||||
const addresses = await this.getAvailableAddressesAsync();
|
||||
return _.includes(addresses, senderAddress);
|
||||
}
|
||||
public async getNodeVersionAsync(): Promise<string> {
|
||||
const nodeVersion = await promisify(this.web3.version.getNode)();
|
||||
return nodeVersion;
|
||||
}
|
||||
public async getTransactionReceiptAsync(txHash: string): Promise<TransactionReceipt> {
|
||||
const transactionReceipt = await promisify(this.web3.eth.getTransactionReceipt)(txHash);
|
||||
transactionReceipt.status = this.normalizeTxReceiptStatus(transactionReceipt.status);
|
||||
return transactionReceipt;
|
||||
}
|
||||
public getCurrentProvider(): Web3.Provider {
|
||||
return this.web3.currentProvider;
|
||||
}
|
||||
public async getNetworkIdIfExistsAsync(): Promise<number|undefined> {
|
||||
if (!_.isUndefined(this.networkIdIfExists)) {
|
||||
return this.networkIdIfExists;
|
||||
}
|
||||
|
||||
try {
|
||||
const networkId = await this.getNetworkAsync();
|
||||
this.networkIdIfExists = Number(networkId);
|
||||
return this.networkIdIfExists;
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
public async getContractInstanceFromArtifactAsync<A extends Web3.ContractInstance>(artifact: Artifact,
|
||||
address?: string): Promise<A> {
|
||||
let contractAddress: string;
|
||||
if (_.isUndefined(address)) {
|
||||
const networkIdIfExists = await this.getNetworkIdIfExistsAsync();
|
||||
if (_.isUndefined(networkIdIfExists)) {
|
||||
throw new Error(ZeroExError.NoNetworkId);
|
||||
}
|
||||
if (_.isUndefined(artifact.networks[networkIdIfExists])) {
|
||||
throw new Error(ZeroExError.ContractNotDeployedOnNetwork);
|
||||
}
|
||||
contractAddress = artifact.networks[networkIdIfExists].address.toLowerCase();
|
||||
} else {
|
||||
contractAddress = address;
|
||||
}
|
||||
const doesContractExist = await this.doesContractExistAtAddressAsync(contractAddress);
|
||||
if (!doesContractExist) {
|
||||
throw new Error(ZeroExError.ContractDoesNotExist);
|
||||
}
|
||||
const contractInstance = this.getContractInstance<A>(
|
||||
artifact.abi, contractAddress,
|
||||
);
|
||||
return contractInstance;
|
||||
}
|
||||
public toWei(ethAmount: BigNumber): BigNumber {
|
||||
const balanceWei = this.web3.toWei(ethAmount, 'ether');
|
||||
return balanceWei;
|
||||
}
|
||||
public async getBalanceInWeiAsync(owner: string): Promise<BigNumber> {
|
||||
let balanceInWei = await promisify(this.web3.eth.getBalance)(owner);
|
||||
balanceInWei = new BigNumber(balanceInWei);
|
||||
return balanceInWei;
|
||||
}
|
||||
public async doesContractExistAtAddressAsync(address: string): Promise<boolean> {
|
||||
const code = await promisify(this.web3.eth.getCode)(address);
|
||||
// Regex matches 0x0, 0x00, 0x in order to accommodate poorly implemented clients
|
||||
const codeIsEmpty = /^0x0{0,40}$/i.test(code);
|
||||
return !codeIsEmpty;
|
||||
}
|
||||
public async signTransactionAsync(address: string, message: string): Promise<string> {
|
||||
const signData = await promisify(this.web3.eth.sign)(address, message);
|
||||
return signData;
|
||||
}
|
||||
public async getBlockNumberAsync(): Promise<number> {
|
||||
const blockNumber = await promisify(this.web3.eth.getBlockNumber)();
|
||||
return blockNumber;
|
||||
}
|
||||
public async getBlockAsync(blockParam: string|Web3.BlockParam): Promise<Web3.BlockWithoutTransactionData> {
|
||||
const block = await promisify(this.web3.eth.getBlock)(blockParam);
|
||||
return block;
|
||||
}
|
||||
public async getBlockTimestampAsync(blockParam: string|Web3.BlockParam): Promise<number> {
|
||||
const {timestamp} = await this.getBlockAsync(blockParam);
|
||||
return timestamp;
|
||||
}
|
||||
public async getAvailableAddressesAsync(): Promise<string[]> {
|
||||
const addresses: string[] = await promisify(this.web3.eth.getAccounts)();
|
||||
return addresses;
|
||||
}
|
||||
public async getLogsAsync(filter: Web3.FilterObject): Promise<Web3.LogEntry[]> {
|
||||
let fromBlock = filter.fromBlock;
|
||||
if (_.isNumber(fromBlock)) {
|
||||
fromBlock = this.web3.toHex(fromBlock);
|
||||
}
|
||||
let toBlock = filter.toBlock;
|
||||
if (_.isNumber(toBlock)) {
|
||||
toBlock = this.web3.toHex(toBlock);
|
||||
}
|
||||
const serializedFilter = {
|
||||
...filter,
|
||||
fromBlock,
|
||||
toBlock,
|
||||
};
|
||||
const payload = {
|
||||
jsonrpc: '2.0',
|
||||
id: this.jsonRpcRequestId++,
|
||||
method: 'eth_getLogs',
|
||||
params: [serializedFilter],
|
||||
};
|
||||
const logs = await this.sendRawPayloadAsync(payload);
|
||||
return logs;
|
||||
}
|
||||
private getContractInstance<A extends Web3.ContractInstance>(abi: Web3.ContractAbi, address: string): A {
|
||||
const web3ContractInstance = this.web3.eth.contract(abi).at(address);
|
||||
const contractInstance = new Contract(web3ContractInstance, this.defaults) as any as A;
|
||||
return contractInstance;
|
||||
}
|
||||
private async getNetworkAsync(): Promise<number> {
|
||||
const networkId = await promisify(this.web3.version.getNetwork)();
|
||||
return networkId;
|
||||
}
|
||||
private async sendRawPayloadAsync(payload: Web3.JSONRPCRequestPayload): Promise<any> {
|
||||
const sendAsync = this.web3.currentProvider.sendAsync.bind(this.web3.currentProvider);
|
||||
const response = await promisify(sendAsync)(payload);
|
||||
const result = response.result;
|
||||
return result;
|
||||
}
|
||||
private normalizeTxReceiptStatus(status: undefined|null|string|0|1): null|0|1 {
|
||||
// Transaction status might have four values
|
||||
// undefined - Testrpc and other old clients
|
||||
// null - New clients on old transactions
|
||||
// number - Parity
|
||||
// hex - Geth
|
||||
if (_.isString(status)) {
|
||||
return this.web3.toDecimal(status) as 0|1;
|
||||
} else if (_.isUndefined(status)) {
|
||||
return null;
|
||||
} else {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
259
packages/0x.js/test/0x.js_test.ts
Normal file
259
packages/0x.js/test/0x.js_test.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as chai from 'chai';
|
||||
import {chaiSetup} from './utils/chai_setup';
|
||||
import 'mocha';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import * as Sinon from 'sinon';
|
||||
import {ZeroEx, Order, ZeroExError, LogWithDecodedArgs, ApprovalContractEventArgs, TokenEvents} from '../src';
|
||||
import {constants} from './utils/constants';
|
||||
import {TokenUtils} from './utils/token_utils';
|
||||
import {web3Factory} from './utils/web3_factory';
|
||||
import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
|
||||
|
||||
const blockchainLifecycle = new BlockchainLifecycle();
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('ZeroEx library', () => {
|
||||
const web3 = web3Factory.create();
|
||||
const zeroEx = new ZeroEx(web3.currentProvider);
|
||||
describe('#setProvider', () => {
|
||||
it('overrides provider in nested web3s and invalidates contractInstances', async () => {
|
||||
// Instantiate the contract instances with the current provider
|
||||
await (zeroEx.exchange as any)._getExchangeContractAsync();
|
||||
await (zeroEx.tokenRegistry as any)._getTokenRegistryContractAsync();
|
||||
expect((zeroEx.exchange as any)._exchangeContractIfExists).to.not.be.undefined();
|
||||
expect((zeroEx.tokenRegistry as any)._tokenRegistryContractIfExists).to.not.be.undefined();
|
||||
|
||||
const newProvider = web3Factory.getRpcProvider();
|
||||
// Add property to newProvider so that we can differentiate it from old provider
|
||||
(newProvider as any).zeroExTestId = 1;
|
||||
await zeroEx.setProviderAsync(newProvider);
|
||||
|
||||
// Check that contractInstances with old provider are removed after provider update
|
||||
expect((zeroEx.exchange as any)._exchangeContractIfExists).to.be.undefined();
|
||||
expect((zeroEx.tokenRegistry as any)._tokenRegistryContractIfExists).to.be.undefined();
|
||||
|
||||
// Check that all nested web3 wrapper instances return the updated provider
|
||||
const nestedWeb3WrapperProvider = (zeroEx as any)._web3Wrapper.getCurrentProvider();
|
||||
expect((nestedWeb3WrapperProvider as any).zeroExTestId).to.be.a('number');
|
||||
const exchangeWeb3WrapperProvider = (zeroEx.exchange as any)._web3Wrapper.getCurrentProvider();
|
||||
expect((exchangeWeb3WrapperProvider as any).zeroExTestId).to.be.a('number');
|
||||
const tokenRegistryWeb3WrapperProvider = (zeroEx.tokenRegistry as any)._web3Wrapper.getCurrentProvider();
|
||||
expect((tokenRegistryWeb3WrapperProvider as any).zeroExTestId).to.be.a('number');
|
||||
});
|
||||
});
|
||||
describe('#isValidSignature', () => {
|
||||
// The Exchange smart contract `isValidSignature` method only validates orderHashes and assumes
|
||||
// the length of the data is exactly 32 bytes. Thus for these tests, we use data of this size.
|
||||
const dataHex = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0';
|
||||
const signature = {
|
||||
v: 27,
|
||||
r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33',
|
||||
s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254',
|
||||
};
|
||||
const address = '0x5409ed021d9299bf6814279a6a1411a7e866a631';
|
||||
it('should return false if the data doesn\'t pertain to the signature & address', async () => {
|
||||
expect(ZeroEx.isValidSignature('0x0', signature, address)).to.be.false();
|
||||
return expect(
|
||||
(zeroEx.exchange as any)._isValidSignatureUsingContractCallAsync('0x0', signature, address),
|
||||
).to.become(false);
|
||||
});
|
||||
it('should return false if the address doesn\'t pertain to the signature & data', async () => {
|
||||
const validUnrelatedAddress = '0x8b0292b11a196601ed2ce54b665cafeca0347d42';
|
||||
expect(ZeroEx.isValidSignature(dataHex, signature, validUnrelatedAddress)).to.be.false();
|
||||
return expect(
|
||||
(zeroEx.exchange as any)._isValidSignatureUsingContractCallAsync(dataHex, signature,
|
||||
validUnrelatedAddress),
|
||||
).to.become(false);
|
||||
});
|
||||
it('should return false if the signature doesn\'t pertain to the dataHex & address', async () => {
|
||||
const wrongSignature = _.assign({}, signature, {v: 28});
|
||||
expect(ZeroEx.isValidSignature(dataHex, wrongSignature, address)).to.be.false();
|
||||
return expect(
|
||||
(zeroEx.exchange as any)._isValidSignatureUsingContractCallAsync(dataHex, wrongSignature, address),
|
||||
).to.become(false);
|
||||
});
|
||||
it('should return true if the signature does pertain to the dataHex & address', async () => {
|
||||
const isValidSignatureLocal = ZeroEx.isValidSignature(dataHex, signature, address);
|
||||
expect(isValidSignatureLocal).to.be.true();
|
||||
const isValidSignatureOnContract = await (zeroEx.exchange as any)
|
||||
._isValidSignatureUsingContractCallAsync(dataHex, signature, address);
|
||||
return expect(isValidSignatureOnContract).to.be.true();
|
||||
});
|
||||
});
|
||||
describe('#generateSalt', () => {
|
||||
it('generates different salts', () => {
|
||||
const equal = ZeroEx.generatePseudoRandomSalt().eq(ZeroEx.generatePseudoRandomSalt());
|
||||
expect(equal).to.be.false();
|
||||
});
|
||||
it('generates salt in range [0..2^256)', () => {
|
||||
const salt = ZeroEx.generatePseudoRandomSalt();
|
||||
expect(salt.greaterThanOrEqualTo(0)).to.be.true();
|
||||
const twoPow256 = new BigNumber(2).pow(256);
|
||||
expect(salt.lessThan(twoPow256)).to.be.true();
|
||||
});
|
||||
});
|
||||
describe('#isValidOrderHash', () => {
|
||||
it('returns false if the value is not a hex string', () => {
|
||||
const isValid = ZeroEx.isValidOrderHash('not a hex');
|
||||
expect(isValid).to.be.false();
|
||||
});
|
||||
it('returns false if the length is wrong', () => {
|
||||
const isValid = ZeroEx.isValidOrderHash('0xdeadbeef');
|
||||
expect(isValid).to.be.false();
|
||||
});
|
||||
it('returns true if order hash is correct', () => {
|
||||
const isValid = ZeroEx.isValidOrderHash('0x' + Array(65).join('0'));
|
||||
expect(isValid).to.be.true();
|
||||
});
|
||||
});
|
||||
describe('#toUnitAmount', () => {
|
||||
it('Should return the expected unit amount for the decimals passed in', () => {
|
||||
const baseUnitAmount = new BigNumber(1000000000);
|
||||
const decimals = 6;
|
||||
const unitAmount = ZeroEx.toUnitAmount(baseUnitAmount, decimals);
|
||||
const expectedUnitAmount = new BigNumber(1000);
|
||||
expect(unitAmount).to.be.bignumber.equal(expectedUnitAmount);
|
||||
});
|
||||
});
|
||||
describe('#toBaseUnitAmount', () => {
|
||||
it('Should return the expected base unit amount for the decimals passed in', () => {
|
||||
const unitAmount = new BigNumber(1000);
|
||||
const decimals = 6;
|
||||
const baseUnitAmount = ZeroEx.toBaseUnitAmount(unitAmount, decimals);
|
||||
const expectedUnitAmount = new BigNumber(1000000000);
|
||||
expect(baseUnitAmount).to.be.bignumber.equal(expectedUnitAmount);
|
||||
});
|
||||
});
|
||||
describe('#getOrderHashHex', () => {
|
||||
const expectedOrderHash = '0x39da987067a3c9e5f1617694f1301326ba8c8b0498ebef5df4863bed394e3c83';
|
||||
const fakeExchangeContractAddress = '0xb69e673309512a9d726f87304c6984054f87a93b';
|
||||
const order: Order = {
|
||||
maker: constants.NULL_ADDRESS,
|
||||
taker: constants.NULL_ADDRESS,
|
||||
feeRecipient: constants.NULL_ADDRESS,
|
||||
makerTokenAddress: constants.NULL_ADDRESS,
|
||||
takerTokenAddress: constants.NULL_ADDRESS,
|
||||
exchangeContractAddress: fakeExchangeContractAddress,
|
||||
salt: new BigNumber(0),
|
||||
makerFee: new BigNumber(0),
|
||||
takerFee: new BigNumber(0),
|
||||
makerTokenAmount: new BigNumber(0),
|
||||
takerTokenAmount: new BigNumber(0),
|
||||
expirationUnixTimestampSec: new BigNumber(0),
|
||||
};
|
||||
it('calculates the order hash', async () => {
|
||||
const orderHash = ZeroEx.getOrderHashHex(order);
|
||||
expect(orderHash).to.be.equal(expectedOrderHash);
|
||||
});
|
||||
});
|
||||
describe('#signOrderHashAsync', () => {
|
||||
let stubs: Sinon.SinonStub[] = [];
|
||||
let makerAddress: string;
|
||||
before(async () => {
|
||||
const availableAddreses = await zeroEx.getAvailableAddressesAsync();
|
||||
makerAddress = availableAddreses[0];
|
||||
});
|
||||
afterEach(() => {
|
||||
// clean up any stubs after the test has completed
|
||||
_.each(stubs, s => s.restore());
|
||||
stubs = [];
|
||||
});
|
||||
it('Should return the correct ECSignature', async () => {
|
||||
const orderHash = '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0';
|
||||
const expectedECSignature = {
|
||||
v: 27,
|
||||
r: '0x61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc33',
|
||||
s: '0x40349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254',
|
||||
};
|
||||
const ecSignature = await zeroEx.signOrderHashAsync(orderHash, makerAddress);
|
||||
expect(ecSignature).to.deep.equal(expectedECSignature);
|
||||
});
|
||||
it('should return the correct ECSignature for signatureHex concatenated as R + S + V', async () => {
|
||||
const orderHash = '0x34decbedc118904df65f379a175bb39ca18209d6ce41d5ed549d54e6e0a95004';
|
||||
// tslint:disable-next-line: max-line-length
|
||||
const signature = '0x22109d11d79cb8bf96ed88625e1cd9558800c4073332a9a02857499883ee5ce3050aa3cc1f2c435e67e114cdce54b9527b4f50548342401bc5d2b77adbdacb021b';
|
||||
const expectedECSignature = {
|
||||
v: 27,
|
||||
r: '0x22109d11d79cb8bf96ed88625e1cd9558800c4073332a9a02857499883ee5ce3',
|
||||
s: '0x050aa3cc1f2c435e67e114cdce54b9527b4f50548342401bc5d2b77adbdacb02',
|
||||
};
|
||||
stubs = [
|
||||
Sinon.stub((zeroEx as any)._web3Wrapper, 'signTransactionAsync')
|
||||
.returns(Promise.resolve(signature)),
|
||||
Sinon.stub(ZeroEx, 'isValidSignature').returns(true),
|
||||
];
|
||||
|
||||
const ecSignature = await zeroEx.signOrderHashAsync(orderHash, makerAddress);
|
||||
expect(ecSignature).to.deep.equal(expectedECSignature);
|
||||
});
|
||||
it('should return the correct ECSignature for signatureHex concatenated as V + R + S', async () => {
|
||||
const orderHash = '0xc793e33ffded933b76f2f48d9aa3339fc090399d5e7f5dec8d3660f5480793f7';
|
||||
// tslint:disable-next-line: max-line-length
|
||||
const signature = '0x1bc80bedc6756722672753413efdd749b5adbd4fd552595f59c13427407ee9aee02dea66f25a608bbae457e020fb6decb763deb8b7192abab624997242da248960';
|
||||
const expectedECSignature = {
|
||||
v: 27,
|
||||
r: '0xc80bedc6756722672753413efdd749b5adbd4fd552595f59c13427407ee9aee0',
|
||||
s: '0x2dea66f25a608bbae457e020fb6decb763deb8b7192abab624997242da248960',
|
||||
};
|
||||
stubs = [
|
||||
Sinon.stub((zeroEx as any)._web3Wrapper, 'signTransactionAsync')
|
||||
.returns(Promise.resolve(signature)),
|
||||
Sinon.stub(ZeroEx, 'isValidSignature').returns(true),
|
||||
];
|
||||
|
||||
const ecSignature = await zeroEx.signOrderHashAsync(orderHash, makerAddress);
|
||||
expect(ecSignature).to.deep.equal(expectedECSignature);
|
||||
});
|
||||
});
|
||||
describe('#awaitTransactionMinedAsync', () => {
|
||||
beforeEach(async () => {
|
||||
await blockchainLifecycle.startAsync();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await blockchainLifecycle.revertAsync();
|
||||
});
|
||||
it('returns transaction receipt with decoded logs', async () => {
|
||||
const availableAddresses = await zeroEx.getAvailableAddressesAsync();
|
||||
const coinbase = availableAddresses[0];
|
||||
const tokens = await zeroEx.tokenRegistry.getTokensAsync();
|
||||
const tokenUtils = new TokenUtils(tokens);
|
||||
const zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address;
|
||||
const proxyAddress = await zeroEx.proxy.getContractAddressAsync();
|
||||
const txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(zrxTokenAddress, coinbase);
|
||||
const txReceiptWithDecodedLogs = await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
const log = txReceiptWithDecodedLogs.logs[0] as LogWithDecodedArgs<ApprovalContractEventArgs>;
|
||||
expect(log.event).to.be.equal(TokenEvents.Approval);
|
||||
expect(log.args._owner).to.be.equal(coinbase);
|
||||
expect(log.args._spender).to.be.equal(proxyAddress);
|
||||
expect(log.args._value).to.be.bignumber.equal(zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
|
||||
});
|
||||
});
|
||||
describe('#config', () => {
|
||||
it('allows to specify exchange contract address', async () => {
|
||||
const config = {
|
||||
exchangeContractAddress: ZeroEx.NULL_ADDRESS,
|
||||
};
|
||||
const zeroExWithWrongExchangeAddress = new ZeroEx(web3.currentProvider, config);
|
||||
return expect(zeroExWithWrongExchangeAddress.exchange.getContractAddressAsync())
|
||||
.to.be.rejectedWith(ZeroExError.ContractDoesNotExist);
|
||||
});
|
||||
it('allows to specify ether token contract address', async () => {
|
||||
const config = {
|
||||
etherTokenContractAddress: ZeroEx.NULL_ADDRESS,
|
||||
};
|
||||
const zeroExWithWrongEtherTokenAddress = new ZeroEx(web3.currentProvider, config);
|
||||
return expect(zeroExWithWrongEtherTokenAddress.etherToken.getContractAddressAsync())
|
||||
.to.be.rejectedWith(ZeroExError.ContractDoesNotExist);
|
||||
});
|
||||
it('allows to specify token registry token contract address', async () => {
|
||||
const config = {
|
||||
tokenRegistryContractAddress: ZeroEx.NULL_ADDRESS,
|
||||
};
|
||||
const zeroExWithWrongTokenRegistryAddress = new ZeroEx(web3.currentProvider, config);
|
||||
return expect(zeroExWithWrongTokenRegistryAddress.tokenRegistry.getContractAddressAsync())
|
||||
.to.be.rejectedWith(ZeroExError.ContractDoesNotExist);
|
||||
});
|
||||
});
|
||||
});
|
49
packages/0x.js/test/artifacts_test.ts
Normal file
49
packages/0x.js/test/artifacts_test.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import * as fs from 'fs';
|
||||
import * as chai from 'chai';
|
||||
import {chaiSetup} from './utils/chai_setup';
|
||||
import HDWalletProvider = require('truffle-hdwallet-provider');
|
||||
import {ZeroEx} from '../src';
|
||||
import {constants} from './utils/constants';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
// Those tests are slower cause they're talking to a remote node
|
||||
const TIMEOUT = 10000;
|
||||
|
||||
describe('Artifacts', () => {
|
||||
describe('contracts are deployed on kovan', () => {
|
||||
const kovanRpcUrl = constants.KOVAN_RPC_URL;
|
||||
const packageJSONContent = fs.readFileSync('package.json', 'utf-8');
|
||||
const packageJSON = JSON.parse(packageJSONContent);
|
||||
const mnemonic = packageJSON.config.mnemonic;
|
||||
const web3Provider = new HDWalletProvider(mnemonic, kovanRpcUrl);
|
||||
const zeroEx = new ZeroEx(web3Provider);
|
||||
it('token registry contract is deployed', async () => {
|
||||
await (zeroEx.tokenRegistry as any)._getTokenRegistryContractAsync();
|
||||
}).timeout(TIMEOUT);
|
||||
it('proxy contract is deployed', async () => {
|
||||
await (zeroEx.token as any)._getTokenTransferProxyAddressAsync();
|
||||
}).timeout(TIMEOUT);
|
||||
it('exchange contract is deployed', async () => {
|
||||
await zeroEx.exchange.getContractAddressAsync();
|
||||
}).timeout(TIMEOUT);
|
||||
});
|
||||
describe('contracts are deployed on ropsten', () => {
|
||||
const ropstenRpcUrl = constants.ROPSTEN_RPC_URL;
|
||||
const packageJSONContent = fs.readFileSync('package.json', 'utf-8');
|
||||
const packageJSON = JSON.parse(packageJSONContent);
|
||||
const mnemonic = packageJSON.config.mnemonic;
|
||||
const web3Provider = new HDWalletProvider(mnemonic, ropstenRpcUrl);
|
||||
const zeroEx = new ZeroEx(web3Provider);
|
||||
it('token registry contract is deployed', async () => {
|
||||
await (zeroEx.tokenRegistry as any)._getTokenRegistryContractAsync();
|
||||
}).timeout(TIMEOUT);
|
||||
it('proxy contract is deployed', async () => {
|
||||
await (zeroEx.token as any)._getTokenTransferProxyAddressAsync();
|
||||
}).timeout(TIMEOUT);
|
||||
it('exchange contract is deployed', async () => {
|
||||
await zeroEx.exchange.getContractAddressAsync();
|
||||
}).timeout(TIMEOUT);
|
||||
});
|
||||
});
|
34
packages/0x.js/test/assert_test.ts
Normal file
34
packages/0x.js/test/assert_test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as chai from 'chai';
|
||||
import 'mocha';
|
||||
import {ZeroEx} from '../src';
|
||||
import {assert} from '../src/utils/assert';
|
||||
import {web3Factory} from './utils/web3_factory';
|
||||
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('Assertion library', () => {
|
||||
const web3 = web3Factory.create();
|
||||
const zeroEx = new ZeroEx(web3.currentProvider);
|
||||
describe('#isSenderAddressHexAsync', () => {
|
||||
it('throws when address is invalid', async () => {
|
||||
const address = '0xdeadbeef';
|
||||
const varName = 'address';
|
||||
return expect(assert.isSenderAddressAsync(varName, address, (zeroEx as any)._web3Wrapper))
|
||||
.to.be.rejectedWith(`Expected ${varName} to be of type ETHAddressHex, encountered: ${address}`);
|
||||
});
|
||||
it('throws when address is unavailable', async () => {
|
||||
const validUnrelatedAddress = '0x8b0292b11a196601eddce54b665cafeca0347d42';
|
||||
const varName = 'address';
|
||||
return expect(assert.isSenderAddressAsync(varName, validUnrelatedAddress, (zeroEx as any)._web3Wrapper))
|
||||
.to.be.rejectedWith(
|
||||
`Specified ${varName} ${validUnrelatedAddress} isn't available through the supplied web3 provider`,
|
||||
);
|
||||
});
|
||||
it('doesn\'t throw if address is available', async () => {
|
||||
const availableAddress = (await zeroEx.getAvailableAddressesAsync())[0];
|
||||
const varName = 'address';
|
||||
return expect(assert.isSenderAddressAsync(varName, availableAddress, (zeroEx as any)._web3Wrapper))
|
||||
.to.become(undefined);
|
||||
});
|
||||
});
|
||||
});
|
111
packages/0x.js/test/ether_token_wrapper_test.ts
Normal file
111
packages/0x.js/test/ether_token_wrapper_test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import 'mocha';
|
||||
import * as chai from 'chai';
|
||||
import {chaiSetup} from './utils/chai_setup';
|
||||
import * as Web3 from 'web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {web3Factory} from './utils/web3_factory';
|
||||
import {ZeroEx, ZeroExError} from '../src';
|
||||
import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
const blockchainLifecycle = new BlockchainLifecycle();
|
||||
|
||||
// Since the address depositing/withdrawing ETH/WETH also needs to pay gas costs for the transaction,
|
||||
// a small amount of ETH will be used to pay this gas cost. We therefore check that the difference between
|
||||
// the expected balance and actual balance (given the amount of ETH deposited), only deviates by the amount
|
||||
// required to pay gas costs.
|
||||
const MAX_REASONABLE_GAS_COST_IN_WEI = 62237;
|
||||
|
||||
describe('EtherTokenWrapper', () => {
|
||||
let web3: Web3;
|
||||
let zeroEx: ZeroEx;
|
||||
let userAddresses: string[];
|
||||
let addressWithETH: string;
|
||||
let wethContractAddress: string;
|
||||
let depositWeiAmount: BigNumber;
|
||||
let decimalPlaces: number;
|
||||
const gasPrice = new BigNumber(1);
|
||||
const zeroExConfig = {
|
||||
gasPrice,
|
||||
};
|
||||
before(async () => {
|
||||
web3 = web3Factory.create();
|
||||
zeroEx = new ZeroEx(web3.currentProvider, zeroExConfig);
|
||||
userAddresses = await zeroEx.getAvailableAddressesAsync();
|
||||
addressWithETH = userAddresses[0];
|
||||
wethContractAddress = await zeroEx.etherToken.getContractAddressAsync();
|
||||
depositWeiAmount = (zeroEx as any)._web3Wrapper.toWei(new BigNumber(5));
|
||||
decimalPlaces = 7;
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await blockchainLifecycle.startAsync();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await blockchainLifecycle.revertAsync();
|
||||
});
|
||||
describe('#depositAsync', () => {
|
||||
it('should successfully deposit ETH and issue Wrapped ETH tokens', async () => {
|
||||
const preETHBalance = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH);
|
||||
const preWETHBalance = await zeroEx.token.getBalanceAsync(wethContractAddress, addressWithETH);
|
||||
expect(preETHBalance).to.be.bignumber.gt(0);
|
||||
expect(preWETHBalance).to.be.bignumber.equal(0);
|
||||
|
||||
const txHash = await zeroEx.etherToken.depositAsync(depositWeiAmount, addressWithETH);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
|
||||
const postETHBalanceInWei = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH);
|
||||
const postWETHBalanceInBaseUnits = await zeroEx.token.getBalanceAsync(wethContractAddress, addressWithETH);
|
||||
|
||||
expect(postWETHBalanceInBaseUnits).to.be.bignumber.equal(depositWeiAmount);
|
||||
const remainingETHInWei = preETHBalance.minus(depositWeiAmount);
|
||||
const gasCost = remainingETHInWei.minus(postETHBalanceInWei);
|
||||
expect(gasCost).to.be.bignumber.lte(MAX_REASONABLE_GAS_COST_IN_WEI);
|
||||
});
|
||||
it('should throw if user has insufficient ETH balance for deposit', async () => {
|
||||
const preETHBalance = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH);
|
||||
|
||||
const extraETHBalance = (zeroEx as any)._web3Wrapper.toWei(5, 'ether');
|
||||
const overETHBalanceinWei = preETHBalance.add(extraETHBalance);
|
||||
|
||||
return expect(
|
||||
zeroEx.etherToken.depositAsync(overETHBalanceinWei, addressWithETH),
|
||||
).to.be.rejectedWith(ZeroExError.InsufficientEthBalanceForDeposit);
|
||||
});
|
||||
});
|
||||
describe('#withdrawAsync', () => {
|
||||
it('should successfully withdraw ETH in return for Wrapped ETH tokens', async () => {
|
||||
const ETHBalanceInWei = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH);
|
||||
|
||||
await zeroEx.etherToken.depositAsync(depositWeiAmount, addressWithETH);
|
||||
|
||||
const expectedPreETHBalance = ETHBalanceInWei.minus(depositWeiAmount);
|
||||
const preETHBalance = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH);
|
||||
const preWETHBalance = await zeroEx.token.getBalanceAsync(wethContractAddress, addressWithETH);
|
||||
let gasCost = expectedPreETHBalance.minus(preETHBalance);
|
||||
expect(gasCost).to.be.bignumber.lte(MAX_REASONABLE_GAS_COST_IN_WEI);
|
||||
expect(preWETHBalance).to.be.bignumber.equal(depositWeiAmount);
|
||||
|
||||
const txHash = await zeroEx.etherToken.withdrawAsync(depositWeiAmount, addressWithETH);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
|
||||
const postETHBalance = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH);
|
||||
const postWETHBalanceInBaseUnits = await zeroEx.token.getBalanceAsync(wethContractAddress, addressWithETH);
|
||||
|
||||
expect(postWETHBalanceInBaseUnits).to.be.bignumber.equal(0);
|
||||
const expectedETHBalance = preETHBalance.add(depositWeiAmount).round(decimalPlaces);
|
||||
gasCost = expectedETHBalance.minus(postETHBalance);
|
||||
expect(gasCost).to.be.bignumber.lte(MAX_REASONABLE_GAS_COST_IN_WEI);
|
||||
});
|
||||
it('should throw if user has insufficient WETH balance for withdrawl', async () => {
|
||||
const preWETHBalance = await zeroEx.token.getBalanceAsync(wethContractAddress, addressWithETH);
|
||||
expect(preWETHBalance).to.be.bignumber.equal(0);
|
||||
|
||||
const overWETHBalance = preWETHBalance.add(999999999);
|
||||
|
||||
return expect(
|
||||
zeroEx.etherToken.withdrawAsync(overWETHBalance, addressWithETH),
|
||||
).to.be.rejectedWith(ZeroExError.InsufficientWEthBalanceForWithdrawal);
|
||||
});
|
||||
});
|
||||
});
|
127
packages/0x.js/test/event_watcher_test.ts
Normal file
127
packages/0x.js/test/event_watcher_test.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import 'mocha';
|
||||
import * as chai from 'chai';
|
||||
import * as _ from 'lodash';
|
||||
import * as Sinon from 'sinon';
|
||||
import * as Web3 from 'web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {chaiSetup} from './utils/chai_setup';
|
||||
import {web3Factory} from './utils/web3_factory';
|
||||
import {Web3Wrapper} from '../src/web3_wrapper';
|
||||
import {EventWatcher} from '../src/order_watcher/event_watcher';
|
||||
import {
|
||||
ZeroEx,
|
||||
LogEvent,
|
||||
DecodedLogEvent,
|
||||
} from '../src';
|
||||
import {DoneCallback} from '../src/types';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('EventWatcher', () => {
|
||||
let web3: Web3;
|
||||
let stubs: Sinon.SinonStub[] = [];
|
||||
let eventWatcher: EventWatcher;
|
||||
let web3Wrapper: Web3Wrapper;
|
||||
const numConfirmations = 0;
|
||||
const logA: Web3.LogEntry = {
|
||||
address: '0x71d271f8b14adef568f8f28f1587ce7271ac4ca5',
|
||||
blockHash: null,
|
||||
blockNumber: null,
|
||||
data: '',
|
||||
logIndex: null,
|
||||
topics: [],
|
||||
transactionHash: '0x004881d38cd4a8f72f1a0d68c8b9b8124504706041ff37019c1d1ed6bfda8e17',
|
||||
transactionIndex: 0,
|
||||
};
|
||||
const logB: Web3.LogEntry = {
|
||||
address: '0x8d12a197cb00d4747a1fe03395095ce2a5cc6819',
|
||||
blockHash: null,
|
||||
blockNumber: null,
|
||||
data: '',
|
||||
logIndex: null,
|
||||
topics: [ '0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567' ],
|
||||
transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25',
|
||||
transactionIndex: 0,
|
||||
};
|
||||
const logC: Web3.LogEntry = {
|
||||
address: '0x1d271f8b174adef58f1587ce68f8f27271ac4ca5',
|
||||
blockHash: null,
|
||||
blockNumber: null,
|
||||
data: '',
|
||||
logIndex: null,
|
||||
topics: [ '0xf341246adaac6f497bc2a656f546ab9e182111d630394f0c57c710a59a2cb567' ],
|
||||
transactionHash: '0x01ef3c048b18d9b09ea195b4ed94cf8dd5f3d857a1905ff886b152cfb1166f25',
|
||||
transactionIndex: 0,
|
||||
};
|
||||
before(async () => {
|
||||
web3 = web3Factory.create();
|
||||
const pollingIntervalMs = 10;
|
||||
web3Wrapper = new Web3Wrapper(web3.currentProvider);
|
||||
eventWatcher = new EventWatcher(web3Wrapper, pollingIntervalMs);
|
||||
});
|
||||
afterEach(() => {
|
||||
// clean up any stubs after the test has completed
|
||||
_.each(stubs, s => s.restore());
|
||||
stubs = [];
|
||||
eventWatcher.unsubscribe();
|
||||
});
|
||||
it('correctly emits initial log events', (done: DoneCallback) => {
|
||||
const logs: Web3.LogEntry[] = [logA, logB];
|
||||
const expectedLogEvents = [
|
||||
{
|
||||
removed: false,
|
||||
...logA,
|
||||
},
|
||||
{
|
||||
removed: false,
|
||||
...logB,
|
||||
},
|
||||
];
|
||||
const getLogsStub = Sinon.stub(web3Wrapper, 'getLogsAsync');
|
||||
getLogsStub.onCall(0).returns(logs);
|
||||
stubs.push(getLogsStub);
|
||||
const callback = (event: LogEvent) => {
|
||||
const expectedLogEvent = expectedLogEvents.shift();
|
||||
expect(event).to.be.deep.equal(expectedLogEvent);
|
||||
if (_.isEmpty(expectedLogEvents)) {
|
||||
done();
|
||||
}
|
||||
};
|
||||
eventWatcher.subscribe(callback);
|
||||
});
|
||||
it('correctly computes the difference and emits only changes', (done: DoneCallback) => {
|
||||
const initialLogs: Web3.LogEntry[] = [logA, logB];
|
||||
const changedLogs: Web3.LogEntry[] = [logA, logC];
|
||||
const expectedLogEvents = [
|
||||
{
|
||||
removed: false,
|
||||
...logA,
|
||||
},
|
||||
{
|
||||
removed: false,
|
||||
...logB,
|
||||
},
|
||||
{
|
||||
removed: true,
|
||||
...logB,
|
||||
},
|
||||
{
|
||||
removed: false,
|
||||
...logC,
|
||||
},
|
||||
];
|
||||
const getLogsStub = Sinon.stub(web3Wrapper, 'getLogsAsync');
|
||||
getLogsStub.onCall(0).returns(initialLogs);
|
||||
getLogsStub.onCall(1).returns(changedLogs);
|
||||
stubs.push(getLogsStub);
|
||||
const callback = (event: LogEvent) => {
|
||||
const expectedLogEvent = expectedLogEvents.shift();
|
||||
expect(event).to.be.deep.equal(expectedLogEvent);
|
||||
if (_.isEmpty(expectedLogEvents)) {
|
||||
done();
|
||||
}
|
||||
};
|
||||
eventWatcher.subscribe(callback);
|
||||
});
|
||||
});
|
87
packages/0x.js/test/exchange_transfer_simulator_test.ts
Normal file
87
packages/0x.js/test/exchange_transfer_simulator_test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import * as chai from 'chai';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {chaiSetup} from './utils/chai_setup';
|
||||
import {web3Factory} from './utils/web3_factory';
|
||||
import {ZeroEx, ExchangeContractErrs, Token} from '../src';
|
||||
import {TradeSide, TransferType} from '../src/types';
|
||||
import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
|
||||
import {ExchangeTransferSimulator} from '../src/utils/exchange_transfer_simulator';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
const blockchainLifecycle = new BlockchainLifecycle();
|
||||
|
||||
describe('ExchangeTransferSimulator', () => {
|
||||
const web3 = web3Factory.create();
|
||||
const zeroEx = new ZeroEx(web3.currentProvider);
|
||||
const transferAmount = new BigNumber(5);
|
||||
let userAddresses: string[];
|
||||
let tokens: Token[];
|
||||
let coinbase: string;
|
||||
let sender: string;
|
||||
let recipient: string;
|
||||
let exampleTokenAddress: string;
|
||||
let exchangeTransferSimulator: ExchangeTransferSimulator;
|
||||
let txHash: string;
|
||||
before(async () => {
|
||||
userAddresses = await zeroEx.getAvailableAddressesAsync();
|
||||
[coinbase, sender, recipient] = userAddresses;
|
||||
tokens = await zeroEx.tokenRegistry.getTokensAsync();
|
||||
exampleTokenAddress = tokens[0].address;
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await blockchainLifecycle.startAsync();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await blockchainLifecycle.revertAsync();
|
||||
});
|
||||
describe('#transferFromAsync', () => {
|
||||
beforeEach(() => {
|
||||
exchangeTransferSimulator = new ExchangeTransferSimulator(zeroEx.token);
|
||||
});
|
||||
it('throws if the user doesn\'t have enough allowance', async () => {
|
||||
return expect(exchangeTransferSimulator.transferFromAsync(
|
||||
exampleTokenAddress, sender, recipient, transferAmount, TradeSide.Taker, TransferType.Trade,
|
||||
)).to.be.rejectedWith(ExchangeContractErrs.InsufficientTakerAllowance);
|
||||
});
|
||||
it('throws if the user doesn\'t have enough balance', async () => {
|
||||
txHash = await zeroEx.token.setProxyAllowanceAsync(exampleTokenAddress, sender, transferAmount);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
return expect(exchangeTransferSimulator.transferFromAsync(
|
||||
exampleTokenAddress, sender, recipient, transferAmount, TradeSide.Maker, TransferType.Trade,
|
||||
)).to.be.rejectedWith(ExchangeContractErrs.InsufficientMakerBalance);
|
||||
});
|
||||
it('updates balances and proxyAllowance after transfer', async () => {
|
||||
txHash = await zeroEx.token.transferAsync(exampleTokenAddress, coinbase, sender, transferAmount);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
txHash = await zeroEx.token.setProxyAllowanceAsync(exampleTokenAddress, sender, transferAmount);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
await exchangeTransferSimulator.transferFromAsync(
|
||||
exampleTokenAddress, sender, recipient, transferAmount, TradeSide.Taker, TransferType.Trade,
|
||||
);
|
||||
const store = (exchangeTransferSimulator as any).store;
|
||||
const senderBalance = await store.getBalanceAsync(exampleTokenAddress, sender);
|
||||
const recipientBalance = await store.getBalanceAsync(exampleTokenAddress, recipient);
|
||||
const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleTokenAddress, sender);
|
||||
expect(senderBalance).to.be.bignumber.equal(0);
|
||||
expect(recipientBalance).to.be.bignumber.equal(transferAmount);
|
||||
expect(senderProxyAllowance).to.be.bignumber.equal(0);
|
||||
});
|
||||
it('doesn\'t update proxyAllowance after transfer if unlimited', async () => {
|
||||
txHash = await zeroEx.token.transferAsync(exampleTokenAddress, coinbase, sender, transferAmount);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(exampleTokenAddress, sender);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
await exchangeTransferSimulator.transferFromAsync(
|
||||
exampleTokenAddress, sender, recipient, transferAmount, TradeSide.Taker, TransferType.Trade,
|
||||
);
|
||||
const store = (exchangeTransferSimulator as any).store;
|
||||
const senderBalance = await store.getBalanceAsync(exampleTokenAddress, sender);
|
||||
const recipientBalance = await store.getBalanceAsync(exampleTokenAddress, recipient);
|
||||
const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleTokenAddress, sender);
|
||||
expect(senderBalance).to.be.bignumber.equal(0);
|
||||
expect(recipientBalance).to.be.bignumber.equal(transferAmount);
|
||||
expect(senderProxyAllowance).to.be.bignumber.equal(zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
|
||||
});
|
||||
});
|
||||
});
|
824
packages/0x.js/test/exchange_wrapper_test.ts
Normal file
824
packages/0x.js/test/exchange_wrapper_test.ts
Normal file
@@ -0,0 +1,824 @@
|
||||
import 'mocha';
|
||||
import * as chai from 'chai';
|
||||
import * as Web3 from 'web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {chaiSetup} from './utils/chai_setup';
|
||||
import {web3Factory} from './utils/web3_factory';
|
||||
import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
|
||||
import {
|
||||
ZeroEx,
|
||||
Token,
|
||||
SignedOrder,
|
||||
SubscriptionOpts,
|
||||
ExchangeEvents,
|
||||
ExchangeContractErrs,
|
||||
OrderCancellationRequest,
|
||||
OrderFillRequest,
|
||||
LogFillContractEventArgs,
|
||||
LogCancelContractEventArgs,
|
||||
LogEvent,
|
||||
DecodedLogEvent,
|
||||
} from '../src';
|
||||
import {DoneCallback, BlockParamLiteral} from '../src/types';
|
||||
import {FillScenarios} from './utils/fill_scenarios';
|
||||
import {TokenUtils} from './utils/token_utils';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
const blockchainLifecycle = new BlockchainLifecycle();
|
||||
|
||||
const NON_EXISTENT_ORDER_HASH = '0x79370342234e7acd6bbeac335bd3bb1d368383294b64b8160a00f4060e4d3777';
|
||||
|
||||
describe('ExchangeWrapper', () => {
|
||||
let web3: Web3;
|
||||
let zeroEx: ZeroEx;
|
||||
let tokenUtils: TokenUtils;
|
||||
let tokens: Token[];
|
||||
let userAddresses: string[];
|
||||
let zrxTokenAddress: string;
|
||||
let fillScenarios: FillScenarios;
|
||||
let exchangeContractAddress: string;
|
||||
before(async () => {
|
||||
web3 = web3Factory.create();
|
||||
zeroEx = new ZeroEx(web3.currentProvider);
|
||||
exchangeContractAddress = await zeroEx.exchange.getContractAddressAsync();
|
||||
userAddresses = await zeroEx.getAvailableAddressesAsync();
|
||||
tokens = await zeroEx.tokenRegistry.getTokensAsync();
|
||||
tokenUtils = new TokenUtils(tokens);
|
||||
zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address;
|
||||
fillScenarios = new FillScenarios(zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress);
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await blockchainLifecycle.startAsync();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await blockchainLifecycle.revertAsync();
|
||||
});
|
||||
describe('fillOrKill order(s)', () => {
|
||||
let makerTokenAddress: string;
|
||||
let takerTokenAddress: string;
|
||||
let coinbase: string;
|
||||
let makerAddress: string;
|
||||
let takerAddress: string;
|
||||
let feeRecipient: string;
|
||||
const takerTokenFillAmount = new BigNumber(5);
|
||||
before(async () => {
|
||||
[coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses;
|
||||
tokens = await zeroEx.tokenRegistry.getTokensAsync();
|
||||
const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
|
||||
makerTokenAddress = makerToken.address;
|
||||
takerTokenAddress = takerToken.address;
|
||||
});
|
||||
describe('#batchFillOrKillAsync', () => {
|
||||
it('successfully batch fillOrKill', async () => {
|
||||
const fillableAmount = new BigNumber(5);
|
||||
const partialFillTakerAmount = new BigNumber(2);
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
const anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
const orderFillRequests = [
|
||||
{
|
||||
signedOrder,
|
||||
takerTokenFillAmount: partialFillTakerAmount,
|
||||
},
|
||||
{
|
||||
signedOrder: anotherSignedOrder,
|
||||
takerTokenFillAmount: partialFillTakerAmount,
|
||||
},
|
||||
];
|
||||
await zeroEx.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress);
|
||||
});
|
||||
describe('order transaction options', () => {
|
||||
let signedOrder: SignedOrder;
|
||||
let orderFillRequests: OrderFillRequest[];
|
||||
const fillableAmount = new BigNumber(5);
|
||||
beforeEach(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
orderFillRequests = [
|
||||
{
|
||||
signedOrder,
|
||||
takerTokenFillAmount: new BigNumber(0),
|
||||
},
|
||||
];
|
||||
});
|
||||
it('should validate when orderTransactionOptions are not present', async () => {
|
||||
return expect(zeroEx.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress))
|
||||
.to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
|
||||
});
|
||||
it('should validate when orderTransactionOptions specify to validate', async () => {
|
||||
return expect(zeroEx.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress, {
|
||||
shouldValidate: true,
|
||||
})).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
|
||||
});
|
||||
it('should not validate when orderTransactionOptions specify not to validate', async () => {
|
||||
return expect(zeroEx.exchange.batchFillOrKillAsync(orderFillRequests, takerAddress, {
|
||||
shouldValidate: false,
|
||||
})).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#fillOrKillOrderAsync', () => {
|
||||
let signedOrder: SignedOrder;
|
||||
const fillableAmount = new BigNumber(5);
|
||||
beforeEach(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
});
|
||||
describe('successful fills', () => {
|
||||
it('should fill a valid order', async () => {
|
||||
expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
|
||||
.to.be.bignumber.equal(fillableAmount);
|
||||
expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
|
||||
.to.be.bignumber.equal(0);
|
||||
expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
|
||||
.to.be.bignumber.equal(0);
|
||||
expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
|
||||
.to.be.bignumber.equal(fillableAmount);
|
||||
await zeroEx.exchange.fillOrKillOrderAsync(signedOrder, takerTokenFillAmount, takerAddress);
|
||||
expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
|
||||
.to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount));
|
||||
expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
|
||||
.to.be.bignumber.equal(takerTokenFillAmount);
|
||||
expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
|
||||
.to.be.bignumber.equal(takerTokenFillAmount);
|
||||
expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
|
||||
.to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount));
|
||||
});
|
||||
it('should partially fill a valid order', async () => {
|
||||
const partialFillAmount = new BigNumber(3);
|
||||
await zeroEx.exchange.fillOrKillOrderAsync(signedOrder, partialFillAmount, takerAddress);
|
||||
expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
|
||||
.to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
|
||||
expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
|
||||
.to.be.bignumber.equal(partialFillAmount);
|
||||
expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
|
||||
.to.be.bignumber.equal(partialFillAmount);
|
||||
expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
|
||||
.to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
|
||||
});
|
||||
});
|
||||
describe('order transaction options', () => {
|
||||
const emptyFillableAmount = new BigNumber(0);
|
||||
it('should validate when orderTransactionOptions are not present', async () => {
|
||||
return expect(zeroEx.exchange.fillOrKillOrderAsync(signedOrder, emptyFillableAmount, takerAddress))
|
||||
.to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
|
||||
});
|
||||
it('should validate when orderTransactionOptions specify to validate', async () => {
|
||||
return expect(zeroEx.exchange.fillOrKillOrderAsync(signedOrder, emptyFillableAmount, takerAddress, {
|
||||
shouldValidate: true,
|
||||
})).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
|
||||
});
|
||||
it('should not validate when orderTransactionOptions specify not to validate', async () => {
|
||||
return expect(zeroEx.exchange.fillOrKillOrderAsync(signedOrder, emptyFillableAmount, takerAddress, {
|
||||
shouldValidate: false,
|
||||
})).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('fill order(s)', () => {
|
||||
let makerTokenAddress: string;
|
||||
let takerTokenAddress: string;
|
||||
let coinbase: string;
|
||||
let makerAddress: string;
|
||||
let takerAddress: string;
|
||||
let feeRecipient: string;
|
||||
const fillableAmount = new BigNumber(5);
|
||||
const takerTokenFillAmount = new BigNumber(5);
|
||||
const shouldThrowOnInsufficientBalanceOrAllowance = true;
|
||||
before(async () => {
|
||||
[coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses;
|
||||
tokens = await zeroEx.tokenRegistry.getTokensAsync();
|
||||
const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
|
||||
makerTokenAddress = makerToken.address;
|
||||
takerTokenAddress = takerToken.address;
|
||||
});
|
||||
describe('#fillOrderAsync', () => {
|
||||
describe('successful fills', () => {
|
||||
it('should fill a valid order', async () => {
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
|
||||
.to.be.bignumber.equal(fillableAmount);
|
||||
expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
|
||||
.to.be.bignumber.equal(0);
|
||||
expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
|
||||
.to.be.bignumber.equal(0);
|
||||
expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
|
||||
.to.be.bignumber.equal(fillableAmount);
|
||||
const txHash = await zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, takerTokenFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
|
||||
.to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount));
|
||||
expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
|
||||
.to.be.bignumber.equal(takerTokenFillAmount);
|
||||
expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
|
||||
.to.be.bignumber.equal(takerTokenFillAmount);
|
||||
expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
|
||||
.to.be.bignumber.equal(fillableAmount.minus(takerTokenFillAmount));
|
||||
});
|
||||
it('should partially fill the valid order', async () => {
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
const partialFillAmount = new BigNumber(3);
|
||||
const txHash = await zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, partialFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, makerAddress))
|
||||
.to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
|
||||
expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, makerAddress))
|
||||
.to.be.bignumber.equal(partialFillAmount);
|
||||
expect(await zeroEx.token.getBalanceAsync(makerTokenAddress, takerAddress))
|
||||
.to.be.bignumber.equal(partialFillAmount);
|
||||
expect(await zeroEx.token.getBalanceAsync(takerTokenAddress, takerAddress))
|
||||
.to.be.bignumber.equal(fillableAmount.minus(partialFillAmount));
|
||||
});
|
||||
it('should fill the valid orders with fees', async () => {
|
||||
const makerFee = new BigNumber(1);
|
||||
const takerFee = new BigNumber(2);
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerFee, takerFee,
|
||||
makerAddress, takerAddress, fillableAmount, feeRecipient,
|
||||
);
|
||||
const txHash = await zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, takerTokenFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
expect(await zeroEx.token.getBalanceAsync(zrxTokenAddress, feeRecipient))
|
||||
.to.be.bignumber.equal(makerFee.plus(takerFee));
|
||||
});
|
||||
});
|
||||
describe('order transaction options', () => {
|
||||
let signedOrder: SignedOrder;
|
||||
const emptyFillTakerAmount = new BigNumber(0);
|
||||
beforeEach(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
});
|
||||
it('should validate when orderTransactionOptions are not present', async () => {
|
||||
return expect(zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, emptyFillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
|
||||
)).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
|
||||
});
|
||||
it('should validate when orderTransactionOptions specify to validate', async () => {
|
||||
return expect(zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, emptyFillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, {
|
||||
shouldValidate: true,
|
||||
})).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
|
||||
});
|
||||
it('should not validate when orderTransactionOptions specify not to validate', async () => {
|
||||
return expect(zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, emptyFillTakerAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, {
|
||||
shouldValidate: false,
|
||||
})).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#batchFillOrdersAsync', () => {
|
||||
let signedOrder: SignedOrder;
|
||||
let signedOrderHashHex: string;
|
||||
let anotherSignedOrder: SignedOrder;
|
||||
let anotherOrderHashHex: string;
|
||||
let orderFillBatch: OrderFillRequest[];
|
||||
beforeEach(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
signedOrderHashHex = ZeroEx.getOrderHashHex(signedOrder);
|
||||
anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
anotherOrderHashHex = ZeroEx.getOrderHashHex(anotherSignedOrder);
|
||||
});
|
||||
describe('successful batch fills', () => {
|
||||
beforeEach(() => {
|
||||
orderFillBatch = [
|
||||
{
|
||||
signedOrder,
|
||||
takerTokenFillAmount,
|
||||
},
|
||||
{
|
||||
signedOrder: anotherSignedOrder,
|
||||
takerTokenFillAmount,
|
||||
},
|
||||
];
|
||||
});
|
||||
it('should throw if a batch is empty', async () => {
|
||||
return expect(zeroEx.exchange.batchFillOrdersAsync(
|
||||
[], shouldThrowOnInsufficientBalanceOrAllowance, takerAddress),
|
||||
).to.be.rejectedWith(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
|
||||
});
|
||||
it('should successfully fill multiple orders', async () => {
|
||||
const txHash = await zeroEx.exchange.batchFillOrdersAsync(
|
||||
orderFillBatch, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
const filledAmount = await zeroEx.exchange.getFilledTakerAmountAsync(signedOrderHashHex);
|
||||
const anotherFilledAmount = await zeroEx.exchange.getFilledTakerAmountAsync(anotherOrderHashHex);
|
||||
expect(filledAmount).to.be.bignumber.equal(takerTokenFillAmount);
|
||||
expect(anotherFilledAmount).to.be.bignumber.equal(takerTokenFillAmount);
|
||||
});
|
||||
});
|
||||
describe('order transaction options', () => {
|
||||
beforeEach(async () => {
|
||||
const emptyFillTakerAmount = new BigNumber(0);
|
||||
orderFillBatch = [
|
||||
{
|
||||
signedOrder,
|
||||
takerTokenFillAmount: emptyFillTakerAmount,
|
||||
},
|
||||
{
|
||||
signedOrder: anotherSignedOrder,
|
||||
takerTokenFillAmount: emptyFillTakerAmount,
|
||||
},
|
||||
];
|
||||
});
|
||||
it('should validate when orderTransactionOptions are not present', async () => {
|
||||
return expect(zeroEx.exchange.batchFillOrdersAsync(
|
||||
orderFillBatch, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress),
|
||||
).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
|
||||
});
|
||||
it('should validate when orderTransactionOptions specify to validate', async () => {
|
||||
return expect(zeroEx.exchange.batchFillOrdersAsync(
|
||||
orderFillBatch, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, {
|
||||
shouldValidate: true,
|
||||
})).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
|
||||
});
|
||||
it('should not validate when orderTransactionOptions specify not to validate', async () => {
|
||||
return expect(zeroEx.exchange.batchFillOrdersAsync(
|
||||
orderFillBatch, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, {
|
||||
shouldValidate: false,
|
||||
})).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#fillOrdersUpTo', () => {
|
||||
let signedOrder: SignedOrder;
|
||||
let signedOrderHashHex: string;
|
||||
let anotherSignedOrder: SignedOrder;
|
||||
let anotherOrderHashHex: string;
|
||||
let signedOrders: SignedOrder[];
|
||||
const fillUpToAmount = fillableAmount.plus(fillableAmount).minus(1);
|
||||
beforeEach(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
signedOrderHashHex = ZeroEx.getOrderHashHex(signedOrder);
|
||||
anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
anotherOrderHashHex = ZeroEx.getOrderHashHex(anotherSignedOrder);
|
||||
signedOrders = [signedOrder, anotherSignedOrder];
|
||||
});
|
||||
describe('successful batch fills', () => {
|
||||
it('should throw if a batch is empty', async () => {
|
||||
return expect(zeroEx.exchange.fillOrdersUpToAsync(
|
||||
[], fillUpToAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress),
|
||||
).to.be.rejectedWith(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem);
|
||||
});
|
||||
it('should successfully fill up to specified amount', async () => {
|
||||
const txHash = await zeroEx.exchange.fillOrdersUpToAsync(
|
||||
signedOrders, fillUpToAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
|
||||
);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
const filledAmount = await zeroEx.exchange.getFilledTakerAmountAsync(signedOrderHashHex);
|
||||
const anotherFilledAmount = await zeroEx.exchange.getFilledTakerAmountAsync(anotherOrderHashHex);
|
||||
expect(filledAmount).to.be.bignumber.equal(fillableAmount);
|
||||
const remainingFillAmount = fillableAmount.minus(1);
|
||||
expect(anotherFilledAmount).to.be.bignumber.equal(remainingFillAmount);
|
||||
});
|
||||
});
|
||||
describe('order transaction options', () => {
|
||||
const emptyFillUpToAmount = new BigNumber(0);
|
||||
it('should validate when orderTransactionOptions are not present', async () => {
|
||||
return expect(zeroEx.exchange.fillOrdersUpToAsync(
|
||||
signedOrders, emptyFillUpToAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
|
||||
)).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
|
||||
});
|
||||
it('should validate when orderTransactionOptions specify to validate', async () => {
|
||||
return expect(zeroEx.exchange.fillOrdersUpToAsync(
|
||||
signedOrders, emptyFillUpToAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, {
|
||||
shouldValidate: true,
|
||||
})).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
|
||||
});
|
||||
it('should not validate when orderTransactionOptions specify not to validate', async () => {
|
||||
return expect(zeroEx.exchange.fillOrdersUpToAsync(
|
||||
signedOrders, emptyFillUpToAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress, {
|
||||
shouldValidate: false,
|
||||
})).to.not.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('cancel order(s)', () => {
|
||||
let makerTokenAddress: string;
|
||||
let takerTokenAddress: string;
|
||||
let coinbase: string;
|
||||
let makerAddress: string;
|
||||
let takerAddress: string;
|
||||
const fillableAmount = new BigNumber(5);
|
||||
let signedOrder: SignedOrder;
|
||||
let orderHashHex: string;
|
||||
const cancelAmount = new BigNumber(3);
|
||||
beforeEach(async () => {
|
||||
[coinbase, makerAddress, takerAddress] = userAddresses;
|
||||
const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
|
||||
makerTokenAddress = makerToken.address;
|
||||
takerTokenAddress = takerToken.address;
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
orderHashHex = ZeroEx.getOrderHashHex(signedOrder);
|
||||
});
|
||||
describe('#cancelOrderAsync', () => {
|
||||
describe('successful cancels', () => {
|
||||
it('should cancel an order', async () => {
|
||||
const txHash = await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmount);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
const cancelledAmount = await zeroEx.exchange.getCanceledTakerAmountAsync(orderHashHex);
|
||||
expect(cancelledAmount).to.be.bignumber.equal(cancelAmount);
|
||||
});
|
||||
});
|
||||
describe('order transaction options', () => {
|
||||
const emptyCancelTakerTokenAmount = new BigNumber(0);
|
||||
it('should validate when orderTransactionOptions are not present', async () => {
|
||||
return expect(zeroEx.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount))
|
||||
.to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
|
||||
});
|
||||
it('should validate when orderTransactionOptions specify to validate', async () => {
|
||||
return expect(zeroEx.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount, {
|
||||
shouldValidate: true,
|
||||
})).to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
|
||||
});
|
||||
it('should not validate when orderTransactionOptions specify not to validate', async () => {
|
||||
return expect(zeroEx.exchange.cancelOrderAsync(signedOrder, emptyCancelTakerTokenAmount, {
|
||||
shouldValidate: false,
|
||||
})).to.not.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#batchCancelOrdersAsync', () => {
|
||||
let anotherSignedOrder: SignedOrder;
|
||||
let anotherOrderHashHex: string;
|
||||
let cancelBatch: OrderCancellationRequest[];
|
||||
beforeEach(async () => {
|
||||
anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
anotherOrderHashHex = ZeroEx.getOrderHashHex(anotherSignedOrder);
|
||||
cancelBatch = [
|
||||
{
|
||||
order: signedOrder,
|
||||
takerTokenCancelAmount: cancelAmount,
|
||||
},
|
||||
{
|
||||
order: anotherSignedOrder,
|
||||
takerTokenCancelAmount: cancelAmount,
|
||||
},
|
||||
];
|
||||
});
|
||||
describe('failed batch cancels', () => {
|
||||
it('should throw when orders have different makers', async () => {
|
||||
const signedOrderWithDifferentMaker = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, takerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
return expect(zeroEx.exchange.batchCancelOrdersAsync([
|
||||
cancelBatch[0],
|
||||
{
|
||||
order: signedOrderWithDifferentMaker,
|
||||
takerTokenCancelAmount: cancelAmount,
|
||||
},
|
||||
])).to.be.rejectedWith(ExchangeContractErrs.MultipleMakersInSingleCancelBatchDisallowed);
|
||||
});
|
||||
});
|
||||
describe('successful batch cancels', () => {
|
||||
it('should cancel a batch of orders', async () => {
|
||||
await zeroEx.exchange.batchCancelOrdersAsync(cancelBatch);
|
||||
const cancelledAmount = await zeroEx.exchange.getCanceledTakerAmountAsync(orderHashHex);
|
||||
const anotherCancelledAmount = await zeroEx.exchange.getCanceledTakerAmountAsync(
|
||||
anotherOrderHashHex,
|
||||
);
|
||||
expect(cancelledAmount).to.be.bignumber.equal(cancelAmount);
|
||||
expect(anotherCancelledAmount).to.be.bignumber.equal(cancelAmount);
|
||||
});
|
||||
});
|
||||
describe('order transaction options', () => {
|
||||
beforeEach(async () => {
|
||||
const emptyTakerTokenCancelAmount = new BigNumber(0);
|
||||
cancelBatch = [
|
||||
{
|
||||
order: signedOrder,
|
||||
takerTokenCancelAmount: emptyTakerTokenCancelAmount,
|
||||
},
|
||||
{
|
||||
order: anotherSignedOrder,
|
||||
takerTokenCancelAmount: emptyTakerTokenCancelAmount,
|
||||
},
|
||||
];
|
||||
});
|
||||
it('should validate when orderTransactionOptions are not present', async () => {
|
||||
return expect(zeroEx.exchange.batchCancelOrdersAsync(cancelBatch))
|
||||
.to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
|
||||
});
|
||||
it('should validate when orderTransactionOptions specify to validate', async () => {
|
||||
return expect(zeroEx.exchange.batchCancelOrdersAsync(cancelBatch, {
|
||||
shouldValidate: true,
|
||||
})).to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
|
||||
});
|
||||
it('should not validate when orderTransactionOptions specify not to validate', async () => {
|
||||
return expect(zeroEx.exchange.batchCancelOrdersAsync(cancelBatch, {
|
||||
shouldValidate: false,
|
||||
})).to.not.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('tests that require partially filled order', () => {
|
||||
let makerTokenAddress: string;
|
||||
let takerTokenAddress: string;
|
||||
let takerAddress: string;
|
||||
let fillableAmount: BigNumber;
|
||||
let partialFillAmount: BigNumber;
|
||||
let signedOrder: SignedOrder;
|
||||
let orderHash: string;
|
||||
before(() => {
|
||||
takerAddress = userAddresses[1];
|
||||
const [makerToken, takerToken] = tokens;
|
||||
makerTokenAddress = makerToken.address;
|
||||
takerTokenAddress = takerToken.address;
|
||||
});
|
||||
beforeEach(async () => {
|
||||
fillableAmount = new BigNumber(5);
|
||||
partialFillAmount = new BigNumber(2);
|
||||
signedOrder = await fillScenarios.createPartiallyFilledSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, takerAddress, fillableAmount, partialFillAmount,
|
||||
);
|
||||
orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
});
|
||||
describe('#getUnavailableTakerAmountAsync', () => {
|
||||
it('should throw if passed an invalid orderHash', async () => {
|
||||
const invalidOrderHashHex = '0x123';
|
||||
return expect(zeroEx.exchange.getUnavailableTakerAmountAsync(invalidOrderHashHex)).to.be.rejected();
|
||||
});
|
||||
it('should return zero if passed a valid but non-existent orderHash', async () => {
|
||||
const unavailableValueT = await zeroEx.exchange.getUnavailableTakerAmountAsync(NON_EXISTENT_ORDER_HASH);
|
||||
expect(unavailableValueT).to.be.bignumber.equal(0);
|
||||
});
|
||||
it('should return the unavailableValueT for a valid and partially filled orderHash', async () => {
|
||||
const unavailableValueT = await zeroEx.exchange.getUnavailableTakerAmountAsync(orderHash);
|
||||
expect(unavailableValueT).to.be.bignumber.equal(partialFillAmount);
|
||||
});
|
||||
});
|
||||
describe('#getFilledTakerAmountAsync', () => {
|
||||
it('should throw if passed an invalid orderHash', async () => {
|
||||
const invalidOrderHashHex = '0x123';
|
||||
return expect(zeroEx.exchange.getFilledTakerAmountAsync(invalidOrderHashHex)).to.be.rejected();
|
||||
});
|
||||
it('should return zero if passed a valid but non-existent orderHash', async () => {
|
||||
const filledValueT = await zeroEx.exchange.getFilledTakerAmountAsync(NON_EXISTENT_ORDER_HASH,
|
||||
);
|
||||
expect(filledValueT).to.be.bignumber.equal(0);
|
||||
});
|
||||
it('should return the filledValueT for a valid and partially filled orderHash', async () => {
|
||||
const filledValueT = await zeroEx.exchange.getFilledTakerAmountAsync(orderHash);
|
||||
expect(filledValueT).to.be.bignumber.equal(partialFillAmount);
|
||||
});
|
||||
});
|
||||
describe('#getCanceledTakerAmountAsync', () => {
|
||||
it('should throw if passed an invalid orderHash', async () => {
|
||||
const invalidOrderHashHex = '0x123';
|
||||
return expect(zeroEx.exchange.getCanceledTakerAmountAsync(invalidOrderHashHex)).to.be.rejected();
|
||||
});
|
||||
it('should return zero if passed a valid but non-existent orderHash', async () => {
|
||||
const cancelledValueT = await zeroEx.exchange.getCanceledTakerAmountAsync(NON_EXISTENT_ORDER_HASH);
|
||||
expect(cancelledValueT).to.be.bignumber.equal(0);
|
||||
});
|
||||
it('should return the cancelledValueT for a valid and partially filled orderHash', async () => {
|
||||
const cancelledValueT = await zeroEx.exchange.getCanceledTakerAmountAsync(orderHash);
|
||||
expect(cancelledValueT).to.be.bignumber.equal(0);
|
||||
});
|
||||
it('should return the cancelledValueT for a valid and cancelled orderHash', async () => {
|
||||
const cancelAmount = fillableAmount.minus(partialFillAmount);
|
||||
await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmount);
|
||||
const cancelledValueT = await zeroEx.exchange.getCanceledTakerAmountAsync(orderHash);
|
||||
expect(cancelledValueT).to.be.bignumber.equal(cancelAmount);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#subscribeAsync', () => {
|
||||
const indexFilterValues = {};
|
||||
const shouldThrowOnInsufficientBalanceOrAllowance = true;
|
||||
let makerTokenAddress: string;
|
||||
let takerTokenAddress: string;
|
||||
let coinbase: string;
|
||||
let takerAddress: string;
|
||||
let makerAddress: string;
|
||||
let fillableAmount: BigNumber;
|
||||
let signedOrder: SignedOrder;
|
||||
const takerTokenFillAmountInBaseUnits = new BigNumber(1);
|
||||
const cancelTakerAmountInBaseUnits = new BigNumber(1);
|
||||
before(() => {
|
||||
[coinbase, makerAddress, takerAddress] = userAddresses;
|
||||
const [makerToken, takerToken] = tokens;
|
||||
makerTokenAddress = makerToken.address;
|
||||
takerTokenAddress = takerToken.address;
|
||||
});
|
||||
beforeEach(async () => {
|
||||
fillableAmount = new BigNumber(5);
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
});
|
||||
afterEach(async () => {
|
||||
zeroEx.exchange.unsubscribeAll();
|
||||
});
|
||||
// Hack: Mocha does not allow a test to be both async and have a `done` callback
|
||||
// Since we need to await the receipt of the event in the `subscribe` callback,
|
||||
// we do need both. A hack is to make the top-level a sync fn w/ a done callback and then
|
||||
// wrap the rest of the test in an async block
|
||||
// Source: https://github.com/mochajs/mocha/issues/2407
|
||||
it('Should receive the LogFill event when an order is filled', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
|
||||
const callback = (err: Error, logEvent: DecodedLogEvent<LogFillContractEventArgs>) => {
|
||||
expect(logEvent.event).to.be.equal(ExchangeEvents.LogFill);
|
||||
done();
|
||||
};
|
||||
await zeroEx.exchange.subscribeAsync(
|
||||
ExchangeEvents.LogFill, indexFilterValues, callback,
|
||||
);
|
||||
await zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, takerTokenFillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance,
|
||||
takerAddress,
|
||||
);
|
||||
})().catch(done);
|
||||
});
|
||||
it('Should receive the LogCancel event when an order is cancelled', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
|
||||
const callback = (err: Error, logEvent: DecodedLogEvent<LogCancelContractEventArgs>) => {
|
||||
expect(logEvent.event).to.be.equal(ExchangeEvents.LogCancel);
|
||||
done();
|
||||
};
|
||||
await zeroEx.exchange.subscribeAsync(
|
||||
ExchangeEvents.LogCancel, indexFilterValues, callback,
|
||||
);
|
||||
await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelTakerAmountInBaseUnits);
|
||||
})().catch(done);
|
||||
});
|
||||
it('Outstanding subscriptions are cancelled when zeroEx.setProviderAsync called', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
|
||||
const callbackNeverToBeCalled = (err: Error, logEvent: DecodedLogEvent<LogFillContractEventArgs>) => {
|
||||
done(new Error('Expected this subscription to have been cancelled'));
|
||||
};
|
||||
await zeroEx.exchange.subscribeAsync(
|
||||
ExchangeEvents.LogFill, indexFilterValues, callbackNeverToBeCalled,
|
||||
);
|
||||
|
||||
const newProvider = web3Factory.getRpcProvider();
|
||||
await zeroEx.setProviderAsync(newProvider);
|
||||
|
||||
const callback = (err: Error, logEvent: DecodedLogEvent<LogFillContractEventArgs>) => {
|
||||
expect(logEvent.event).to.be.equal(ExchangeEvents.LogFill);
|
||||
done();
|
||||
};
|
||||
await zeroEx.exchange.subscribeAsync(
|
||||
ExchangeEvents.LogFill, indexFilterValues, callback,
|
||||
);
|
||||
await zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, takerTokenFillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance,
|
||||
takerAddress,
|
||||
);
|
||||
})().catch(done);
|
||||
});
|
||||
it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
const callbackNeverToBeCalled = (err: Error, logEvent: DecodedLogEvent<LogFillContractEventArgs>) => {
|
||||
done(new Error('Expected this subscription to have been cancelled'));
|
||||
};
|
||||
const subscriptionToken = await zeroEx.exchange.subscribeAsync(
|
||||
ExchangeEvents.LogFill, indexFilterValues, callbackNeverToBeCalled,
|
||||
);
|
||||
zeroEx.exchange.unsubscribe(subscriptionToken);
|
||||
await zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, takerTokenFillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance,
|
||||
takerAddress,
|
||||
);
|
||||
done();
|
||||
})().catch(done);
|
||||
});
|
||||
});
|
||||
describe('#getOrderHashHexUsingContractCallAsync', () => {
|
||||
let makerTokenAddress: string;
|
||||
let takerTokenAddress: string;
|
||||
let makerAddress: string;
|
||||
let takerAddress: string;
|
||||
const fillableAmount = new BigNumber(5);
|
||||
before(async () => {
|
||||
[, makerAddress, takerAddress] = userAddresses;
|
||||
const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
|
||||
makerTokenAddress = makerToken.address;
|
||||
takerTokenAddress = takerToken.address;
|
||||
});
|
||||
it('get\'s the same hash as the local function', async () => {
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
const orderHashFromContract = await (zeroEx.exchange as any)
|
||||
._getOrderHashHexUsingContractCallAsync(signedOrder);
|
||||
expect(orderHash).to.equal(orderHashFromContract);
|
||||
});
|
||||
});
|
||||
describe('#getZRXTokenAddressAsync', () => {
|
||||
it('gets the same token as is in token registry', async () => {
|
||||
const zrxAddress = await zeroEx.exchange.getZRXTokenAddressAsync();
|
||||
const zrxToken = tokenUtils.getProtocolTokenOrThrow();
|
||||
expect(zrxAddress).to.equal(zrxToken.address);
|
||||
});
|
||||
});
|
||||
describe('#getLogsAsync', () => {
|
||||
let makerTokenAddress: string;
|
||||
let takerTokenAddress: string;
|
||||
let makerAddress: string;
|
||||
let takerAddress: string;
|
||||
const fillableAmount = new BigNumber(5);
|
||||
const shouldThrowOnInsufficientBalanceOrAllowance = true;
|
||||
const subscriptionOpts: SubscriptionOpts = {
|
||||
fromBlock: BlockParamLiteral.Earliest,
|
||||
toBlock: BlockParamLiteral.Latest,
|
||||
};
|
||||
let txHash: string;
|
||||
before(async () => {
|
||||
[, makerAddress, takerAddress] = userAddresses;
|
||||
const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
|
||||
makerTokenAddress = makerToken.address;
|
||||
takerTokenAddress = takerToken.address;
|
||||
});
|
||||
it('should get logs with decoded args emitted by LogFill', async () => {
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
txHash = await zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, fillableAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
|
||||
);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
const eventName = ExchangeEvents.LogFill;
|
||||
const indexFilterValues = {};
|
||||
const logs = await zeroEx.exchange.getLogsAsync(eventName, subscriptionOpts, indexFilterValues);
|
||||
expect(logs).to.have.length(1);
|
||||
expect(logs[0].event).to.be.equal(eventName);
|
||||
});
|
||||
it('should only get the logs with the correct event name', async () => {
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
txHash = await zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, fillableAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
|
||||
);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
const differentEventName = ExchangeEvents.LogCancel;
|
||||
const indexFilterValues = {};
|
||||
const logs = await zeroEx.exchange.getLogsAsync(differentEventName, subscriptionOpts, indexFilterValues);
|
||||
expect(logs).to.have.length(0);
|
||||
});
|
||||
it('should only get the logs with the correct indexed fields', async () => {
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
txHash = await zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, fillableAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
|
||||
);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
|
||||
const differentMakerAddress = userAddresses[2];
|
||||
const anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, differentMakerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
txHash = await zeroEx.exchange.fillOrderAsync(
|
||||
anotherSignedOrder, fillableAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
|
||||
);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
|
||||
const eventName = ExchangeEvents.LogFill;
|
||||
const indexFilterValues = {
|
||||
maker: differentMakerAddress,
|
||||
};
|
||||
const logs = await zeroEx.exchange.getLogsAsync<LogFillContractEventArgs>(
|
||||
eventName, subscriptionOpts, indexFilterValues,
|
||||
);
|
||||
expect(logs).to.have.length(1);
|
||||
const args = logs[0].args;
|
||||
expect(args.maker).to.be.equal(differentMakerAddress);
|
||||
});
|
||||
});
|
||||
});
|
356
packages/0x.js/test/order_state_watcher_test.ts
Normal file
356
packages/0x.js/test/order_state_watcher_test.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
import 'mocha';
|
||||
import * as chai from 'chai';
|
||||
import * as _ from 'lodash';
|
||||
import * as Web3 from 'web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { chaiSetup } from './utils/chai_setup';
|
||||
import { web3Factory } from './utils/web3_factory';
|
||||
import { Web3Wrapper } from '../src/web3_wrapper';
|
||||
import { OrderStateWatcher } from '../src/order_watcher/order_state_watcher';
|
||||
import {
|
||||
Token,
|
||||
ZeroEx,
|
||||
LogEvent,
|
||||
DecodedLogEvent,
|
||||
ZeroExConfig,
|
||||
OrderState,
|
||||
SignedOrder,
|
||||
ZeroExError,
|
||||
OrderStateValid,
|
||||
OrderStateInvalid,
|
||||
ExchangeContractErrs,
|
||||
} from '../src';
|
||||
import { TokenUtils } from './utils/token_utils';
|
||||
import { FillScenarios } from './utils/fill_scenarios';
|
||||
import { DoneCallback } from '../src/types';
|
||||
import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
|
||||
import {reportCallbackErrors} from './utils/report_callback_errors';
|
||||
|
||||
const TIMEOUT_MS = 150;
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
const blockchainLifecycle = new BlockchainLifecycle();
|
||||
|
||||
describe('OrderStateWatcher', () => {
|
||||
let web3: Web3;
|
||||
let zeroEx: ZeroEx;
|
||||
let tokens: Token[];
|
||||
let tokenUtils: TokenUtils;
|
||||
let fillScenarios: FillScenarios;
|
||||
let userAddresses: string[];
|
||||
let zrxTokenAddress: string;
|
||||
let exchangeContractAddress: string;
|
||||
let makerToken: Token;
|
||||
let takerToken: Token;
|
||||
let maker: string;
|
||||
let taker: string;
|
||||
let web3Wrapper: Web3Wrapper;
|
||||
let signedOrder: SignedOrder;
|
||||
const fillableAmount = new BigNumber(5);
|
||||
before(async () => {
|
||||
web3 = web3Factory.create();
|
||||
zeroEx = new ZeroEx(web3.currentProvider);
|
||||
exchangeContractAddress = await zeroEx.exchange.getContractAddressAsync();
|
||||
userAddresses = await zeroEx.getAvailableAddressesAsync();
|
||||
[, maker, taker] = userAddresses;
|
||||
tokens = await zeroEx.tokenRegistry.getTokensAsync();
|
||||
tokenUtils = new TokenUtils(tokens);
|
||||
zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address;
|
||||
fillScenarios = new FillScenarios(zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress);
|
||||
[makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
|
||||
web3Wrapper = (zeroEx as any)._web3Wrapper;
|
||||
});
|
||||
describe('#removeOrder', async () => {
|
||||
it('should successfully remove existing order', async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, fillableAmount,
|
||||
);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
expect((zeroEx.orderStateWatcher as any)._orderByOrderHash).to.include({
|
||||
[orderHash]: signedOrder,
|
||||
});
|
||||
let dependentOrderHashes = (zeroEx.orderStateWatcher as any)._dependentOrderHashes;
|
||||
expect(dependentOrderHashes[signedOrder.maker][signedOrder.makerTokenAddress]).to.have.keys(orderHash);
|
||||
zeroEx.orderStateWatcher.removeOrder(orderHash);
|
||||
expect((zeroEx.orderStateWatcher as any)._orderByOrderHash).to.not.include({
|
||||
[orderHash]: signedOrder,
|
||||
});
|
||||
dependentOrderHashes = (zeroEx.orderStateWatcher as any)._dependentOrderHashes;
|
||||
expect(dependentOrderHashes[signedOrder.maker]).to.be.undefined();
|
||||
});
|
||||
it('should no-op when removing a non-existing order', async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, fillableAmount,
|
||||
);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
const nonExistentOrderHash = `0x${orderHash.substr(2).split('').reverse().join('')}`;
|
||||
zeroEx.orderStateWatcher.removeOrder(nonExistentOrderHash);
|
||||
});
|
||||
});
|
||||
describe('#subscribe', async () => {
|
||||
afterEach(async () => {
|
||||
zeroEx.orderStateWatcher.unsubscribe();
|
||||
});
|
||||
it('should fail when trying to subscribe twice', async () => {
|
||||
zeroEx.orderStateWatcher.subscribe(_.noop);
|
||||
expect(() => zeroEx.orderStateWatcher.subscribe(_.noop))
|
||||
.to.throw(ZeroExError.SubscriptionAlreadyPresent);
|
||||
});
|
||||
});
|
||||
describe('tests with cleanup', async () => {
|
||||
afterEach(async () => {
|
||||
zeroEx.orderStateWatcher.unsubscribe();
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
zeroEx.orderStateWatcher.removeOrder(orderHash);
|
||||
});
|
||||
it('should emit orderStateInvalid when maker allowance set to 0 for watched order', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, fillableAmount,
|
||||
);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
expect(orderState.isValid).to.be.false();
|
||||
const invalidOrderState = orderState as OrderStateInvalid;
|
||||
expect(invalidOrderState.orderHash).to.be.equal(orderHash);
|
||||
expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerAllowance);
|
||||
done();
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, new BigNumber(0));
|
||||
})().catch(done);
|
||||
});
|
||||
it('should not emit an orderState event when irrelevant Transfer event received', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, fillableAmount,
|
||||
);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
throw new Error('OrderState callback fired for irrelevant order');
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
const notTheMaker = userAddresses[0];
|
||||
const anyRecipient = taker;
|
||||
const transferAmount = new BigNumber(2);
|
||||
const notTheMakerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, notTheMaker);
|
||||
await zeroEx.token.transferAsync(makerToken.address, notTheMaker, anyRecipient, transferAmount);
|
||||
setTimeout(() => {
|
||||
done();
|
||||
}, TIMEOUT_MS);
|
||||
})().catch(done);
|
||||
});
|
||||
it('should emit orderStateInvalid when maker moves balance backing watched order', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, fillableAmount,
|
||||
);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
expect(orderState.isValid).to.be.false();
|
||||
const invalidOrderState = orderState as OrderStateInvalid;
|
||||
expect(invalidOrderState.orderHash).to.be.equal(orderHash);
|
||||
expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerBalance);
|
||||
done();
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
const anyRecipient = taker;
|
||||
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||
await zeroEx.token.transferAsync(makerToken.address, maker, anyRecipient, makerBalance);
|
||||
})().catch(done);
|
||||
});
|
||||
it('should emit orderStateInvalid when watched order fully filled', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, fillableAmount,
|
||||
);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
|
||||
let eventCount = 0;
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
eventCount++;
|
||||
expect(orderState.isValid).to.be.false();
|
||||
const invalidOrderState = orderState as OrderStateInvalid;
|
||||
expect(invalidOrderState.orderHash).to.be.equal(orderHash);
|
||||
expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderRemainingFillAmountZero);
|
||||
if (eventCount === 2) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
|
||||
const shouldThrowOnInsufficientBalanceOrAllowance = true;
|
||||
await zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, fillableAmount, shouldThrowOnInsufficientBalanceOrAllowance, taker,
|
||||
);
|
||||
})().catch(done);
|
||||
});
|
||||
it('should emit orderStateValid when watched order partially filled', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, fillableAmount,
|
||||
);
|
||||
|
||||
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||
const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker);
|
||||
|
||||
const fillAmountInBaseUnits = new BigNumber(2);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
|
||||
let eventCount = 0;
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
eventCount++;
|
||||
expect(orderState.isValid).to.be.true();
|
||||
const validOrderState = orderState as OrderStateValid;
|
||||
expect(validOrderState.orderHash).to.be.equal(orderHash);
|
||||
const orderRelevantState = validOrderState.orderRelevantState;
|
||||
const remainingMakerBalance = makerBalance.sub(fillAmountInBaseUnits);
|
||||
const remainingFillable = fillableAmount.minus(fillAmountInBaseUnits);
|
||||
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
|
||||
remainingFillable);
|
||||
expect(orderRelevantState.makerBalance).to.be.bignumber.equal(remainingMakerBalance);
|
||||
if (eventCount === 2) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
const shouldThrowOnInsufficientBalanceOrAllowance = true;
|
||||
await zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, fillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, taker,
|
||||
);
|
||||
})().catch(done);
|
||||
});
|
||||
describe('remainingFillableMakerTokenAmount', () => {
|
||||
it('should calculate correct remaining fillable', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
const takerFillableAmount = new BigNumber(10);
|
||||
const makerFillableAmount = new BigNumber(20);
|
||||
signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, makerFillableAmount, takerFillableAmount);
|
||||
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||
const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker);
|
||||
const fillAmountInBaseUnits = new BigNumber(2);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
let eventCount = 0;
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
eventCount++;
|
||||
expect(orderState.isValid).to.be.true();
|
||||
const validOrderState = orderState as OrderStateValid;
|
||||
expect(validOrderState.orderHash).to.be.equal(orderHash);
|
||||
const orderRelevantState = validOrderState.orderRelevantState;
|
||||
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
|
||||
new BigNumber(16));
|
||||
if (eventCount === 2) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
const shouldThrowOnInsufficientBalanceOrAllowance = true;
|
||||
await zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, fillAmountInBaseUnits, shouldThrowOnInsufficientBalanceOrAllowance, taker,
|
||||
);
|
||||
})().catch(done);
|
||||
});
|
||||
it('should equal approved amount when approved amount is lowest', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, fillableAmount,
|
||||
);
|
||||
|
||||
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||
|
||||
const changedMakerApprovalAmount = new BigNumber(3);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
const validOrderState = orderState as OrderStateValid;
|
||||
const orderRelevantState = validOrderState.orderRelevantState;
|
||||
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
|
||||
changedMakerApprovalAmount);
|
||||
done();
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
await zeroEx.token.setProxyAllowanceAsync(makerToken.address, maker, changedMakerApprovalAmount);
|
||||
})().catch(done);
|
||||
});
|
||||
it('should equal balance amount when balance amount is lowest', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, fillableAmount,
|
||||
);
|
||||
|
||||
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||
|
||||
const remainingAmount = new BigNumber(1);
|
||||
const transferAmount = makerBalance.sub(remainingAmount);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
const validOrderState = orderState as OrderStateValid;
|
||||
const orderRelevantState = validOrderState.orderRelevantState;
|
||||
expect(orderRelevantState.remainingFillableMakerTokenAmount).to.be.bignumber.equal(
|
||||
remainingAmount);
|
||||
done();
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
await zeroEx.token.transferAsync(
|
||||
makerToken.address, maker, ZeroEx.NULL_ADDRESS, transferAmount);
|
||||
})().catch(done);
|
||||
});
|
||||
});
|
||||
it('should emit orderStateInvalid when watched order cancelled', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, fillableAmount,
|
||||
);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
expect(orderState.isValid).to.be.false();
|
||||
const invalidOrderState = orderState as OrderStateInvalid;
|
||||
expect(invalidOrderState.orderHash).to.be.equal(orderHash);
|
||||
expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.OrderRemainingFillAmountZero);
|
||||
done();
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
|
||||
const shouldThrowOnInsufficientBalanceOrAllowance = true;
|
||||
await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount);
|
||||
})().catch(done);
|
||||
});
|
||||
it('should emit orderStateValid when watched order partially cancelled', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, fillableAmount,
|
||||
);
|
||||
|
||||
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||
const takerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, taker);
|
||||
|
||||
const cancelAmountInBaseUnits = new BigNumber(2);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
expect(orderState.isValid).to.be.true();
|
||||
const validOrderState = orderState as OrderStateValid;
|
||||
expect(validOrderState.orderHash).to.be.equal(orderHash);
|
||||
const orderRelevantState = validOrderState.orderRelevantState;
|
||||
expect(orderRelevantState.canceledTakerTokenAmount).to.be.bignumber.equal(cancelAmountInBaseUnits);
|
||||
done();
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmountInBaseUnits);
|
||||
})().catch(done);
|
||||
});
|
||||
});
|
||||
});
|
327
packages/0x.js/test/order_validation_test.ts
Normal file
327
packages/0x.js/test/order_validation_test.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import * as chai from 'chai';
|
||||
import * as Web3 from 'web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import * as Sinon from 'sinon';
|
||||
import {chaiSetup} from './utils/chai_setup';
|
||||
import {web3Factory} from './utils/web3_factory';
|
||||
import {ZeroEx, SignedOrder, Token, ExchangeContractErrs, ZeroExError} from '../src';
|
||||
import {TradeSide, TransferType} from '../src/types';
|
||||
import {TokenUtils} from './utils/token_utils';
|
||||
import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
|
||||
import {FillScenarios} from './utils/fill_scenarios';
|
||||
import {OrderValidationUtils} from '../src/utils/order_validation_utils';
|
||||
import {ExchangeTransferSimulator} from '../src/utils/exchange_transfer_simulator';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
const blockchainLifecycle = new BlockchainLifecycle();
|
||||
|
||||
describe('OrderValidation', () => {
|
||||
let web3: Web3;
|
||||
let zeroEx: ZeroEx;
|
||||
let userAddresses: string[];
|
||||
let tokens: Token[];
|
||||
let tokenUtils: TokenUtils;
|
||||
let exchangeContractAddress: string;
|
||||
let zrxTokenAddress: string;
|
||||
let fillScenarios: FillScenarios;
|
||||
let makerTokenAddress: string;
|
||||
let takerTokenAddress: string;
|
||||
let coinbase: string;
|
||||
let makerAddress: string;
|
||||
let takerAddress: string;
|
||||
let feeRecipient: string;
|
||||
let orderValidationUtils: OrderValidationUtils;
|
||||
const fillableAmount = new BigNumber(5);
|
||||
const fillTakerAmount = new BigNumber(5);
|
||||
before(async () => {
|
||||
web3 = web3Factory.create();
|
||||
zeroEx = new ZeroEx(web3.currentProvider);
|
||||
exchangeContractAddress = await zeroEx.exchange.getContractAddressAsync();
|
||||
userAddresses = await zeroEx.getAvailableAddressesAsync();
|
||||
[coinbase, makerAddress, takerAddress, feeRecipient] = userAddresses;
|
||||
tokens = await zeroEx.tokenRegistry.getTokensAsync();
|
||||
tokenUtils = new TokenUtils(tokens);
|
||||
zrxTokenAddress = tokenUtils.getProtocolTokenOrThrow().address;
|
||||
fillScenarios = new FillScenarios(zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress);
|
||||
const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
|
||||
makerTokenAddress = makerToken.address;
|
||||
takerTokenAddress = takerToken.address;
|
||||
orderValidationUtils = new OrderValidationUtils(zeroEx.token, zeroEx.exchange);
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await blockchainLifecycle.startAsync();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await blockchainLifecycle.revertAsync();
|
||||
});
|
||||
describe('validateOrderFillableOrThrowAsync', () => {
|
||||
it('should succeed if the order is fillable', async () => {
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
await zeroEx.exchange.validateOrderFillableOrThrowAsync(
|
||||
signedOrder,
|
||||
);
|
||||
});
|
||||
it('should succeed if the order is asymmetric and fillable', async () => {
|
||||
const makerFillableAmount = fillableAmount;
|
||||
const takerFillableAmount = fillableAmount.minus(4);
|
||||
const signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress,
|
||||
makerFillableAmount, takerFillableAmount,
|
||||
);
|
||||
await zeroEx.exchange.validateOrderFillableOrThrowAsync(
|
||||
signedOrder,
|
||||
);
|
||||
});
|
||||
it('should throw when the order is fully filled or cancelled', async () => {
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount);
|
||||
return expect(zeroEx.exchange.validateOrderFillableOrThrowAsync(
|
||||
signedOrder,
|
||||
)).to.be.rejectedWith(ExchangeContractErrs.OrderRemainingFillAmountZero);
|
||||
});
|
||||
it('should throw when order is expired', async () => {
|
||||
const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress,
|
||||
fillableAmount, expirationInPast,
|
||||
);
|
||||
return expect(zeroEx.exchange.validateOrderFillableOrThrowAsync(
|
||||
signedOrder,
|
||||
)).to.be.rejectedWith(ExchangeContractErrs.OrderFillExpired);
|
||||
});
|
||||
});
|
||||
describe('validateFillOrderAndThrowIfInvalidAsync', () => {
|
||||
it('should throw when the fill amount is zero', async () => {
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
const zeroFillAmount = new BigNumber(0);
|
||||
return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync(
|
||||
signedOrder, zeroFillAmount, takerAddress,
|
||||
)).to.be.rejectedWith(ExchangeContractErrs.OrderFillAmountZero);
|
||||
});
|
||||
it('should throw when the signature is invalid', async () => {
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
// 27 <--> 28
|
||||
signedOrder.ecSignature.v = 27 + (28 - signedOrder.ecSignature.v);
|
||||
return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync(
|
||||
signedOrder, fillableAmount, takerAddress,
|
||||
)).to.be.rejectedWith(ZeroExError.InvalidSignature);
|
||||
});
|
||||
it('should throw when the order is fully filled or cancelled', async () => {
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount);
|
||||
return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync(
|
||||
signedOrder, fillableAmount, takerAddress,
|
||||
)).to.be.rejectedWith(ExchangeContractErrs.OrderRemainingFillAmountZero);
|
||||
});
|
||||
it('should throw when sender is not a taker', async () => {
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
const nonTakerAddress = userAddresses[6];
|
||||
return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync(
|
||||
signedOrder, fillTakerAmount, nonTakerAddress,
|
||||
)).to.be.rejectedWith(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker);
|
||||
});
|
||||
it('should throw when order is expired', async () => {
|
||||
const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress,
|
||||
fillableAmount, expirationInPast,
|
||||
);
|
||||
return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync(
|
||||
signedOrder, fillTakerAmount, takerAddress,
|
||||
)).to.be.rejectedWith(ExchangeContractErrs.OrderFillExpired);
|
||||
});
|
||||
it('should throw when there a rounding error would have occurred', async () => {
|
||||
const makerAmount = new BigNumber(3);
|
||||
const takerAmount = new BigNumber(5);
|
||||
const signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress,
|
||||
makerAmount, takerAmount,
|
||||
);
|
||||
const fillTakerAmountThatCausesRoundingError = new BigNumber(3);
|
||||
return expect(zeroEx.exchange.validateFillOrderThrowIfInvalidAsync(
|
||||
signedOrder, fillTakerAmountThatCausesRoundingError, takerAddress,
|
||||
)).to.be.rejectedWith(ExchangeContractErrs.OrderFillRoundingError);
|
||||
});
|
||||
});
|
||||
describe('#validateFillOrKillOrderAndThrowIfInvalidAsync', () => {
|
||||
it('should throw if remaining fillAmount is less then the desired fillAmount', async () => {
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
const tooLargeFillAmount = new BigNumber(7);
|
||||
const fillAmountDifference = tooLargeFillAmount.minus(fillableAmount);
|
||||
await zeroEx.token.transferAsync(takerTokenAddress, coinbase, takerAddress, fillAmountDifference);
|
||||
await zeroEx.token.setProxyAllowanceAsync(takerTokenAddress, takerAddress, tooLargeFillAmount);
|
||||
await zeroEx.token.transferAsync(makerTokenAddress, coinbase, makerAddress, fillAmountDifference);
|
||||
await zeroEx.token.setProxyAllowanceAsync(makerTokenAddress, makerAddress, tooLargeFillAmount);
|
||||
|
||||
return expect(zeroEx.exchange.validateFillOrKillOrderThrowIfInvalidAsync(
|
||||
signedOrder, tooLargeFillAmount, takerAddress,
|
||||
)).to.be.rejectedWith(ExchangeContractErrs.InsufficientRemainingFillAmount);
|
||||
});
|
||||
});
|
||||
describe('validateCancelOrderAndThrowIfInvalidAsync', () => {
|
||||
let signedOrder: SignedOrder;
|
||||
let orderHashHex: string;
|
||||
const cancelAmount = new BigNumber(3);
|
||||
beforeEach(async () => {
|
||||
[coinbase, makerAddress, takerAddress] = userAddresses;
|
||||
const [makerToken, takerToken] = tokenUtils.getNonProtocolTokens();
|
||||
makerTokenAddress = makerToken.address;
|
||||
takerTokenAddress = takerToken.address;
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, fillableAmount,
|
||||
);
|
||||
orderHashHex = ZeroEx.getOrderHashHex(signedOrder);
|
||||
});
|
||||
it('should throw when cancel amount is zero', async () => {
|
||||
const zeroCancelAmount = new BigNumber(0);
|
||||
return expect(zeroEx.exchange.validateCancelOrderThrowIfInvalidAsync(signedOrder, zeroCancelAmount))
|
||||
.to.be.rejectedWith(ExchangeContractErrs.OrderCancelAmountZero);
|
||||
});
|
||||
it('should throw when order is expired', async () => {
|
||||
const expirationInPast = new BigNumber(1496826058); // 7th Jun 2017
|
||||
const expiredSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress,
|
||||
fillableAmount, expirationInPast,
|
||||
);
|
||||
orderHashHex = ZeroEx.getOrderHashHex(expiredSignedOrder);
|
||||
return expect(zeroEx.exchange.validateCancelOrderThrowIfInvalidAsync(expiredSignedOrder, cancelAmount))
|
||||
.to.be.rejectedWith(ExchangeContractErrs.OrderCancelExpired);
|
||||
});
|
||||
it('should throw when order is already cancelled or filled', async () => {
|
||||
await zeroEx.exchange.cancelOrderAsync(signedOrder, fillableAmount);
|
||||
return expect(zeroEx.exchange.validateCancelOrderThrowIfInvalidAsync(signedOrder, fillableAmount))
|
||||
.to.be.rejectedWith(ExchangeContractErrs.OrderAlreadyCancelledOrFilled);
|
||||
});
|
||||
});
|
||||
describe('#validateFillOrderBalancesAllowancesThrowIfInvalidAsync', () => {
|
||||
let exchangeTransferSimulator: ExchangeTransferSimulator;
|
||||
let transferFromAsync: Sinon.SinonSpy;
|
||||
const bigNumberMatch = (expected: BigNumber) => {
|
||||
return Sinon.match((value: BigNumber) => value.eq(expected));
|
||||
};
|
||||
beforeEach('create exchangeTransferSimulator', async () => {
|
||||
exchangeTransferSimulator = new ExchangeTransferSimulator(zeroEx.token);
|
||||
transferFromAsync = Sinon.spy();
|
||||
exchangeTransferSimulator.transferFromAsync = transferFromAsync as any;
|
||||
});
|
||||
it('should call exchangeTransferSimulator.transferFrom in a correct order', async () => {
|
||||
const makerFee = new BigNumber(2);
|
||||
const takerFee = new BigNumber(2);
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerFee, takerFee,
|
||||
makerAddress, takerAddress, fillableAmount, feeRecipient,
|
||||
);
|
||||
await orderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
|
||||
exchangeTransferSimulator, signedOrder, fillableAmount, takerAddress, zrxTokenAddress,
|
||||
);
|
||||
expect(transferFromAsync.callCount).to.be.equal(4);
|
||||
expect(
|
||||
transferFromAsync.getCall(0).calledWith(
|
||||
makerTokenAddress, makerAddress, takerAddress, bigNumberMatch(fillableAmount),
|
||||
TradeSide.Maker, TransferType.Trade,
|
||||
),
|
||||
).to.be.true();
|
||||
expect(
|
||||
transferFromAsync.getCall(1).calledWith(
|
||||
takerTokenAddress, takerAddress, makerAddress, bigNumberMatch(fillableAmount),
|
||||
TradeSide.Taker, TransferType.Trade,
|
||||
),
|
||||
).to.be.true();
|
||||
expect(
|
||||
transferFromAsync.getCall(2).calledWith(
|
||||
zrxTokenAddress, makerAddress, feeRecipient, bigNumberMatch(makerFee),
|
||||
TradeSide.Maker, TransferType.Fee,
|
||||
),
|
||||
).to.be.true();
|
||||
expect(
|
||||
transferFromAsync.getCall(3).calledWith(
|
||||
zrxTokenAddress, takerAddress, feeRecipient, bigNumberMatch(takerFee),
|
||||
TradeSide.Taker, TransferType.Fee,
|
||||
),
|
||||
).to.be.true();
|
||||
});
|
||||
it('should call exchangeTransferSimulator.transferFrom with correct values for an open order', async () => {
|
||||
const makerFee = new BigNumber(2);
|
||||
const takerFee = new BigNumber(2);
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerFee, takerFee,
|
||||
makerAddress, ZeroEx.NULL_ADDRESS, fillableAmount, feeRecipient,
|
||||
);
|
||||
await orderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
|
||||
exchangeTransferSimulator, signedOrder, fillableAmount, takerAddress, zrxTokenAddress,
|
||||
);
|
||||
expect(transferFromAsync.callCount).to.be.equal(4);
|
||||
expect(
|
||||
transferFromAsync.getCall(0).calledWith(
|
||||
makerTokenAddress, makerAddress, takerAddress, bigNumberMatch(fillableAmount),
|
||||
TradeSide.Maker, TransferType.Trade,
|
||||
),
|
||||
).to.be.true();
|
||||
expect(
|
||||
transferFromAsync.getCall(1).calledWith(
|
||||
takerTokenAddress, takerAddress, makerAddress, bigNumberMatch(fillableAmount),
|
||||
TradeSide.Taker, TransferType.Trade,
|
||||
),
|
||||
).to.be.true();
|
||||
expect(
|
||||
transferFromAsync.getCall(2).calledWith(
|
||||
zrxTokenAddress, makerAddress, feeRecipient, bigNumberMatch(makerFee),
|
||||
TradeSide.Maker, TransferType.Fee,
|
||||
),
|
||||
).to.be.true();
|
||||
expect(
|
||||
transferFromAsync.getCall(3).calledWith(
|
||||
zrxTokenAddress, takerAddress, feeRecipient, bigNumberMatch(takerFee),
|
||||
TradeSide.Taker, TransferType.Fee,
|
||||
),
|
||||
).to.be.true();
|
||||
});
|
||||
it('should correctly round the fillMakerTokenAmount', async () => {
|
||||
const makerTokenAmount = new BigNumber(3);
|
||||
const takerTokenAmount = new BigNumber(1);
|
||||
const signedOrder = await fillScenarios.createAsymmetricFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress, makerTokenAmount, takerTokenAmount,
|
||||
);
|
||||
await orderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
|
||||
exchangeTransferSimulator, signedOrder, takerTokenAmount, takerAddress, zrxTokenAddress,
|
||||
);
|
||||
expect(transferFromAsync.callCount).to.be.equal(4);
|
||||
const makerFillAmount = transferFromAsync.getCall(0).args[3];
|
||||
expect(makerFillAmount).to.be.bignumber.equal(makerTokenAmount);
|
||||
});
|
||||
it('should correctly round the makerFeeAmount', async () => {
|
||||
const makerFee = new BigNumber(2);
|
||||
const takerFee = new BigNumber(4);
|
||||
const signedOrder = await fillScenarios.createFillableSignedOrderWithFeesAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerFee, takerFee, makerAddress, takerAddress,
|
||||
fillableAmount, ZeroEx.NULL_ADDRESS,
|
||||
);
|
||||
const fillTakerTokenAmount = fillableAmount.div(2).round(0);
|
||||
await orderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync(
|
||||
exchangeTransferSimulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress,
|
||||
);
|
||||
const makerPartialFee = makerFee.div(2);
|
||||
const takerPartialFee = takerFee.div(2);
|
||||
expect(transferFromAsync.callCount).to.be.equal(4);
|
||||
const partialMakerFee = transferFromAsync.getCall(2).args[3];
|
||||
expect(partialMakerFee).to.be.bignumber.equal(makerPartialFee);
|
||||
const partialTakerFee = transferFromAsync.getCall(3).args[3];
|
||||
expect(partialTakerFee).to.be.bignumber.equal(takerPartialFee);
|
||||
});
|
||||
});
|
||||
});
|
95
packages/0x.js/test/subscription_test.ts
Normal file
95
packages/0x.js/test/subscription_test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import 'mocha';
|
||||
import * as _ from 'lodash';
|
||||
import * as chai from 'chai';
|
||||
import * as Sinon from 'sinon';
|
||||
import {chaiSetup} from './utils/chai_setup';
|
||||
import * as Web3 from 'web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import promisify = require('es6-promisify');
|
||||
import {web3Factory} from './utils/web3_factory';
|
||||
import {
|
||||
ZeroEx,
|
||||
ZeroExError,
|
||||
Token,
|
||||
ApprovalContractEventArgs,
|
||||
TokenEvents,
|
||||
LogEvent,
|
||||
} from '../src';
|
||||
import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
|
||||
import {TokenUtils} from './utils/token_utils';
|
||||
import {DoneCallback, BlockParamLiteral} from '../src/types';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
const blockchainLifecycle = new BlockchainLifecycle();
|
||||
|
||||
describe('SubscriptionTest', () => {
|
||||
let web3: Web3;
|
||||
let zeroEx: ZeroEx;
|
||||
let userAddresses: string[];
|
||||
let tokens: Token[];
|
||||
let tokenUtils: TokenUtils;
|
||||
let coinbase: string;
|
||||
let addressWithoutFunds: string;
|
||||
before(async () => {
|
||||
web3 = web3Factory.create();
|
||||
zeroEx = new ZeroEx(web3.currentProvider);
|
||||
userAddresses = await zeroEx.getAvailableAddressesAsync();
|
||||
tokens = await zeroEx.tokenRegistry.getTokensAsync();
|
||||
tokenUtils = new TokenUtils(tokens);
|
||||
coinbase = userAddresses[0];
|
||||
addressWithoutFunds = userAddresses[1];
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await blockchainLifecycle.startAsync();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await blockchainLifecycle.revertAsync();
|
||||
});
|
||||
describe('#subscribe', () => {
|
||||
const indexFilterValues = {};
|
||||
const shouldThrowOnInsufficientBalanceOrAllowance = true;
|
||||
let tokenAddress: string;
|
||||
const transferAmount = new BigNumber(42);
|
||||
const allowanceAmount = new BigNumber(42);
|
||||
let stubs: Sinon.SinonStub[] = [];
|
||||
before(() => {
|
||||
const token = tokens[0];
|
||||
tokenAddress = token.address;
|
||||
});
|
||||
afterEach(() => {
|
||||
zeroEx.token.unsubscribeAll();
|
||||
_.each(stubs, s => s.restore());
|
||||
stubs = [];
|
||||
});
|
||||
it('Should receive the Error when an error occurs', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
const callback = (err: Error, logEvent: LogEvent<ApprovalContractEventArgs>) => {
|
||||
expect(err).to.not.be.null();
|
||||
expect(logEvent).to.be.undefined();
|
||||
done();
|
||||
};
|
||||
stubs = [
|
||||
Sinon.stub((zeroEx as any)._web3Wrapper, 'getBlockAsync')
|
||||
.throws("JSON RPC error")
|
||||
]
|
||||
zeroEx.token.subscribe(
|
||||
tokenAddress, TokenEvents.Approval, indexFilterValues, callback);
|
||||
await zeroEx.token.setAllowanceAsync(tokenAddress, coinbase, addressWithoutFunds, allowanceAmount);
|
||||
})().catch(done);
|
||||
});
|
||||
it('Should allow unsubscribeAll to be called successfully after an error', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
const callback = (err: Error, logEvent: LogEvent<ApprovalContractEventArgs>) => { };
|
||||
zeroEx.token.subscribe(
|
||||
tokenAddress, TokenEvents.Approval, indexFilterValues, callback);
|
||||
stubs = [
|
||||
Sinon.stub((zeroEx as any)._web3Wrapper, 'getBlockAsync')
|
||||
.throws("JSON RPC error")
|
||||
]
|
||||
zeroEx.token.unsubscribeAll();
|
||||
done();
|
||||
})().catch(done);
|
||||
});
|
||||
})
|
||||
})
|
123
packages/0x.js/test/token_registry_wrapper_test.ts
Normal file
123
packages/0x.js/test/token_registry_wrapper_test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import * as _ from 'lodash';
|
||||
import 'mocha';
|
||||
import * as chai from 'chai';
|
||||
import {SchemaValidator, schemas} from '0x-json-schemas';
|
||||
import {chaiSetup} from './utils/chai_setup';
|
||||
import {web3Factory} from './utils/web3_factory';
|
||||
import {ZeroEx, Token} from '../src';
|
||||
import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
const blockchainLifecycle = new BlockchainLifecycle();
|
||||
|
||||
const TOKEN_REGISTRY_SIZE_AFTER_MIGRATION = 7;
|
||||
|
||||
describe('TokenRegistryWrapper', () => {
|
||||
let zeroEx: ZeroEx;
|
||||
let tokens: Token[];
|
||||
const tokenAddressBySymbol: {[symbol: string]: string} = {};
|
||||
const tokenAddressByName: {[symbol: string]: string} = {};
|
||||
const tokenBySymbol: {[symbol: string]: Token} = {};
|
||||
const tokenByName: {[symbol: string]: Token} = {};
|
||||
const registeredSymbol = 'ZRX';
|
||||
const registeredName = '0x Protocol Token';
|
||||
const unregisteredSymbol = 'MAL';
|
||||
const unregisteredName = 'Malicious Token';
|
||||
before(async () => {
|
||||
const web3 = web3Factory.create();
|
||||
zeroEx = new ZeroEx(web3.currentProvider);
|
||||
tokens = await zeroEx.tokenRegistry.getTokensAsync();
|
||||
_.map(tokens, token => {
|
||||
tokenAddressBySymbol[token.symbol] = token.address;
|
||||
tokenAddressByName[token.name] = token.address;
|
||||
tokenBySymbol[token.symbol] = token;
|
||||
tokenByName[token.name] = token;
|
||||
});
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await blockchainLifecycle.startAsync();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await blockchainLifecycle.revertAsync();
|
||||
});
|
||||
describe('#getTokensAsync', () => {
|
||||
it('should return all the tokens added to the tokenRegistry during the migration', async () => {
|
||||
expect(tokens).to.have.lengthOf(TOKEN_REGISTRY_SIZE_AFTER_MIGRATION);
|
||||
|
||||
const schemaValidator = new SchemaValidator();
|
||||
_.each(tokens, token => {
|
||||
const validationResult = schemaValidator.validate(token, schemas.tokenSchema);
|
||||
expect(validationResult.errors).to.have.lengthOf(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#getTokenAddressesAsync', () => {
|
||||
it('should return all the token addresses added to the tokenRegistry during the migration', async () => {
|
||||
const tokenAddresses = await zeroEx.tokenRegistry.getTokenAddressesAsync();
|
||||
expect(tokenAddresses).to.have.lengthOf(TOKEN_REGISTRY_SIZE_AFTER_MIGRATION);
|
||||
|
||||
const schemaValidator = new SchemaValidator();
|
||||
_.each(tokenAddresses, tokenAddress => {
|
||||
const validationResult = schemaValidator.validate(tokenAddress, schemas.addressSchema);
|
||||
expect(validationResult.errors).to.have.lengthOf(0);
|
||||
expect(tokenAddress).to.not.be.equal(ZeroEx.NULL_ADDRESS);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#getTokenAddressBySymbol', () => {
|
||||
it('should return correct address for a token in the registry', async () => {
|
||||
const tokenAddress = await zeroEx.tokenRegistry.getTokenAddressBySymbolIfExistsAsync(registeredSymbol);
|
||||
expect(tokenAddress).to.be.equal(tokenAddressBySymbol[registeredSymbol]);
|
||||
});
|
||||
it('should return undefined for a token out of registry', async () => {
|
||||
const tokenAddress = await zeroEx.tokenRegistry.getTokenAddressBySymbolIfExistsAsync(unregisteredSymbol);
|
||||
expect(tokenAddress).to.be.undefined();
|
||||
});
|
||||
});
|
||||
describe('#getTokenAddressByName', () => {
|
||||
it('should return correct address for a token in the registry', async () => {
|
||||
const tokenAddress = await zeroEx.tokenRegistry.getTokenAddressByNameIfExistsAsync(registeredName);
|
||||
expect(tokenAddress).to.be.equal(tokenAddressByName[registeredName]);
|
||||
});
|
||||
it('should return undefined for a token out of registry', async () => {
|
||||
const tokenAddress = await zeroEx.tokenRegistry.getTokenAddressByNameIfExistsAsync(unregisteredName);
|
||||
expect(tokenAddress).to.be.undefined();
|
||||
});
|
||||
});
|
||||
describe('#getTokenBySymbol', () => {
|
||||
it('should return correct token for a token in the registry', async () => {
|
||||
const token = await zeroEx.tokenRegistry.getTokenBySymbolIfExistsAsync(registeredSymbol);
|
||||
expect(token).to.be.deep.equal(tokenBySymbol[registeredSymbol]);
|
||||
});
|
||||
it('should return undefined for a token out of registry', async () => {
|
||||
const token = await zeroEx.tokenRegistry.getTokenBySymbolIfExistsAsync(unregisteredSymbol);
|
||||
expect(token).to.be.undefined();
|
||||
});
|
||||
});
|
||||
describe('#getTokenByName', () => {
|
||||
it('should return correct token for a token in the registry', async () => {
|
||||
const token = await zeroEx.tokenRegistry.getTokenByNameIfExistsAsync(registeredName);
|
||||
expect(token).to.be.deep.equal(tokenByName[registeredName]);
|
||||
});
|
||||
it('should return undefined for a token out of registry', async () => {
|
||||
const token = await zeroEx.tokenRegistry.getTokenByNameIfExistsAsync(unregisteredName);
|
||||
expect(token).to.be.undefined();
|
||||
});
|
||||
});
|
||||
describe('#getTokenIfExistsAsync', () => {
|
||||
it('should return the token added to the tokenRegistry during the migration', async () => {
|
||||
const aToken = tokens[0];
|
||||
|
||||
const token = await zeroEx.tokenRegistry.getTokenIfExistsAsync(aToken.address);
|
||||
const schemaValidator = new SchemaValidator();
|
||||
const validationResult = schemaValidator.validate(token, schemas.tokenSchema);
|
||||
expect(validationResult.errors).to.have.lengthOf(0);
|
||||
});
|
||||
it('should return return undefined when passed a token address not in the tokenRegistry', async () => {
|
||||
const unregisteredTokenAddress = '0x5409ed021d9299bf6814279a6a1411a7e866a631';
|
||||
const tokenIfExists = await zeroEx.tokenRegistry.getTokenIfExistsAsync(unregisteredTokenAddress);
|
||||
expect(tokenIfExists).to.be.undefined();
|
||||
});
|
||||
});
|
||||
});
|
31
packages/0x.js/test/token_transfer_proxy_wrapper_test.ts
Normal file
31
packages/0x.js/test/token_transfer_proxy_wrapper_test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import * as chai from 'chai';
|
||||
import {chaiSetup} from './utils/chai_setup';
|
||||
import {web3Factory} from './utils/web3_factory';
|
||||
import {ZeroEx} from '../src';
|
||||
import {TokenTransferProxyWrapper} from '../src/contract_wrappers/token_transfer_proxy_wrapper';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('TokenTransferProxyWrapper', () => {
|
||||
let zeroEx: ZeroEx;
|
||||
before(async () => {
|
||||
const web3 = web3Factory.create();
|
||||
zeroEx = new ZeroEx(web3.currentProvider);
|
||||
});
|
||||
describe('#isAuthorizedAsync', () => {
|
||||
it('should return false if the address is not authorized', async () => {
|
||||
const isAuthorized = await zeroEx.proxy.isAuthorizedAsync(ZeroEx.NULL_ADDRESS);
|
||||
expect(isAuthorized).to.be.false();
|
||||
});
|
||||
});
|
||||
describe('#getAuthorizedAddressesAsync', () => {
|
||||
it('should return the list of authorized addresses', async () => {
|
||||
const authorizedAddresses = await zeroEx.proxy.getAuthorizedAddressesAsync();
|
||||
for (const authorizedAddress of authorizedAddresses) {
|
||||
const isAuthorized = await zeroEx.proxy.isAuthorizedAsync(authorizedAddress);
|
||||
expect(isAuthorized).to.be.true();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
477
packages/0x.js/test/token_wrapper_test.ts
Normal file
477
packages/0x.js/test/token_wrapper_test.ts
Normal file
@@ -0,0 +1,477 @@
|
||||
import 'mocha';
|
||||
import * as chai from 'chai';
|
||||
import {chaiSetup} from './utils/chai_setup';
|
||||
import * as Web3 from 'web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import promisify = require('es6-promisify');
|
||||
import {web3Factory} from './utils/web3_factory';
|
||||
import {
|
||||
ZeroEx,
|
||||
ZeroExError,
|
||||
Token,
|
||||
SubscriptionOpts,
|
||||
TokenEvents,
|
||||
ContractEvent,
|
||||
TransferContractEventArgs,
|
||||
ApprovalContractEventArgs,
|
||||
TokenContractEventArgs,
|
||||
LogWithDecodedArgs,
|
||||
LogEvent,
|
||||
DecodedLogEvent,
|
||||
} from '../src';
|
||||
import {BlockchainLifecycle} from './utils/blockchain_lifecycle';
|
||||
import {TokenUtils} from './utils/token_utils';
|
||||
import {DoneCallback, BlockParamLiteral} from '../src/types';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
const blockchainLifecycle = new BlockchainLifecycle();
|
||||
|
||||
describe('TokenWrapper', () => {
|
||||
let web3: Web3;
|
||||
let zeroEx: ZeroEx;
|
||||
let userAddresses: string[];
|
||||
let tokens: Token[];
|
||||
let tokenUtils: TokenUtils;
|
||||
let coinbase: string;
|
||||
let addressWithoutFunds: string;
|
||||
before(async () => {
|
||||
web3 = web3Factory.create();
|
||||
zeroEx = new ZeroEx(web3.currentProvider);
|
||||
userAddresses = await zeroEx.getAvailableAddressesAsync();
|
||||
tokens = await zeroEx.tokenRegistry.getTokensAsync();
|
||||
tokenUtils = new TokenUtils(tokens);
|
||||
coinbase = userAddresses[0];
|
||||
addressWithoutFunds = userAddresses[1];
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await blockchainLifecycle.startAsync();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await blockchainLifecycle.revertAsync();
|
||||
});
|
||||
describe('#transferAsync', () => {
|
||||
let token: Token;
|
||||
let transferAmount: BigNumber;
|
||||
before(() => {
|
||||
token = tokens[0];
|
||||
transferAmount = new BigNumber(42);
|
||||
});
|
||||
it('should successfully transfer tokens', async () => {
|
||||
const fromAddress = coinbase;
|
||||
const toAddress = addressWithoutFunds;
|
||||
const preBalance = await zeroEx.token.getBalanceAsync(token.address, toAddress);
|
||||
expect(preBalance).to.be.bignumber.equal(0);
|
||||
const txHash = await zeroEx.token.transferAsync(token.address, fromAddress, toAddress, transferAmount);
|
||||
const receipt = await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
const postBalance = await zeroEx.token.getBalanceAsync(token.address, toAddress);
|
||||
return expect(postBalance).to.be.bignumber.equal(transferAmount);
|
||||
});
|
||||
it('should fail to transfer tokens if fromAddress has an insufficient balance', async () => {
|
||||
const fromAddress = addressWithoutFunds;
|
||||
const toAddress = coinbase;
|
||||
return expect(zeroEx.token.transferAsync(
|
||||
token.address, fromAddress, toAddress, transferAmount,
|
||||
)).to.be.rejectedWith(ZeroExError.InsufficientBalanceForTransfer);
|
||||
});
|
||||
it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => {
|
||||
const nonExistentTokenAddress = '0x9dd402f14d67e001d8efbe6583e51bf9706aa065';
|
||||
const fromAddress = coinbase;
|
||||
const toAddress = coinbase;
|
||||
return expect(zeroEx.token.transferAsync(
|
||||
nonExistentTokenAddress, fromAddress, toAddress, transferAmount,
|
||||
)).to.be.rejectedWith(ZeroExError.ContractDoesNotExist);
|
||||
});
|
||||
});
|
||||
describe('#transferFromAsync', () => {
|
||||
let token: Token;
|
||||
let toAddress: string;
|
||||
let senderAddress: string;
|
||||
before(async () => {
|
||||
token = tokens[0];
|
||||
toAddress = addressWithoutFunds;
|
||||
senderAddress = userAddresses[2];
|
||||
});
|
||||
it('should fail to transfer tokens if fromAddress has insufficient allowance set', async () => {
|
||||
const fromAddress = coinbase;
|
||||
const transferAmount = new BigNumber(42);
|
||||
|
||||
const fromAddressBalance = await zeroEx.token.getBalanceAsync(token.address, fromAddress);
|
||||
expect(fromAddressBalance).to.be.bignumber.greaterThan(transferAmount);
|
||||
|
||||
const fromAddressAllowance = await zeroEx.token.getAllowanceAsync(token.address, fromAddress,
|
||||
toAddress);
|
||||
expect(fromAddressAllowance).to.be.bignumber.equal(0);
|
||||
|
||||
return expect(zeroEx.token.transferFromAsync(
|
||||
token.address, fromAddress, toAddress, senderAddress, transferAmount,
|
||||
)).to.be.rejectedWith(ZeroExError.InsufficientAllowanceForTransfer);
|
||||
});
|
||||
it('[regression] should fail to transfer tokens if set allowance for toAddress instead of senderAddress',
|
||||
async () => {
|
||||
const fromAddress = coinbase;
|
||||
const transferAmount = new BigNumber(42);
|
||||
|
||||
await zeroEx.token.setAllowanceAsync(token.address, fromAddress, toAddress, transferAmount);
|
||||
|
||||
return expect(zeroEx.token.transferFromAsync(
|
||||
token.address, fromAddress, toAddress, senderAddress, transferAmount,
|
||||
)).to.be.rejectedWith(ZeroExError.InsufficientAllowanceForTransfer);
|
||||
});
|
||||
it('should fail to transfer tokens if fromAddress has insufficient balance', async () => {
|
||||
const fromAddress = addressWithoutFunds;
|
||||
const transferAmount = new BigNumber(42);
|
||||
|
||||
const fromAddressBalance = await zeroEx.token.getBalanceAsync(token.address, fromAddress);
|
||||
expect(fromAddressBalance).to.be.bignumber.equal(0);
|
||||
|
||||
await zeroEx.token.setAllowanceAsync(token.address, fromAddress, senderAddress, transferAmount);
|
||||
const fromAddressAllowance = await zeroEx.token.getAllowanceAsync(token.address, fromAddress,
|
||||
senderAddress);
|
||||
expect(fromAddressAllowance).to.be.bignumber.equal(transferAmount);
|
||||
|
||||
return expect(zeroEx.token.transferFromAsync(
|
||||
token.address, fromAddress, toAddress, senderAddress, transferAmount,
|
||||
)).to.be.rejectedWith(ZeroExError.InsufficientBalanceForTransfer);
|
||||
});
|
||||
it('should successfully transfer tokens', async () => {
|
||||
const fromAddress = coinbase;
|
||||
|
||||
const preBalance = await zeroEx.token.getBalanceAsync(token.address, toAddress);
|
||||
expect(preBalance).to.be.bignumber.equal(0);
|
||||
|
||||
const transferAmount = new BigNumber(42);
|
||||
await zeroEx.token.setAllowanceAsync(token.address, fromAddress, senderAddress, transferAmount);
|
||||
|
||||
await zeroEx.token.transferFromAsync(token.address, fromAddress, toAddress, senderAddress,
|
||||
transferAmount);
|
||||
const postBalance = await zeroEx.token.getBalanceAsync(token.address, toAddress);
|
||||
return expect(postBalance).to.be.bignumber.equal(transferAmount);
|
||||
});
|
||||
it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => {
|
||||
const fromAddress = coinbase;
|
||||
const nonExistentTokenAddress = '0x9dd402f14d67e001d8efbe6583e51bf9706aa065';
|
||||
return expect(zeroEx.token.transferFromAsync(
|
||||
nonExistentTokenAddress, fromAddress, toAddress, senderAddress, new BigNumber(42),
|
||||
)).to.be.rejectedWith(ZeroExError.ContractDoesNotExist);
|
||||
});
|
||||
});
|
||||
describe('#getBalanceAsync', () => {
|
||||
describe('With web3 provider with accounts', () => {
|
||||
it('should return the balance for an existing ERC20 token', async () => {
|
||||
const token = tokens[0];
|
||||
const ownerAddress = coinbase;
|
||||
const balance = await zeroEx.token.getBalanceAsync(token.address, ownerAddress);
|
||||
const expectedBalance = new BigNumber('1000000000000000000000000000');
|
||||
return expect(balance).to.be.bignumber.equal(expectedBalance);
|
||||
});
|
||||
it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => {
|
||||
const nonExistentTokenAddress = '0x9dd402f14d67e001d8efbe6583e51bf9706aa065';
|
||||
const ownerAddress = coinbase;
|
||||
return expect(zeroEx.token.getBalanceAsync(nonExistentTokenAddress, ownerAddress))
|
||||
.to.be.rejectedWith(ZeroExError.ContractDoesNotExist);
|
||||
});
|
||||
it('should return a balance of 0 for a non-existent owner address', async () => {
|
||||
const token = tokens[0];
|
||||
const nonExistentOwner = '0x198c6ad858f213fb31b6fe809e25040e6b964593';
|
||||
const balance = await zeroEx.token.getBalanceAsync(token.address, nonExistentOwner);
|
||||
const expectedBalance = new BigNumber(0);
|
||||
return expect(balance).to.be.bignumber.equal(expectedBalance);
|
||||
});
|
||||
});
|
||||
describe('With web3 provider without accounts', () => {
|
||||
let zeroExWithoutAccounts: ZeroEx;
|
||||
before(async () => {
|
||||
const hasAddresses = false;
|
||||
const web3WithoutAccounts = web3Factory.create(hasAddresses);
|
||||
zeroExWithoutAccounts = new ZeroEx(web3WithoutAccounts.currentProvider);
|
||||
});
|
||||
it('should return balance even when called with Web3 provider instance without addresses', async () => {
|
||||
const token = tokens[0];
|
||||
const ownerAddress = coinbase;
|
||||
const balance = await zeroExWithoutAccounts.token.getBalanceAsync(token.address, ownerAddress);
|
||||
const expectedBalance = new BigNumber('1000000000000000000000000000');
|
||||
return expect(balance).to.be.bignumber.equal(expectedBalance);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#setAllowanceAsync', () => {
|
||||
it('should set the spender\'s allowance', async () => {
|
||||
const token = tokens[0];
|
||||
const ownerAddress = coinbase;
|
||||
const spenderAddress = addressWithoutFunds;
|
||||
|
||||
const allowanceBeforeSet = await zeroEx.token.getAllowanceAsync(token.address, ownerAddress,
|
||||
spenderAddress);
|
||||
const expectedAllowanceBeforeAllowanceSet = new BigNumber(0);
|
||||
expect(allowanceBeforeSet).to.be.bignumber.equal(expectedAllowanceBeforeAllowanceSet);
|
||||
|
||||
const amountInBaseUnits = new BigNumber(50);
|
||||
await zeroEx.token.setAllowanceAsync(token.address, ownerAddress, spenderAddress, amountInBaseUnits);
|
||||
|
||||
const allowanceAfterSet = await zeroEx.token.getAllowanceAsync(token.address, ownerAddress, spenderAddress);
|
||||
const expectedAllowanceAfterAllowanceSet = amountInBaseUnits;
|
||||
return expect(allowanceAfterSet).to.be.bignumber.equal(expectedAllowanceAfterAllowanceSet);
|
||||
});
|
||||
});
|
||||
describe('#setUnlimitedAllowanceAsync', () => {
|
||||
it('should set the unlimited spender\'s allowance', async () => {
|
||||
const token = tokens[0];
|
||||
const ownerAddress = coinbase;
|
||||
const spenderAddress = addressWithoutFunds;
|
||||
|
||||
await zeroEx.token.setUnlimitedAllowanceAsync(token.address, ownerAddress, spenderAddress);
|
||||
const allowance = await zeroEx.token.getAllowanceAsync(token.address, ownerAddress, spenderAddress);
|
||||
return expect(allowance).to.be.bignumber.equal(zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
|
||||
});
|
||||
it('should reduce the gas cost for transfers including tokens with unlimited allowance support', async () => {
|
||||
const transferAmount = new BigNumber(5);
|
||||
const zrx = tokenUtils.getProtocolTokenOrThrow();
|
||||
const [, userWithNormalAllowance, userWithUnlimitedAllowance] = userAddresses;
|
||||
await zeroEx.token.setAllowanceAsync(zrx.address, coinbase, userWithNormalAllowance, transferAmount);
|
||||
await zeroEx.token.setUnlimitedAllowanceAsync(zrx.address, coinbase, userWithUnlimitedAllowance);
|
||||
|
||||
const initBalanceWithNormalAllowance = await promisify(web3.eth.getBalance)(userWithNormalAllowance);
|
||||
const initBalanceWithUnlimitedAllowance = await promisify(web3.eth.getBalance)(userWithUnlimitedAllowance);
|
||||
|
||||
await zeroEx.token.transferFromAsync(
|
||||
zrx.address, coinbase, userWithNormalAllowance, userWithNormalAllowance, transferAmount,
|
||||
);
|
||||
await zeroEx.token.transferFromAsync(
|
||||
zrx.address, coinbase, userWithUnlimitedAllowance, userWithUnlimitedAllowance, transferAmount,
|
||||
);
|
||||
|
||||
const finalBalanceWithNormalAllowance = await promisify(web3.eth.getBalance)(userWithNormalAllowance);
|
||||
const finalBalanceWithUnlimitedAllowance = await promisify(web3.eth.getBalance)(userWithUnlimitedAllowance);
|
||||
|
||||
const normalGasCost = initBalanceWithNormalAllowance.minus(finalBalanceWithNormalAllowance);
|
||||
const unlimitedGasCost = initBalanceWithUnlimitedAllowance.minus(finalBalanceWithUnlimitedAllowance);
|
||||
|
||||
// In theory the gas cost with unlimited allowance should be smaller, but with testrpc it's actually bigger.
|
||||
// This needs to be investigated in ethereumjs-vm. This test is essentially a repro.
|
||||
// TODO: Make this test pass with inverted assertion.
|
||||
expect(unlimitedGasCost.toNumber()).to.be.gt(normalGasCost.toNumber());
|
||||
});
|
||||
});
|
||||
describe('#getAllowanceAsync', () => {
|
||||
describe('With web3 provider with accounts', () => {
|
||||
it('should get the proxy allowance', async () => {
|
||||
const token = tokens[0];
|
||||
const ownerAddress = coinbase;
|
||||
const spenderAddress = addressWithoutFunds;
|
||||
|
||||
const amountInBaseUnits = new BigNumber(50);
|
||||
await zeroEx.token.setAllowanceAsync(token.address, ownerAddress, spenderAddress, amountInBaseUnits);
|
||||
|
||||
const allowance = await zeroEx.token.getAllowanceAsync(token.address, ownerAddress, spenderAddress);
|
||||
const expectedAllowance = amountInBaseUnits;
|
||||
return expect(allowance).to.be.bignumber.equal(expectedAllowance);
|
||||
});
|
||||
it('should return 0 if no allowance set yet', async () => {
|
||||
const token = tokens[0];
|
||||
const ownerAddress = coinbase;
|
||||
const spenderAddress = addressWithoutFunds;
|
||||
const allowance = await zeroEx.token.getAllowanceAsync(token.address, ownerAddress, spenderAddress);
|
||||
const expectedAllowance = new BigNumber(0);
|
||||
return expect(allowance).to.be.bignumber.equal(expectedAllowance);
|
||||
});
|
||||
});
|
||||
describe('With web3 provider without accounts', () => {
|
||||
let zeroExWithoutAccounts: ZeroEx;
|
||||
before(async () => {
|
||||
const hasAddresses = false;
|
||||
const web3WithoutAccounts = web3Factory.create(hasAddresses);
|
||||
zeroExWithoutAccounts = new ZeroEx(web3WithoutAccounts.currentProvider);
|
||||
});
|
||||
it('should get the proxy allowance', async () => {
|
||||
const token = tokens[0];
|
||||
const ownerAddress = coinbase;
|
||||
const spenderAddress = addressWithoutFunds;
|
||||
|
||||
const amountInBaseUnits = new BigNumber(50);
|
||||
await zeroEx.token.setAllowanceAsync(token.address, ownerAddress, spenderAddress, amountInBaseUnits);
|
||||
|
||||
const allowance = await zeroExWithoutAccounts.token.getAllowanceAsync(
|
||||
token.address, ownerAddress, spenderAddress,
|
||||
);
|
||||
const expectedAllowance = amountInBaseUnits;
|
||||
return expect(allowance).to.be.bignumber.equal(expectedAllowance);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#getProxyAllowanceAsync', () => {
|
||||
it('should get the proxy allowance', async () => {
|
||||
const token = tokens[0];
|
||||
const ownerAddress = coinbase;
|
||||
|
||||
const amountInBaseUnits = new BigNumber(50);
|
||||
await zeroEx.token.setProxyAllowanceAsync(token.address, ownerAddress, amountInBaseUnits);
|
||||
|
||||
const allowance = await zeroEx.token.getProxyAllowanceAsync(token.address, ownerAddress);
|
||||
const expectedAllowance = amountInBaseUnits;
|
||||
return expect(allowance).to.be.bignumber.equal(expectedAllowance);
|
||||
});
|
||||
});
|
||||
describe('#setProxyAllowanceAsync', () => {
|
||||
it('should set the proxy allowance', async () => {
|
||||
const token = tokens[0];
|
||||
const ownerAddress = coinbase;
|
||||
|
||||
const allowanceBeforeSet = await zeroEx.token.getProxyAllowanceAsync(token.address, ownerAddress);
|
||||
const expectedAllowanceBeforeAllowanceSet = new BigNumber(0);
|
||||
expect(allowanceBeforeSet).to.be.bignumber.equal(expectedAllowanceBeforeAllowanceSet);
|
||||
|
||||
const amountInBaseUnits = new BigNumber(50);
|
||||
await zeroEx.token.setProxyAllowanceAsync(token.address, ownerAddress, amountInBaseUnits);
|
||||
|
||||
const allowanceAfterSet = await zeroEx.token.getProxyAllowanceAsync(token.address, ownerAddress);
|
||||
const expectedAllowanceAfterAllowanceSet = amountInBaseUnits;
|
||||
return expect(allowanceAfterSet).to.be.bignumber.equal(expectedAllowanceAfterAllowanceSet);
|
||||
});
|
||||
});
|
||||
describe('#setUnlimitedProxyAllowanceAsync', () => {
|
||||
it('should set the unlimited proxy allowance', async () => {
|
||||
const token = tokens[0];
|
||||
const ownerAddress = coinbase;
|
||||
|
||||
await zeroEx.token.setUnlimitedProxyAllowanceAsync(token.address, ownerAddress);
|
||||
const allowance = await zeroEx.token.getProxyAllowanceAsync(token.address, ownerAddress);
|
||||
return expect(allowance).to.be.bignumber.equal(zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
|
||||
});
|
||||
});
|
||||
describe('#subscribe', () => {
|
||||
const indexFilterValues = {};
|
||||
const shouldThrowOnInsufficientBalanceOrAllowance = true;
|
||||
let tokenAddress: string;
|
||||
const transferAmount = new BigNumber(42);
|
||||
const allowanceAmount = new BigNumber(42);
|
||||
before(() => {
|
||||
const token = tokens[0];
|
||||
tokenAddress = token.address;
|
||||
});
|
||||
afterEach(() => {
|
||||
zeroEx.token.unsubscribeAll();
|
||||
});
|
||||
// Hack: Mocha does not allow a test to be both async and have a `done` callback
|
||||
// Since we need to await the receipt of the event in the `subscribe` callback,
|
||||
// we do need both. A hack is to make the top-level a sync fn w/ a done callback and then
|
||||
// wrap the rest of the test in an async block
|
||||
// Source: https://github.com/mochajs/mocha/issues/2407
|
||||
it('Should receive the Transfer event when tokens are transfered', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
const callback = (err: Error, logEvent: DecodedLogEvent<TransferContractEventArgs>) => {
|
||||
expect(logEvent).to.not.be.undefined();
|
||||
const args = logEvent.args;
|
||||
expect(args._from).to.be.equal(coinbase);
|
||||
expect(args._to).to.be.equal(addressWithoutFunds);
|
||||
expect(args._value).to.be.bignumber.equal(transferAmount);
|
||||
done();
|
||||
};
|
||||
zeroEx.token.subscribe(
|
||||
tokenAddress, TokenEvents.Transfer, indexFilterValues, callback);
|
||||
await zeroEx.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount);
|
||||
})().catch(done);
|
||||
});
|
||||
it('Should receive the Approval event when allowance is being set', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
const callback = (err: Error, logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => {
|
||||
expect(logEvent).to.not.be.undefined();
|
||||
const args = logEvent.args;
|
||||
expect(args._owner).to.be.equal(coinbase);
|
||||
expect(args._spender).to.be.equal(addressWithoutFunds);
|
||||
expect(args._value).to.be.bignumber.equal(allowanceAmount);
|
||||
done();
|
||||
};
|
||||
zeroEx.token.subscribe(
|
||||
tokenAddress, TokenEvents.Approval, indexFilterValues, callback);
|
||||
await zeroEx.token.setAllowanceAsync(tokenAddress, coinbase, addressWithoutFunds, allowanceAmount);
|
||||
})().catch(done);
|
||||
});
|
||||
it('Outstanding subscriptions are cancelled when zeroEx.setProviderAsync called', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
const callbackNeverToBeCalled = (err: Error, logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => {
|
||||
done(new Error('Expected this subscription to have been cancelled'));
|
||||
};
|
||||
zeroEx.token.subscribe(
|
||||
tokenAddress, TokenEvents.Transfer, indexFilterValues, callbackNeverToBeCalled,
|
||||
);
|
||||
const callbackToBeCalled = (err: Error, logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => {
|
||||
done();
|
||||
};
|
||||
const newProvider = web3Factory.getRpcProvider();
|
||||
await zeroEx.setProviderAsync(newProvider);
|
||||
zeroEx.token.subscribe(
|
||||
tokenAddress, TokenEvents.Transfer, indexFilterValues, callbackToBeCalled,
|
||||
);
|
||||
await zeroEx.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount);
|
||||
})().catch(done);
|
||||
});
|
||||
it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => {
|
||||
(async () => {
|
||||
const callbackNeverToBeCalled = (err: Error, logEvent: DecodedLogEvent<ApprovalContractEventArgs>) => {
|
||||
done(new Error('Expected this subscription to have been cancelled'));
|
||||
};
|
||||
const subscriptionToken = zeroEx.token.subscribe(
|
||||
tokenAddress, TokenEvents.Transfer, indexFilterValues, callbackNeverToBeCalled);
|
||||
zeroEx.token.unsubscribe(subscriptionToken);
|
||||
await zeroEx.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount);
|
||||
done();
|
||||
})().catch(done);
|
||||
});
|
||||
});
|
||||
describe('#getLogsAsync', () => {
|
||||
let tokenAddress: string;
|
||||
let tokenTransferProxyAddress: string;
|
||||
const subscriptionOpts: SubscriptionOpts = {
|
||||
fromBlock: BlockParamLiteral.Earliest,
|
||||
toBlock: BlockParamLiteral.Latest,
|
||||
};
|
||||
let txHash: string;
|
||||
before(async () => {
|
||||
const token = tokens[0];
|
||||
tokenAddress = token.address;
|
||||
tokenTransferProxyAddress = await zeroEx.proxy.getContractAddressAsync();
|
||||
});
|
||||
it('should get logs with decoded args emitted by Approval', async () => {
|
||||
txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
const eventName = TokenEvents.Approval;
|
||||
const indexFilterValues = {};
|
||||
const logs = await zeroEx.token.getLogsAsync<ApprovalContractEventArgs>(
|
||||
tokenAddress, eventName, subscriptionOpts, indexFilterValues,
|
||||
);
|
||||
expect(logs).to.have.length(1);
|
||||
const args = logs[0].args;
|
||||
expect(logs[0].event).to.be.equal(eventName);
|
||||
expect(args._owner).to.be.equal(coinbase);
|
||||
expect(args._spender).to.be.equal(tokenTransferProxyAddress);
|
||||
expect(args._value).to.be.bignumber.equal(zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
|
||||
});
|
||||
it('should only get the logs with the correct event name', async () => {
|
||||
txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
const differentEventName = TokenEvents.Transfer;
|
||||
const indexFilterValues = {};
|
||||
const logs = await zeroEx.token.getLogsAsync(
|
||||
tokenAddress, differentEventName, subscriptionOpts, indexFilterValues,
|
||||
);
|
||||
expect(logs).to.have.length(0);
|
||||
});
|
||||
it('should only get the logs with the correct indexed fields', async () => {
|
||||
txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
txHash = await zeroEx.token.setUnlimitedProxyAllowanceAsync(tokenAddress, addressWithoutFunds);
|
||||
await zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
const eventName = TokenEvents.Approval;
|
||||
const indexFilterValues = {
|
||||
_owner: coinbase,
|
||||
};
|
||||
const logs = await zeroEx.token.getLogsAsync<ApprovalContractEventArgs>(
|
||||
tokenAddress, eventName, subscriptionOpts, indexFilterValues,
|
||||
);
|
||||
expect(logs).to.have.length(1);
|
||||
const args = logs[0].args;
|
||||
expect(args._owner).to.be.equal(coinbase);
|
||||
});
|
||||
});
|
||||
});
|
26
packages/0x.js/test/utils/blockchain_lifecycle.ts
Normal file
26
packages/0x.js/test/utils/blockchain_lifecycle.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {RPC} from './rpc';
|
||||
|
||||
export class BlockchainLifecycle {
|
||||
private rpc: RPC;
|
||||
private snapshotIdsStack: number[];
|
||||
constructor() {
|
||||
this.rpc = new RPC();
|
||||
this.snapshotIdsStack = [];
|
||||
}
|
||||
// TODO: In order to run these tests on an actual node, we should check if we are running against
|
||||
// TestRPC, if so, use snapshots, otherwise re-deploy contracts before every test
|
||||
public async startAsync(): Promise<void> {
|
||||
const snapshotId = await this.rpc.takeSnapshotAsync();
|
||||
this.snapshotIdsStack.push(snapshotId);
|
||||
}
|
||||
public async revertAsync(): Promise<void> {
|
||||
const snapshotId = this.snapshotIdsStack.pop() as number;
|
||||
const didRevert = await this.rpc.revertSnapshotAsync(snapshotId);
|
||||
if (!didRevert) {
|
||||
throw new Error(`Snapshot with id #${snapshotId} failed to revert`);
|
||||
}
|
||||
}
|
||||
public async mineABlock(): Promise<void> {
|
||||
await this.rpc.mineBlockAsync();
|
||||
}
|
||||
}
|
13
packages/0x.js/test/utils/chai_setup.ts
Normal file
13
packages/0x.js/test/utils/chai_setup.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import * as chai from 'chai';
|
||||
import * as dirtyChai from 'dirty-chai';
|
||||
import ChaiBigNumber = require('chai-bignumber');
|
||||
import chaiAsPromised = require('chai-as-promised');
|
||||
|
||||
export const chaiSetup = {
|
||||
configure() {
|
||||
chai.config.includeStack = true;
|
||||
chai.use(ChaiBigNumber());
|
||||
chai.use(dirtyChai);
|
||||
chai.use(chaiAsPromised);
|
||||
},
|
||||
};
|
8
packages/0x.js/test/utils/constants.ts
Normal file
8
packages/0x.js/test/utils/constants.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const constants = {
|
||||
NULL_ADDRESS: '0x0000000000000000000000000000000000000000',
|
||||
RPC_HOST: 'localhost',
|
||||
RPC_PORT: 8545,
|
||||
TESTRPC_NETWORK_ID: 50,
|
||||
KOVAN_RPC_URL: 'https://kovan.infura.io',
|
||||
ROPSTEN_RPC_URL: 'https://ropsten.infura.io',
|
||||
};
|
114
packages/0x.js/test/utils/fill_scenarios.ts
Normal file
114
packages/0x.js/test/utils/fill_scenarios.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {ZeroEx, Token, SignedOrder} from '../../src';
|
||||
import {orderFactory} from '../utils/order_factory';
|
||||
import {constants} from './constants';
|
||||
|
||||
export class FillScenarios {
|
||||
private zeroEx: ZeroEx;
|
||||
private userAddresses: string[];
|
||||
private tokens: Token[];
|
||||
private coinbase: string;
|
||||
private zrxTokenAddress: string;
|
||||
private exchangeContractAddress: string;
|
||||
constructor(zeroEx: ZeroEx, userAddresses: string[],
|
||||
tokens: Token[], zrxTokenAddress: string, exchangeContractAddress: string) {
|
||||
this.zeroEx = zeroEx;
|
||||
this.userAddresses = userAddresses;
|
||||
this.tokens = tokens;
|
||||
this.coinbase = userAddresses[0];
|
||||
this.zrxTokenAddress = zrxTokenAddress;
|
||||
this.exchangeContractAddress = exchangeContractAddress;
|
||||
}
|
||||
public async createFillableSignedOrderAsync(makerTokenAddress: string, takerTokenAddress: string,
|
||||
makerAddress: string, takerAddress: string,
|
||||
fillableAmount: BigNumber,
|
||||
expirationUnixTimestampSec?: BigNumber):
|
||||
Promise<SignedOrder> {
|
||||
return this.createAsymmetricFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress,
|
||||
fillableAmount, fillableAmount, expirationUnixTimestampSec,
|
||||
);
|
||||
}
|
||||
public async createFillableSignedOrderWithFeesAsync(
|
||||
makerTokenAddress: string, takerTokenAddress: string,
|
||||
makerFee: BigNumber, takerFee: BigNumber,
|
||||
makerAddress: string, takerAddress: string,
|
||||
fillableAmount: BigNumber,
|
||||
feeRecepient: string, expirationUnixTimestampSec?: BigNumber,
|
||||
): Promise<SignedOrder> {
|
||||
return this.createAsymmetricFillableSignedOrderWithFeesAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerFee, takerFee, makerAddress, takerAddress,
|
||||
fillableAmount, fillableAmount, feeRecepient, expirationUnixTimestampSec,
|
||||
);
|
||||
}
|
||||
public async createAsymmetricFillableSignedOrderAsync(
|
||||
makerTokenAddress: string, takerTokenAddress: string, makerAddress: string, takerAddress: string,
|
||||
makerFillableAmount: BigNumber, takerFillableAmount: BigNumber,
|
||||
expirationUnixTimestampSec?: BigNumber): Promise<SignedOrder> {
|
||||
const makerFee = new BigNumber(0);
|
||||
const takerFee = new BigNumber(0);
|
||||
const feeRecepient = constants.NULL_ADDRESS;
|
||||
return this.createAsymmetricFillableSignedOrderWithFeesAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerFee, takerFee, makerAddress, takerAddress,
|
||||
makerFillableAmount, takerFillableAmount, feeRecepient, expirationUnixTimestampSec,
|
||||
);
|
||||
}
|
||||
public async createPartiallyFilledSignedOrderAsync(makerTokenAddress: string, takerTokenAddress: string,
|
||||
takerAddress: string, fillableAmount: BigNumber,
|
||||
partialFillAmount: BigNumber) {
|
||||
const [makerAddress] = this.userAddresses;
|
||||
const signedOrder = await this.createAsymmetricFillableSignedOrderAsync(
|
||||
makerTokenAddress, takerTokenAddress, makerAddress, takerAddress,
|
||||
fillableAmount, fillableAmount,
|
||||
);
|
||||
const shouldThrowOnInsufficientBalanceOrAllowance = false;
|
||||
await this.zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, partialFillAmount, shouldThrowOnInsufficientBalanceOrAllowance, takerAddress,
|
||||
);
|
||||
return signedOrder;
|
||||
}
|
||||
private async createAsymmetricFillableSignedOrderWithFeesAsync(
|
||||
makerTokenAddress: string, takerTokenAddress: string,
|
||||
makerFee: BigNumber, takerFee: BigNumber,
|
||||
makerAddress: string, takerAddress: string,
|
||||
makerFillableAmount: BigNumber, takerFillableAmount: BigNumber,
|
||||
feeRecepient: string, expirationUnixTimestampSec?: BigNumber): Promise<SignedOrder> {
|
||||
|
||||
await Promise.all([
|
||||
this.increaseBalanceAndAllowanceAsync(makerTokenAddress, makerAddress, makerFillableAmount),
|
||||
this.increaseBalanceAndAllowanceAsync(takerTokenAddress, takerAddress, takerFillableAmount),
|
||||
]);
|
||||
await Promise.all([
|
||||
this.increaseBalanceAndAllowanceAsync(this.zrxTokenAddress, makerAddress, makerFee),
|
||||
this.increaseBalanceAndAllowanceAsync(this.zrxTokenAddress, takerAddress, takerFee),
|
||||
]);
|
||||
|
||||
const signedOrder = await orderFactory.createSignedOrderAsync(this.zeroEx,
|
||||
makerAddress, takerAddress, makerFee, takerFee,
|
||||
makerFillableAmount, makerTokenAddress, takerFillableAmount, takerTokenAddress,
|
||||
this.exchangeContractAddress, feeRecepient, expirationUnixTimestampSec);
|
||||
return signedOrder;
|
||||
}
|
||||
private async increaseBalanceAndAllowanceAsync(
|
||||
tokenAddress: string, address: string, amount: BigNumber): Promise<void> {
|
||||
if (amount.isZero() || address === ZeroEx.NULL_ADDRESS) {
|
||||
return; // noop
|
||||
}
|
||||
await Promise.all([
|
||||
this.increaseBalanceAsync(tokenAddress, address, amount),
|
||||
this.increaseAllowanceAsync(tokenAddress, address, amount),
|
||||
]);
|
||||
}
|
||||
private async increaseBalanceAsync(
|
||||
tokenAddress: string, address: string, amount: BigNumber): Promise<void> {
|
||||
await this.zeroEx.token.transferAsync(tokenAddress, this.coinbase, address, amount);
|
||||
}
|
||||
private async increaseAllowanceAsync(
|
||||
tokenAddress: string, address: string, amount: BigNumber): Promise<void> {
|
||||
const oldMakerAllowance = await this.zeroEx.token.getProxyAllowanceAsync(tokenAddress, address);
|
||||
const newMakerAllowance = oldMakerAllowance.plus(amount);
|
||||
await this.zeroEx.token.setProxyAllowanceAsync(
|
||||
tokenAddress, address, newMakerAllowance,
|
||||
);
|
||||
}
|
||||
}
|
42
packages/0x.js/test/utils/order_factory.ts
Normal file
42
packages/0x.js/test/utils/order_factory.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as _ from 'lodash';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {ZeroEx, SignedOrder} from '../../src';
|
||||
|
||||
export const orderFactory = {
|
||||
async createSignedOrderAsync(
|
||||
zeroEx: ZeroEx,
|
||||
maker: string,
|
||||
taker: string,
|
||||
makerFee: BigNumber,
|
||||
takerFee: BigNumber,
|
||||
makerTokenAmount: BigNumber,
|
||||
makerTokenAddress: string,
|
||||
takerTokenAmount: BigNumber,
|
||||
takerTokenAddress: string,
|
||||
exchangeContractAddress: string,
|
||||
feeRecipient: string,
|
||||
expirationUnixTimestampSec?: BigNumber): Promise<SignedOrder> {
|
||||
const defaultExpirationUnixTimestampSec = new BigNumber(2524604400); // Close to infinite
|
||||
expirationUnixTimestampSec = _.isUndefined(expirationUnixTimestampSec) ?
|
||||
defaultExpirationUnixTimestampSec :
|
||||
expirationUnixTimestampSec;
|
||||
const order = {
|
||||
maker,
|
||||
taker,
|
||||
makerFee,
|
||||
takerFee,
|
||||
makerTokenAmount,
|
||||
takerTokenAmount,
|
||||
makerTokenAddress,
|
||||
takerTokenAddress,
|
||||
salt: ZeroEx.generatePseudoRandomSalt(),
|
||||
exchangeContractAddress,
|
||||
feeRecipient,
|
||||
expirationUnixTimestampSec,
|
||||
};
|
||||
const orderHash = ZeroEx.getOrderHashHex(order);
|
||||
const ecSignature = await zeroEx.signOrderHashAsync(orderHash, maker);
|
||||
const signedOrder: SignedOrder = _.assign(order, {ecSignature});
|
||||
return signedOrder;
|
||||
},
|
||||
};
|
14
packages/0x.js/test/utils/report_callback_errors.ts
Normal file
14
packages/0x.js/test/utils/report_callback_errors.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { DoneCallback } from '../../src/types';
|
||||
|
||||
export const reportCallbackErrors = (done: DoneCallback) => {
|
||||
return (f: (...args: any[]) => void) => {
|
||||
const wrapped = (...args: any[]) => {
|
||||
try {
|
||||
f(...args);
|
||||
} catch (err) {
|
||||
done(err);
|
||||
}
|
||||
};
|
||||
return wrapped;
|
||||
};
|
||||
};
|
57
packages/0x.js/test/utils/rpc.ts
Normal file
57
packages/0x.js/test/utils/rpc.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import * as ethUtil from 'ethereumjs-util';
|
||||
import * as request from 'request-promise-native';
|
||||
import {constants} from './constants';
|
||||
|
||||
export class RPC {
|
||||
private host: string;
|
||||
private port: number;
|
||||
private id: number;
|
||||
constructor() {
|
||||
this.host = constants.RPC_HOST;
|
||||
this.port = constants.RPC_PORT;
|
||||
this.id = 0;
|
||||
}
|
||||
public async takeSnapshotAsync(): Promise<number> {
|
||||
const method = 'evm_snapshot';
|
||||
const params: any[] = [];
|
||||
const payload = this.toPayload(method, params);
|
||||
const snapshotIdHex = await this.sendAsync(payload);
|
||||
const snapshotId = ethUtil.bufferToInt(ethUtil.toBuffer(snapshotIdHex));
|
||||
return snapshotId;
|
||||
}
|
||||
public async revertSnapshotAsync(snapshotId: number): Promise<boolean> {
|
||||
const method = 'evm_revert';
|
||||
const params = [snapshotId];
|
||||
const payload = this.toPayload(method, params);
|
||||
const didRevert = await this.sendAsync(payload);
|
||||
return didRevert;
|
||||
}
|
||||
public async mineBlockAsync(): Promise<void> {
|
||||
const method = 'evm_mine';
|
||||
const params: any[] = [];
|
||||
const payload = this.toPayload(method, params);
|
||||
await this.sendAsync(payload);
|
||||
}
|
||||
private toPayload(method: string, params: any[] = []): string {
|
||||
const payload = JSON.stringify({
|
||||
id: this.id,
|
||||
method,
|
||||
params,
|
||||
});
|
||||
this.id += 1;
|
||||
return payload;
|
||||
}
|
||||
private async sendAsync(payload: string): Promise<any> {
|
||||
const opts = {
|
||||
method: 'POST',
|
||||
uri: `http://${this.host}:${this.port}`,
|
||||
body: payload,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
};
|
||||
const bodyString = await request(opts);
|
||||
const body = JSON.parse(bodyString);
|
||||
return body.result;
|
||||
}
|
||||
}
|
24
packages/0x.js/test/utils/token_utils.ts
Normal file
24
packages/0x.js/test/utils/token_utils.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as _ from 'lodash';
|
||||
import {Token, InternalZeroExError} from '../../src/types';
|
||||
|
||||
const PROTOCOL_TOKEN_SYMBOL = 'ZRX';
|
||||
|
||||
export class TokenUtils {
|
||||
private tokens: Token[];
|
||||
constructor(tokens: Token[]) {
|
||||
this.tokens = tokens;
|
||||
}
|
||||
public getProtocolTokenOrThrow(): Token {
|
||||
const zrxToken = _.find(this.tokens, {symbol: PROTOCOL_TOKEN_SYMBOL});
|
||||
if (_.isUndefined(zrxToken)) {
|
||||
throw new Error(InternalZeroExError.ZrxNotInTokenRegistry);
|
||||
}
|
||||
return zrxToken;
|
||||
}
|
||||
public getNonProtocolTokens(): Token[] {
|
||||
const nonProtocolTokens = _.filter(this.tokens, token => {
|
||||
return token.symbol !== PROTOCOL_TOKEN_SYMBOL;
|
||||
});
|
||||
return nonProtocolTokens;
|
||||
}
|
||||
}
|
31
packages/0x.js/test/utils/web3_factory.ts
Normal file
31
packages/0x.js/test/utils/web3_factory.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// HACK: web3 injects XMLHttpRequest into the global scope and ProviderEngine checks XMLHttpRequest
|
||||
// to know whether it is running in a browser or node environment. We need it to be undefined since
|
||||
// we are not running in a browser env.
|
||||
// Filed issue: https://github.com/ethereum/web3.js/issues/844
|
||||
(global as any).XMLHttpRequest = undefined;
|
||||
import ProviderEngine = require('web3-provider-engine');
|
||||
import RpcSubprovider = require('web3-provider-engine/subproviders/rpc');
|
||||
import * as Web3 from 'web3';
|
||||
import {constants} from './constants';
|
||||
import {EmptyWalletSubProvider} from '../../src/subproviders/empty_wallet_subprovider';
|
||||
|
||||
export const web3Factory = {
|
||||
create(hasAddresses: boolean = true): Web3 {
|
||||
const provider = this.getRpcProvider(hasAddresses);
|
||||
const web3 = new Web3();
|
||||
web3.setProvider(provider);
|
||||
return web3;
|
||||
},
|
||||
getRpcProvider(hasAddresses: boolean = true): Web3.Provider {
|
||||
const provider = new ProviderEngine();
|
||||
const rpcUrl = `http://${constants.RPC_HOST}:${constants.RPC_PORT}`;
|
||||
if (!hasAddresses) {
|
||||
provider.addProvider(new EmptyWalletSubProvider());
|
||||
}
|
||||
provider.addProvider(new RpcSubprovider({
|
||||
rpcUrl,
|
||||
}));
|
||||
provider.start();
|
||||
return provider;
|
||||
},
|
||||
};
|
29
packages/0x.js/test/web3_wrapper_test.ts
Normal file
29
packages/0x.js/test/web3_wrapper_test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as chai from 'chai';
|
||||
import {web3Factory} from './utils/web3_factory';
|
||||
import {ZeroEx} from '../src/';
|
||||
import {Web3Wrapper} from '../src/web3_wrapper';
|
||||
import {constants} from './utils/constants';
|
||||
|
||||
chai.config.includeStack = true;
|
||||
const expect = chai.expect;
|
||||
|
||||
describe('Web3Wrapper', () => {
|
||||
const web3Provider = web3Factory.create().currentProvider;
|
||||
describe('#getNetworkIdIfExistsAsync', () => {
|
||||
it('caches network id requests', async () => {
|
||||
const web3Wrapper = (new ZeroEx(web3Provider) as any)._web3Wrapper as Web3Wrapper;
|
||||
expect((web3Wrapper as any).networkIdIfExists).to.be.undefined();
|
||||
const networkIdIfExists = await web3Wrapper.getNetworkIdIfExistsAsync();
|
||||
expect((web3Wrapper as any).networkIdIfExists).to.be.equal(constants.TESTRPC_NETWORK_ID);
|
||||
});
|
||||
it('invalidates network id cache on setProvider call', async () => {
|
||||
const web3Wrapper = (new ZeroEx(web3Provider) as any)._web3Wrapper as Web3Wrapper;
|
||||
expect((web3Wrapper as any).networkIdIfExists).to.be.undefined();
|
||||
const networkIdIfExists = await web3Wrapper.getNetworkIdIfExistsAsync();
|
||||
expect((web3Wrapper as any).networkIdIfExists).to.be.equal(constants.TESTRPC_NETWORK_ID);
|
||||
const newProvider = web3Factory.create().currentProvider;
|
||||
web3Wrapper.setProvider(newProvider);
|
||||
expect((web3Wrapper as any).networkIdIfExists).to.be.undefined();
|
||||
});
|
||||
});
|
||||
});
|
22
packages/0x.js/tsconfig.json
Normal file
22
packages/0x.js/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [ "es2015", "dom" ],
|
||||
"outDir": "lib",
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"noImplicitAny": true,
|
||||
"experimentalDecorators": true,
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*",
|
||||
"./test/**/*",
|
||||
"./node_modules/types-bn/index.d.ts",
|
||||
"./node_modules/types-ethereumjs-util/index.d.ts",
|
||||
"./node_modules/web3-typescript-typings/index.d.ts",
|
||||
"./node_modules/chai-typescript-typings/index.d.ts",
|
||||
"./node_modules/chai-as-promised-typescript-typings/index.d.ts"
|
||||
]
|
||||
}
|
56
packages/0x.js/webpack.config.js
Normal file
56
packages/0x.js/webpack.config.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* This is to generate the umd bundle only
|
||||
*/
|
||||
const _ = require('lodash');
|
||||
const webpack = require('webpack');
|
||||
const path = require('path');
|
||||
const production = process.env.NODE_ENV === 'production';
|
||||
|
||||
let entry = {
|
||||
'index': './src/index.ts',
|
||||
};
|
||||
if (production) {
|
||||
entry = _.assign({}, entry, {'index.min': './src/index.ts'});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
entry,
|
||||
output: {
|
||||
path: path.resolve(__dirname, '_bundles'),
|
||||
filename: '[name].js',
|
||||
libraryTarget: 'umd',
|
||||
library: 'ZeroEx',
|
||||
umdNamedDefine: true,
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js', '.json'],
|
||||
},
|
||||
devtool: 'source-map',
|
||||
plugins: [
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
minimize: true,
|
||||
sourceMap: true,
|
||||
include: /\.min\.js$/,
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.ts$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'awesome-typescript-loader',
|
||||
query: {
|
||||
declaration: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.json$/,
|
||||
loader: 'json-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
Reference in New Issue
Block a user