Merge branch 'development' of https://github.com/0xProject/0x-monorepo into feature/sra/add-sra-package
This commit is contained in:
@@ -82,10 +82,12 @@ We strongly recommend that the community help us make improvements and determine
|
||||
|
||||
### Install dependencies
|
||||
|
||||
If you don't have yarn workspaces enabled (Yarn < v1.0) - enable them:
|
||||
Make sure you are using Yarn v1.6. To install using brew:
|
||||
|
||||
```bash
|
||||
yarn config set workspaces-experimental true
|
||||
```
|
||||
brew unlink yarn
|
||||
brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/76215230de5f7f7bee2cfcdd7185cf49d949862d/Formula/yarn.rb
|
||||
brew switch yarn 1.6.0_1
|
||||
```
|
||||
|
||||
Then install dependencies
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
[
|
||||
{
|
||||
"version": "1.0.1-rc.3",
|
||||
"changes": [
|
||||
{
|
||||
"note": "Add ForwarderWrapper",
|
||||
"pr": 934
|
||||
},
|
||||
{
|
||||
"note": "Optimize orders in ForwarderWrapper",
|
||||
"pr": 936
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.0.1-rc.2",
|
||||
"changes": [
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"watch_without_deps": "yarn pre_build && tsc -w",
|
||||
"build": "yarn pre_build && tsc && copyfiles -u 3 './lib/src/monorepo_scripts/**/*' ./scripts",
|
||||
"pre_build": "run-s update_artifacts_v2_beta update_artifacts_v2 generate_contract_wrappers copy_artifacts",
|
||||
"generate_contract_wrappers": "abi-gen --abis 'src/artifacts/@(Exchange|DummyERC20Token|DummyERC721Token|ZRXToken|ERC20Token|ERC721Token|WETH9|ERC20Proxy|ERC721Proxy).json' --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/contract_wrappers/generated --backend ethers",
|
||||
"generate_contract_wrappers": "abi-gen --abis 'src/artifacts/@(Exchange|DummyERC20Token|DummyERC721Token|ZRXToken|ERC20Token|ERC721Token|WETH9|ERC20Proxy|ERC721Proxy|Forwarder).json' --template ../contract_templates/contract.handlebars --partials '../contract_templates/partials/**/*.handlebars' --output src/contract_wrappers/generated --backend ethers",
|
||||
"lint": "tslint --project . --exclude **/src/contract_wrappers/**/* --exclude **/lib/**/*",
|
||||
"test:circleci": "run-s test:coverage",
|
||||
"test": "yarn run_mocha",
|
||||
@@ -29,7 +29,7 @@
|
||||
"manual:postpublish": "yarn build; node ./scripts/postpublish.js"
|
||||
},
|
||||
"config": {
|
||||
"contracts_v2_beta": "Exchange ERC20Proxy ERC20Token ERC721Proxy ERC721Token WETH9 ZRXToken",
|
||||
"contracts_v2_beta": "Exchange ERC20Proxy ERC20Token ERC721Proxy ERC721Token WETH9 ZRXToken Forwarder",
|
||||
"contracts_v2": "DummyERC20Token DummyERC721Token"
|
||||
},
|
||||
"repository": {
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as ERC20Token from './artifacts/ERC20Token.json';
|
||||
import * as ERC721Proxy from './artifacts/ERC721Proxy.json';
|
||||
import * as ERC721Token from './artifacts/ERC721Token.json';
|
||||
import * as Exchange from './artifacts/Exchange.json';
|
||||
import * as Forwarder from './artifacts/Forwarder.json';
|
||||
import * as EtherToken from './artifacts/WETH9.json';
|
||||
import * as ZRXToken from './artifacts/ZRXToken.json';
|
||||
|
||||
@@ -20,4 +21,5 @@ export const artifacts = {
|
||||
EtherToken: (EtherToken as any) as ContractArtifact,
|
||||
ERC20Proxy: (ERC20Proxy as any) as ContractArtifact,
|
||||
ERC721Proxy: (ERC721Proxy as any) as ContractArtifact,
|
||||
Forwarder: (Forwarder as any) as ContractArtifact,
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ERC721ProxyWrapper } from './contract_wrappers/erc721_proxy_wrapper';
|
||||
import { ERC721TokenWrapper } from './contract_wrappers/erc721_token_wrapper';
|
||||
import { EtherTokenWrapper } from './contract_wrappers/ether_token_wrapper';
|
||||
import { ExchangeWrapper } from './contract_wrappers/exchange_wrapper';
|
||||
import { ForwarderWrapper } from './contract_wrappers/forwarder_wrapper';
|
||||
import { ContractWrappersConfigSchema } from './schemas/contract_wrappers_config_schema';
|
||||
import { contractWrappersPrivateNetworkConfigSchema } from './schemas/contract_wrappers_private_network_config_schema';
|
||||
import { contractWrappersPublicNetworkConfigSchema } from './schemas/contract_wrappers_public_network_config_schema';
|
||||
@@ -47,6 +48,11 @@ export class ContractWrappers {
|
||||
* erc721Proxy smart contract.
|
||||
*/
|
||||
public erc721Proxy: ERC721ProxyWrapper;
|
||||
/**
|
||||
* An instance of the ForwarderWrapper class containing methods for interacting with any Forwarder smart contract.
|
||||
*/
|
||||
public forwarder: ForwarderWrapper;
|
||||
|
||||
private _web3Wrapper: Web3Wrapper;
|
||||
/**
|
||||
* Instantiates a new ContractWrappers instance.
|
||||
@@ -104,6 +110,12 @@ export class ContractWrappers {
|
||||
config.zrxContractAddress,
|
||||
blockPollingIntervalMs,
|
||||
);
|
||||
this.forwarder = new ForwarderWrapper(
|
||||
this._web3Wrapper,
|
||||
config.networkId,
|
||||
config.forwarderContractAddress,
|
||||
config.zrxContractAddress,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Sets a new web3 provider for 0x.js. Updating the provider will stop all
|
||||
|
||||
@@ -869,15 +869,35 @@ export class ExchangeWrapper extends ContractWrapper {
|
||||
*/
|
||||
@decorators.asyncZeroExErrorHandler
|
||||
public async getOrderInfoAsync(order: Order | SignedOrder, methodOpts: MethodOpts = {}): Promise<OrderInfo> {
|
||||
assert.doesConformToSchema('order', order, schemas.orderSchema);
|
||||
if (!_.isUndefined(methodOpts)) {
|
||||
assert.doesConformToSchema('methodOpts', methodOpts, methodOptsSchema);
|
||||
}
|
||||
const exchangeInstance = await this._getExchangeContractAsync();
|
||||
|
||||
const txData = {};
|
||||
const orderInfo = await exchangeInstance.getOrderInfo.callAsync(order, txData, methodOpts.defaultBlock);
|
||||
return orderInfo;
|
||||
}
|
||||
/**
|
||||
* Get order info for multiple orders
|
||||
* @param orders Orders
|
||||
* @param methodOpts Optional arguments this method accepts.
|
||||
* @returns Array of Order infos
|
||||
*/
|
||||
@decorators.asyncZeroExErrorHandler
|
||||
public async getOrdersInfoAsync(
|
||||
orders: Array<Order | SignedOrder>,
|
||||
methodOpts: MethodOpts = {},
|
||||
): Promise<OrderInfo[]> {
|
||||
assert.doesConformToSchema('orders', orders, schemas.ordersSchema);
|
||||
if (!_.isUndefined(methodOpts)) {
|
||||
assert.doesConformToSchema('methodOpts', methodOpts, methodOptsSchema);
|
||||
}
|
||||
const exchangeInstance = await this._getExchangeContractAsync();
|
||||
const txData = {};
|
||||
const ordersInfo = await exchangeInstance.getOrdersInfo.callAsync(orders, txData, methodOpts.defaultBlock);
|
||||
return ordersInfo;
|
||||
}
|
||||
/**
|
||||
* Cancel a given order.
|
||||
* @param order An object that conforms to the Order or SignedOrder interface. The order you would like to cancel.
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
import { schemas } from '@0xproject/json-schemas';
|
||||
import { AssetProxyId, SignedOrder } from '@0xproject/types';
|
||||
import { BigNumber } from '@0xproject/utils';
|
||||
import { Web3Wrapper } from '@0xproject/web3-wrapper';
|
||||
import { ContractAbi } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { artifacts } from '../artifacts';
|
||||
import { orderTxOptsSchema } from '../schemas/order_tx_opts_schema';
|
||||
import { txOptsSchema } from '../schemas/tx_opts_schema';
|
||||
import { TransactionOpts } from '../types';
|
||||
import { assert } from '../utils/assert';
|
||||
import { calldataOptimizationUtils } from '../utils/calldata_optimization_utils';
|
||||
import { constants } from '../utils/constants';
|
||||
|
||||
import { ContractWrapper } from './contract_wrapper';
|
||||
import { ForwarderContract } from './generated/forwarder';
|
||||
|
||||
/**
|
||||
* This class includes the functionality related to interacting with the Forwarder contract.
|
||||
*/
|
||||
export class ForwarderWrapper extends ContractWrapper {
|
||||
public abi: ContractAbi = artifacts.Forwarder.compilerOutput.abi;
|
||||
private _forwarderContractIfExists?: ForwarderContract;
|
||||
private _contractAddressIfExists?: string;
|
||||
private _zrxContractAddressIfExists?: string;
|
||||
constructor(
|
||||
web3Wrapper: Web3Wrapper,
|
||||
networkId: number,
|
||||
contractAddressIfExists?: string,
|
||||
zrxContractAddressIfExists?: string,
|
||||
) {
|
||||
super(web3Wrapper, networkId);
|
||||
this._contractAddressIfExists = contractAddressIfExists;
|
||||
this._zrxContractAddressIfExists = zrxContractAddressIfExists;
|
||||
}
|
||||
/**
|
||||
* Purchases as much of orders' makerAssets as possible by selling up to 95% of transaction's ETH value.
|
||||
* Any ZRX required to pay fees for primary orders will automatically be purchased by this contract.
|
||||
* 5% of ETH value is reserved for paying fees to order feeRecipients (in ZRX) and forwarding contract feeRecipient (in ETH).
|
||||
* Any ETH not spent will be refunded to sender.
|
||||
* @param signedOrders An array of objects that conform to the SignedOrder interface. All orders must specify the same makerAsset.
|
||||
* All orders must specify WETH as the takerAsset
|
||||
* @param takerAddress The user Ethereum address who would like to fill this order. Must be available via the supplied
|
||||
* Provider provided at instantiation.
|
||||
* @param ethAmount The amount of eth to send with the transaction (in wei).
|
||||
* @param signedFeeOrders An array of objects that conform to the SignedOrder interface. All orders must specify ZRX as makerAsset and WETH as takerAsset.
|
||||
* Used to purchase ZRX for primary order fees.
|
||||
* @param feePercentage The percentage of WETH sold that will payed as fee to forwarding contract feeRecipient.
|
||||
* Defaults to 0.
|
||||
* @param feeRecipientAddress The address that will receive ETH when signedFeeOrders are filled.
|
||||
* @param txOpts Transaction parameters.
|
||||
* @return Transaction hash.
|
||||
*/
|
||||
public async marketSellOrdersWithEthAsync(
|
||||
signedOrders: SignedOrder[],
|
||||
takerAddress: string,
|
||||
ethAmount: BigNumber,
|
||||
signedFeeOrders: SignedOrder[] = [],
|
||||
feePercentage: BigNumber = constants.ZERO_AMOUNT,
|
||||
feeRecipientAddress: string = constants.NULL_ADDRESS,
|
||||
txOpts: TransactionOpts = {},
|
||||
): Promise<string> {
|
||||
// type assertions
|
||||
assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
|
||||
await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
|
||||
assert.isBigNumber('ethAmount', ethAmount);
|
||||
assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema);
|
||||
assert.isBigNumber('feePercentage', feePercentage);
|
||||
assert.isETHAddressHex('feeRecipientAddress', feeRecipientAddress);
|
||||
assert.doesConformToSchema('txOpts', txOpts, txOptsSchema);
|
||||
// other assertions
|
||||
assert.ordersCanBeUsedForForwarderContract(signedOrders, this.getEtherTokenAddress());
|
||||
assert.feeOrdersCanBeUsedForForwarderContract(
|
||||
signedFeeOrders,
|
||||
this.getZRXTokenAddress(),
|
||||
this.getEtherTokenAddress(),
|
||||
);
|
||||
// lowercase input addresses
|
||||
const normalizedTakerAddress = takerAddress.toLowerCase();
|
||||
const normalizedFeeRecipientAddress = feeRecipientAddress.toLowerCase();
|
||||
// optimize orders
|
||||
const optimizedMarketOrders = calldataOptimizationUtils.optimizeForwarderOrders(signedOrders);
|
||||
const optimizedFeeOrders = calldataOptimizationUtils.optimizeForwarderFeeOrders(signedFeeOrders);
|
||||
// send transaction
|
||||
const forwarderContractInstance = await this._getForwarderContractAsync();
|
||||
const txHash = await forwarderContractInstance.marketSellOrdersWithEth.sendTransactionAsync(
|
||||
optimizedMarketOrders,
|
||||
_.map(optimizedMarketOrders, order => order.signature),
|
||||
optimizedFeeOrders,
|
||||
_.map(optimizedFeeOrders, order => order.signature),
|
||||
feePercentage,
|
||||
feeRecipientAddress,
|
||||
{
|
||||
value: ethAmount,
|
||||
from: normalizedTakerAddress,
|
||||
gas: txOpts.gasLimit,
|
||||
gasPrice: txOpts.gasPrice,
|
||||
},
|
||||
);
|
||||
return txHash;
|
||||
}
|
||||
/**
|
||||
* Attempt to purchase makerAssetFillAmount of makerAsset by selling ethAmount provided with transaction.
|
||||
* Any ZRX required to pay fees for primary orders will automatically be purchased by the contract.
|
||||
* Any ETH not spent will be refunded to sender.
|
||||
* @param signedOrders An array of objects that conform to the SignedOrder interface. All orders must specify the same makerAsset.
|
||||
* All orders must specify WETH as the takerAsset
|
||||
* @param makerAssetFillAmount The amount of the order (in taker asset baseUnits) that you wish to fill.
|
||||
* @param takerAddress The user Ethereum address who would like to fill this order. Must be available via the supplied
|
||||
* Provider provided at instantiation.
|
||||
* @param ethAmount The amount of eth to send with the transaction (in wei).
|
||||
* @param signedFeeOrders An array of objects that conform to the SignedOrder interface. All orders must specify ZRX as makerAsset and WETH as takerAsset.
|
||||
* Used to purchase ZRX for primary order fees.
|
||||
* @param feePercentage The percentage of WETH sold that will payed as fee to forwarding contract feeRecipient.
|
||||
* Defaults to 0.
|
||||
* @param feeRecipientAddress The address that will receive ETH when signedFeeOrders are filled.
|
||||
* @param txOpts Transaction parameters.
|
||||
* @return Transaction hash.
|
||||
*/
|
||||
public async marketBuyOrdersWithEthAsync(
|
||||
signedOrders: SignedOrder[],
|
||||
makerAssetFillAmount: BigNumber,
|
||||
takerAddress: string,
|
||||
ethAmount: BigNumber,
|
||||
signedFeeOrders: SignedOrder[] = [],
|
||||
feePercentage: BigNumber = constants.ZERO_AMOUNT,
|
||||
feeRecipientAddress: string = constants.NULL_ADDRESS,
|
||||
txOpts: TransactionOpts = {},
|
||||
): Promise<string> {
|
||||
// type assertions
|
||||
assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
|
||||
assert.isBigNumber('makerAssetFillAmount', makerAssetFillAmount);
|
||||
await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper);
|
||||
assert.isBigNumber('ethAmount', ethAmount);
|
||||
assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema);
|
||||
assert.isBigNumber('feePercentage', feePercentage);
|
||||
assert.isETHAddressHex('feeRecipientAddress', feeRecipientAddress);
|
||||
assert.doesConformToSchema('txOpts', txOpts, txOptsSchema);
|
||||
// other assertions
|
||||
assert.ordersCanBeUsedForForwarderContract(signedOrders, this.getEtherTokenAddress());
|
||||
assert.feeOrdersCanBeUsedForForwarderContract(
|
||||
signedFeeOrders,
|
||||
this.getZRXTokenAddress(),
|
||||
this.getEtherTokenAddress(),
|
||||
);
|
||||
// lowercase input addresses
|
||||
const normalizedTakerAddress = takerAddress.toLowerCase();
|
||||
const normalizedFeeRecipientAddress = feeRecipientAddress.toLowerCase();
|
||||
// optimize orders
|
||||
const optimizedMarketOrders = calldataOptimizationUtils.optimizeForwarderOrders(signedOrders);
|
||||
const optimizedFeeOrders = calldataOptimizationUtils.optimizeForwarderFeeOrders(signedFeeOrders);
|
||||
// send transaction
|
||||
const forwarderContractInstance = await this._getForwarderContractAsync();
|
||||
const txHash = await forwarderContractInstance.marketBuyOrdersWithEth.sendTransactionAsync(
|
||||
optimizedMarketOrders,
|
||||
makerAssetFillAmount,
|
||||
_.map(optimizedMarketOrders, order => order.signature),
|
||||
optimizedFeeOrders,
|
||||
_.map(optimizedFeeOrders, order => order.signature),
|
||||
feePercentage,
|
||||
feeRecipientAddress,
|
||||
{
|
||||
value: ethAmount,
|
||||
from: normalizedTakerAddress,
|
||||
gas: txOpts.gasLimit,
|
||||
gasPrice: txOpts.gasPrice,
|
||||
},
|
||||
);
|
||||
return txHash;
|
||||
}
|
||||
/**
|
||||
* Retrieves the Ethereum address of the Forwarder contract deployed on the network
|
||||
* that the user-passed web3 provider is connected to.
|
||||
* @returns The Ethereum address of the Forwarder contract being used.
|
||||
*/
|
||||
public getContractAddress(): string {
|
||||
const contractAddress = this._getContractAddress(artifacts.Forwarder, this._contractAddressIfExists);
|
||||
return contractAddress;
|
||||
}
|
||||
/**
|
||||
* Returns the ZRX token address used by the forwarder contract.
|
||||
* @return Address of ZRX token
|
||||
*/
|
||||
public getZRXTokenAddress(): string {
|
||||
const contractAddress = this._getContractAddress(artifacts.ZRXToken, this._zrxContractAddressIfExists);
|
||||
return contractAddress;
|
||||
}
|
||||
/**
|
||||
* Returns the Ether token address used by the forwarder contract.
|
||||
* @return Address of Ether token
|
||||
*/
|
||||
public getEtherTokenAddress(): string {
|
||||
const contractAddress = this._getContractAddress(artifacts.EtherToken);
|
||||
return contractAddress;
|
||||
}
|
||||
// HACK: We don't want this method to be visible to the other units within that package but not to the end user.
|
||||
// TS doesn't give that possibility and therefore we make it private and access it over an any cast. Because of that tslint sees it as unused.
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
private _invalidateContractInstance(): void {
|
||||
delete this._forwarderContractIfExists;
|
||||
}
|
||||
private async _getForwarderContractAsync(): Promise<ForwarderContract> {
|
||||
if (!_.isUndefined(this._forwarderContractIfExists)) {
|
||||
return this._forwarderContractIfExists;
|
||||
}
|
||||
const [abi, address] = await this._getContractAbiAndAddressFromArtifactsAsync(
|
||||
artifacts.Forwarder,
|
||||
this._contractAddressIfExists,
|
||||
);
|
||||
const contractInstance = new ForwarderContract(
|
||||
abi,
|
||||
address,
|
||||
this._web3Wrapper.getProvider(),
|
||||
this._web3Wrapper.getContractDefaults(),
|
||||
);
|
||||
this._forwarderContractIfExists = contractInstance;
|
||||
return this._forwarderContractIfExists;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ export { EtherTokenWrapper } from './contract_wrappers/ether_token_wrapper';
|
||||
export { ExchangeWrapper } from './contract_wrappers/exchange_wrapper';
|
||||
export { ERC20ProxyWrapper } from './contract_wrappers/erc20_proxy_wrapper';
|
||||
export { ERC721ProxyWrapper } from './contract_wrappers/erc721_proxy_wrapper';
|
||||
export { ForwarderWrapper } from './contract_wrappers/forwarder_wrapper';
|
||||
|
||||
export {
|
||||
ContractWrappersError,
|
||||
|
||||
@@ -109,6 +109,7 @@ export type SyncMethod = (...args: any[]) => any;
|
||||
* zrxContractAddress: The address of the ZRX contract to use
|
||||
* erc20ProxyContractAddress: The address of the erc20 token transfer proxy contract to use
|
||||
* erc721ProxyContractAddress: The address of the erc721 token transfer proxy contract to use
|
||||
* forwarderContractAddress: The address of the forwarder contract to use
|
||||
* orderWatcherConfig: All the configs related to the orderWatcher
|
||||
* blockPollingIntervalMs: The interval to use for block polling in event watching methods (defaults to 1000)
|
||||
*/
|
||||
@@ -119,6 +120,7 @@ export interface ContractWrappersConfig {
|
||||
zrxContractAddress?: string;
|
||||
erc20ProxyContractAddress?: string;
|
||||
erc721ProxyContractAddress?: string;
|
||||
forwarderContractAddress?: string;
|
||||
blockPollingIntervalMs?: number;
|
||||
}
|
||||
|
||||
@@ -172,13 +174,13 @@ export enum TransferType {
|
||||
export type OnOrderStateChangeCallback = (err: Error | null, orderState?: OrderState) => void;
|
||||
|
||||
export interface OrderInfo {
|
||||
orderStatus: number;
|
||||
orderStatus: OrderStatus;
|
||||
orderHash: string;
|
||||
orderTakerAssetFilledAmount: BigNumber;
|
||||
}
|
||||
|
||||
export enum OrderStatus {
|
||||
INVALID,
|
||||
INVALID = 0,
|
||||
INVALID_MAKER_ASSET_AMOUNT,
|
||||
INVALID_TAKER_ASSET_AMOUNT,
|
||||
FILLABLE,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { assert as sharedAssert } from '@0xproject/assert';
|
||||
// HACK: We need those two unused imports because they're actually used by sharedAssert which gets injected here
|
||||
import { Schema } from '@0xproject/json-schemas'; // tslint:disable-line:no-unused-variable
|
||||
import { isValidSignatureAsync } from '@0xproject/order-utils';
|
||||
import { ECSignature } from '@0xproject/types'; // tslint:disable-line:no-unused-variable
|
||||
import { assetDataUtils, isValidSignatureAsync } from '@0xproject/order-utils';
|
||||
import { ECSignature, Order } from '@0xproject/types'; // tslint:disable-line:no-unused-variable
|
||||
import { BigNumber } from '@0xproject/utils'; // tslint:disable-line:no-unused-variable
|
||||
import { Web3Wrapper } from '@0xproject/web3-wrapper';
|
||||
import { Provider } from 'ethereum-types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { constants } from './constants';
|
||||
|
||||
export const assert = {
|
||||
...sharedAssert,
|
||||
@@ -16,12 +19,12 @@ export const assert = {
|
||||
signerAddress: string,
|
||||
): Promise<void> {
|
||||
const isValid = await isValidSignatureAsync(provider, orderHash, signature, signerAddress);
|
||||
this.assert(isValid, `Expected order with hash '${orderHash}' to have a valid signature`);
|
||||
sharedAssert.assert(isValid, `Expected order with hash '${orderHash}' to have a valid signature`);
|
||||
},
|
||||
isValidSubscriptionToken(variableName: string, subscriptionToken: string): void {
|
||||
const uuidRegex = new RegExp('^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$');
|
||||
const isValid = uuidRegex.test(subscriptionToken);
|
||||
this.assert(isValid, `Expected ${variableName} to be a valid subscription token`);
|
||||
sharedAssert.assert(isValid, `Expected ${variableName} to be a valid subscription token`);
|
||||
},
|
||||
async isSenderAddressAsync(
|
||||
variableName: string,
|
||||
@@ -35,4 +38,53 @@ export const assert = {
|
||||
`Specified ${variableName} ${senderAddressHex} isn't available through the supplied web3 provider`,
|
||||
);
|
||||
},
|
||||
ordersCanBeUsedForForwarderContract(orders: Order[], etherTokenAddress: string): void {
|
||||
sharedAssert.assert(!_.isEmpty(orders), 'Expected at least 1 signed order. Found no orders');
|
||||
assert.ordersHaveAtMostOneUniqueValueForProperty(orders, 'makerAssetData');
|
||||
assert.allTakerAssetDatasAreErc20Token(orders, etherTokenAddress);
|
||||
assert.allTakerAddressesAreNull(orders);
|
||||
},
|
||||
feeOrdersCanBeUsedForForwarderContract(orders: Order[], zrxTokenAddress: string, etherTokenAddress: string): void {
|
||||
if (!_.isEmpty(orders)) {
|
||||
assert.allMakerAssetDatasAreErc20Token(orders, zrxTokenAddress);
|
||||
assert.allTakerAssetDatasAreErc20Token(orders, etherTokenAddress);
|
||||
}
|
||||
},
|
||||
allTakerAddressesAreNull(orders: Order[]): void {
|
||||
assert.ordersHaveAtMostOneUniqueValueForProperty(orders, 'takerAddress', constants.NULL_ADDRESS);
|
||||
},
|
||||
allMakerAssetDatasAreErc20Token(orders: Order[], tokenAddress: string): void {
|
||||
assert.ordersHaveAtMostOneUniqueValueForProperty(
|
||||
orders,
|
||||
'makerAssetData',
|
||||
assetDataUtils.encodeERC20AssetData(tokenAddress),
|
||||
);
|
||||
},
|
||||
allTakerAssetDatasAreErc20Token(orders: Order[], tokenAddress: string): void {
|
||||
assert.ordersHaveAtMostOneUniqueValueForProperty(
|
||||
orders,
|
||||
'takerAssetData',
|
||||
assetDataUtils.encodeERC20AssetData(tokenAddress),
|
||||
);
|
||||
},
|
||||
/*
|
||||
* Asserts that all the orders have the same value for the provided propertyName
|
||||
* If the value parameter is provided, this asserts that all orders have the prope
|
||||
*/
|
||||
ordersHaveAtMostOneUniqueValueForProperty(orders: Order[], propertyName: string, value?: any): void {
|
||||
const allValues = _.map(orders, order => _.get(order, propertyName));
|
||||
sharedAssert.hasAtMostOneUniqueValue(
|
||||
allValues,
|
||||
`Expected all orders to have the same ${propertyName} field. Found the following ${propertyName} values: ${JSON.stringify(
|
||||
allValues,
|
||||
)}`,
|
||||
);
|
||||
if (!_.isUndefined(value)) {
|
||||
const firstValue = _.head(allValues);
|
||||
sharedAssert.assert(
|
||||
firstValue === value,
|
||||
`Expected all orders to have a ${propertyName} field with value: ${value}. Found: ${firstValue}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { SignedOrder } from '@0xproject/types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { constants } from './constants';
|
||||
|
||||
export const calldataOptimizationUtils = {
|
||||
/**
|
||||
* Takes an array of orders and outputs an array of equivalent orders where all takerAssetData are '0x' and
|
||||
* all makerAssetData are '0x' except for that of the first order, which retains its original value
|
||||
* @param orders An array of SignedOrder objects
|
||||
* @returns optimized orders
|
||||
*/
|
||||
optimizeForwarderOrders(orders: SignedOrder[]): SignedOrder[] {
|
||||
const optimizedOrders = _.map(orders, (order, index) =>
|
||||
transformOrder(order, {
|
||||
makerAssetData: index === 0 ? order.makerAssetData : constants.NULL_BYTES,
|
||||
takerAssetData: constants.NULL_BYTES,
|
||||
}),
|
||||
);
|
||||
return optimizedOrders;
|
||||
},
|
||||
/**
|
||||
* Takes an array of orders and outputs an array of equivalent orders where all takerAssetData are '0x' and
|
||||
* all makerAssetData are '0x'
|
||||
* @param orders An array of SignedOrder objects
|
||||
* @returns optimized orders
|
||||
*/
|
||||
optimizeForwarderFeeOrders(orders: SignedOrder[]): SignedOrder[] {
|
||||
const optimizedOrders = _.map(orders, (order, index) =>
|
||||
transformOrder(order, {
|
||||
makerAssetData: constants.NULL_BYTES,
|
||||
takerAssetData: constants.NULL_BYTES,
|
||||
}),
|
||||
);
|
||||
return optimizedOrders;
|
||||
},
|
||||
};
|
||||
|
||||
const transformOrder = (order: SignedOrder, partialOrder: Partial<SignedOrder>) => {
|
||||
return {
|
||||
...order,
|
||||
...partialOrder,
|
||||
};
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { BigNumber } from '@0xproject/utils';
|
||||
|
||||
export const constants = {
|
||||
NULL_ADDRESS: '0x0000000000000000000000000000000000000000',
|
||||
NULL_BYTES: '0x',
|
||||
TESTRPC_NETWORK_ID: 50,
|
||||
INVALID_JUMP_PATTERN: 'invalid JUMP at',
|
||||
REVERT: 'revert',
|
||||
@@ -10,4 +11,5 @@ export const constants = {
|
||||
// tslint:disable-next-line:custom-no-magic-numbers
|
||||
UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1),
|
||||
DEFAULT_BLOCK_POLLING_INTERVAL: 1000,
|
||||
ZERO_AMOUNT: new BigNumber(0),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { orderFactory } from '@0xproject/order-utils';
|
||||
import * as chai from 'chai';
|
||||
import * as _ from 'lodash';
|
||||
import 'mocha';
|
||||
|
||||
import { assert } from '../src/utils/assert';
|
||||
import { calldataOptimizationUtils } from '../src/utils/calldata_optimization_utils';
|
||||
import { constants } from '../src/utils/constants';
|
||||
|
||||
import { chaiSetup } from './utils/chai_setup';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
// utility for generating a set of order objects with mostly NULL values
|
||||
// except for a specified makerAssetData and takerAssetData
|
||||
const FAKE_ORDERS_COUNT = 5;
|
||||
const generateFakeOrders = (makerAssetData: string, takerAssetData: string) =>
|
||||
_.map(_.range(FAKE_ORDERS_COUNT), index => {
|
||||
const order = orderFactory.createOrder(
|
||||
constants.NULL_ADDRESS,
|
||||
constants.ZERO_AMOUNT,
|
||||
makerAssetData,
|
||||
constants.ZERO_AMOUNT,
|
||||
takerAssetData,
|
||||
constants.NULL_ADDRESS,
|
||||
);
|
||||
return {
|
||||
...order,
|
||||
signature: 'dummy signature',
|
||||
};
|
||||
});
|
||||
|
||||
describe('calldataOptimizationUtils', () => {
|
||||
const fakeMakerAssetData = 'fakeMakerAssetData';
|
||||
const fakeTakerAssetData = 'fakeTakerAssetData';
|
||||
const orders = generateFakeOrders(fakeMakerAssetData, fakeTakerAssetData);
|
||||
describe('#optimizeForwarderOrders', () => {
|
||||
it('should make makerAssetData `0x` unless first order', () => {
|
||||
const optimizedOrders = calldataOptimizationUtils.optimizeForwarderOrders(orders);
|
||||
expect(optimizedOrders[0].makerAssetData).to.equal(fakeMakerAssetData);
|
||||
const ordersWithoutHead = _.slice(optimizedOrders, 1);
|
||||
_.forEach(ordersWithoutHead, order => expect(order.makerAssetData).to.equal(constants.NULL_BYTES));
|
||||
});
|
||||
it('should make all takerAssetData `0x`', () => {
|
||||
const optimizedOrders = calldataOptimizationUtils.optimizeForwarderOrders(orders);
|
||||
_.forEach(optimizedOrders, order => expect(order.takerAssetData).to.equal(constants.NULL_BYTES));
|
||||
});
|
||||
});
|
||||
describe('#optimizeForwarderFeeOrders', () => {
|
||||
it('should make all makerAssetData `0x`', () => {
|
||||
const optimizedOrders = calldataOptimizationUtils.optimizeForwarderFeeOrders(orders);
|
||||
_.forEach(optimizedOrders, order => expect(order.makerAssetData).to.equal(constants.NULL_BYTES));
|
||||
});
|
||||
it('should make all takerAssetData `0x`', () => {
|
||||
const optimizedOrders = calldataOptimizationUtils.optimizeForwarderFeeOrders(orders);
|
||||
_.forEach(optimizedOrders, order => expect(order.takerAssetData).to.equal(constants.NULL_BYTES));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -277,6 +277,15 @@ describe('ExchangeWrapper', () => {
|
||||
expect(orderInfo.orderHash).to.be.equal(orderHash);
|
||||
});
|
||||
});
|
||||
describe('#getOrdersInfoAsync', () => {
|
||||
it('should get the orders info', async () => {
|
||||
const ordersInfo = await contractWrappers.exchange.getOrdersInfoAsync([signedOrder, anotherSignedOrder]);
|
||||
const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
|
||||
expect(ordersInfo[0].orderHash).to.be.equal(orderHash);
|
||||
const anotherOrderHash = orderHashUtils.getOrderHashHex(anotherSignedOrder);
|
||||
expect(ordersInfo[1].orderHash).to.be.equal(anotherOrderHash);
|
||||
});
|
||||
});
|
||||
describe('#isValidSignature', () => {
|
||||
it('should check if the signature is valid', async () => {
|
||||
const orderHash = orderHashUtils.getOrderHashHex(signedOrder);
|
||||
@@ -295,7 +304,7 @@ describe('ExchangeWrapper', () => {
|
||||
});
|
||||
});
|
||||
describe('#isAllowedValidatorAsync', () => {
|
||||
it('should check if the validator is alllowed', async () => {
|
||||
it('should check if the validator is allowed', async () => {
|
||||
const signerAddress = makerAddress;
|
||||
const validatorAddress = constants.NULL_ADDRESS;
|
||||
const isAllowed = await contractWrappers.exchange.isAllowedValidatorAsync(signerAddress, validatorAddress);
|
||||
|
||||
130
packages/contract-wrappers/test/forwarder_wrapper_test.ts
Normal file
130
packages/contract-wrappers/test/forwarder_wrapper_test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { BlockchainLifecycle, callbackErrorReporter } from '@0xproject/dev-utils';
|
||||
import { FillScenarios } from '@0xproject/fill-scenarios';
|
||||
import { assetDataUtils, orderHashUtils } from '@0xproject/order-utils';
|
||||
import { DoneCallback, SignedOrder } from '@0xproject/types';
|
||||
import { BigNumber } from '@0xproject/utils';
|
||||
import * as chai from 'chai';
|
||||
import { BlockParamLiteral } from 'ethereum-types';
|
||||
import 'mocha';
|
||||
|
||||
import {
|
||||
ContractWrappers,
|
||||
DecodedLogEvent,
|
||||
ExchangeCancelEventArgs,
|
||||
ExchangeEvents,
|
||||
ExchangeFillEventArgs,
|
||||
OrderStatus,
|
||||
} from '../src';
|
||||
|
||||
import { chaiSetup } from './utils/chai_setup';
|
||||
import { constants } from './utils/constants';
|
||||
import { tokenUtils } from './utils/token_utils';
|
||||
import { provider, web3Wrapper } from './utils/web3_wrapper';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
|
||||
|
||||
describe('ForwarderWrapper', () => {
|
||||
const contractWrappersConfig = {
|
||||
networkId: constants.TESTRPC_NETWORK_ID,
|
||||
blockPollingIntervalMs: 0,
|
||||
};
|
||||
const fillableAmount = new BigNumber(5);
|
||||
const takerTokenFillAmount = new BigNumber(5);
|
||||
let contractWrappers: ContractWrappers;
|
||||
let fillScenarios: FillScenarios;
|
||||
let forwarderContractAddress: string;
|
||||
let exchangeContractAddress: string;
|
||||
let zrxTokenAddress: string;
|
||||
let userAddresses: string[];
|
||||
let coinbase: string;
|
||||
let makerAddress: string;
|
||||
let takerAddress: string;
|
||||
let feeRecipient: string;
|
||||
let anotherMakerAddress: string;
|
||||
let makerTokenAddress: string;
|
||||
let takerTokenAddress: string;
|
||||
let makerAssetData: string;
|
||||
let takerAssetData: string;
|
||||
let signedOrder: SignedOrder;
|
||||
let anotherSignedOrder: SignedOrder;
|
||||
before(async () => {
|
||||
await blockchainLifecycle.startAsync();
|
||||
contractWrappers = new ContractWrappers(provider, contractWrappersConfig);
|
||||
forwarderContractAddress = contractWrappers.forwarder.getContractAddress();
|
||||
exchangeContractAddress = contractWrappers.exchange.getContractAddress();
|
||||
userAddresses = await web3Wrapper.getAvailableAddressesAsync();
|
||||
zrxTokenAddress = tokenUtils.getProtocolTokenAddress();
|
||||
fillScenarios = new FillScenarios(
|
||||
provider,
|
||||
userAddresses,
|
||||
zrxTokenAddress,
|
||||
exchangeContractAddress,
|
||||
contractWrappers.erc20Proxy.getContractAddress(),
|
||||
contractWrappers.erc721Proxy.getContractAddress(),
|
||||
);
|
||||
[coinbase, makerAddress, takerAddress, feeRecipient, anotherMakerAddress] = userAddresses;
|
||||
[makerTokenAddress] = tokenUtils.getDummyERC20TokenAddresses();
|
||||
takerTokenAddress = tokenUtils.getWethTokenAddress();
|
||||
[makerAssetData, takerAssetData] = [
|
||||
assetDataUtils.encodeERC20AssetData(makerTokenAddress),
|
||||
assetDataUtils.encodeERC20AssetData(takerTokenAddress),
|
||||
];
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerAssetData,
|
||||
takerAssetData,
|
||||
makerAddress,
|
||||
constants.NULL_ADDRESS,
|
||||
fillableAmount,
|
||||
);
|
||||
anotherSignedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerAssetData,
|
||||
takerAssetData,
|
||||
makerAddress,
|
||||
constants.NULL_ADDRESS,
|
||||
fillableAmount,
|
||||
);
|
||||
});
|
||||
after(async () => {
|
||||
await blockchainLifecycle.revertAsync();
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await blockchainLifecycle.startAsync();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await blockchainLifecycle.revertAsync();
|
||||
});
|
||||
describe('#marketBuyOrdersWithEthAsync', () => {
|
||||
it('should market buy orders with eth', async () => {
|
||||
const signedOrders = [signedOrder, anotherSignedOrder];
|
||||
const makerAssetFillAmount = signedOrder.makerAssetAmount.plus(anotherSignedOrder.makerAssetAmount);
|
||||
const txHash = await contractWrappers.forwarder.marketBuyOrdersWithEthAsync(
|
||||
signedOrders,
|
||||
makerAssetFillAmount,
|
||||
takerAddress,
|
||||
makerAssetFillAmount,
|
||||
);
|
||||
await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS);
|
||||
const ordersInfo = await contractWrappers.exchange.getOrdersInfoAsync([signedOrder, anotherSignedOrder]);
|
||||
expect(ordersInfo[0].orderStatus).to.be.equal(OrderStatus.FULLY_FILLED);
|
||||
expect(ordersInfo[1].orderStatus).to.be.equal(OrderStatus.FULLY_FILLED);
|
||||
});
|
||||
});
|
||||
describe('#marketSellOrdersWithEthAsync', () => {
|
||||
it('should market sell orders with eth', async () => {
|
||||
const signedOrders = [signedOrder, anotherSignedOrder];
|
||||
const makerAssetFillAmount = signedOrder.makerAssetAmount.plus(anotherSignedOrder.makerAssetAmount);
|
||||
const txHash = await contractWrappers.forwarder.marketSellOrdersWithEthAsync(
|
||||
signedOrders,
|
||||
takerAddress,
|
||||
makerAssetFillAmount,
|
||||
);
|
||||
await web3Wrapper.awaitTransactionSuccessAsync(txHash, constants.AWAIT_TRANSACTION_MINED_MS);
|
||||
const ordersInfo = await contractWrappers.exchange.getOrdersInfoAsync([signedOrder, anotherSignedOrder]);
|
||||
expect(ordersInfo[0].orderStatus).to.be.equal(OrderStatus.FULLY_FILLED);
|
||||
expect(ordersInfo[1].orderStatus).to.be.equal(OrderStatus.FILLABLE);
|
||||
expect(ordersInfo[1].orderTakerAssetFilledAmount).to.be.bignumber.equal(new BigNumber(4)); // only 95% of ETH is sold
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -42,12 +42,13 @@
|
||||
"TestConstants",
|
||||
"TestLibBytes",
|
||||
"TestLibs",
|
||||
"TestExchangeInternals",
|
||||
"TestSignatureValidator",
|
||||
"TokenRegistry",
|
||||
"Validator",
|
||||
"Wallet",
|
||||
"Whitelist",
|
||||
"WETH9",
|
||||
"Whitelist",
|
||||
"ZRXToken"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"lint-contracts": "solhint src/2.0.0/**/**/**/**/*.sol"
|
||||
},
|
||||
"config": {
|
||||
"abis": "../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|ERC20Proxy|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestLibBytes|TestLibs|TestSignatureValidator|Validator|Wallet|TokenRegistry|Whitelist|WETH9|ZRXToken).json"
|
||||
"abis": "../migrations/artifacts/2.0.0/@(AssetProxyOwner|DummyERC20Token|DummyERC721Receiver|DummyERC721Token|ERC20Proxy|ERC721Proxy|Forwarder|Exchange|ExchangeWrapper|IAssetData|IAssetProxy|MixinAuthorizable|MultiSigWallet|MultiSigWalletWithTimeLock|TestAssetProxyOwner|TestAssetProxyDispatcher|TestConstants|TestExchangeInternals|TestLibBytes|TestLibs|TestSignatureValidator|Validator|Wallet|TokenRegistry|Whitelist|WETH9|ZRXToken).json"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -79,11 +79,13 @@
|
||||
"@0xproject/typescript-typings": "^1.0.3",
|
||||
"@0xproject/utils": "^1.0.4",
|
||||
"@0xproject/web3-wrapper": "^1.1.2",
|
||||
"@types/js-combinatorics": "^0.5.29",
|
||||
"bn.js": "^4.11.8",
|
||||
"ethereum-types": "^1.0.3",
|
||||
"ethereumjs-abi": "0.6.5",
|
||||
"ethereumjs-util": "^5.1.1",
|
||||
"ethers": "3.0.22",
|
||||
"js-combinatorics": "^0.5.3",
|
||||
"lodash": "^4.17.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +207,6 @@ contract MixinExchangeCore is
|
||||
/// @param order that was filled.
|
||||
/// @param takerAddress Address of taker who filled the order.
|
||||
/// @param orderTakerAssetFilledAmount Amount of order already filled.
|
||||
/// @return fillResults Amounts filled and fees paid by maker and taker.
|
||||
function updateFilledState(
|
||||
Order memory order,
|
||||
address takerAddress,
|
||||
|
||||
120
packages/contracts/src/2.0.0/test/TestExchangeInternals/TestExchangeInternals.sol
vendored
Normal file
120
packages/contracts/src/2.0.0/test/TestExchangeInternals/TestExchangeInternals.sol
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
|
||||
Copyright 2018 ZeroEx Intl.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
*/
|
||||
|
||||
pragma solidity 0.4.24;
|
||||
pragma experimental ABIEncoderV2;
|
||||
|
||||
import "../../protocol/Exchange/Exchange.sol";
|
||||
|
||||
|
||||
contract TestExchangeInternals is
|
||||
Exchange
|
||||
{
|
||||
constructor ()
|
||||
public
|
||||
Exchange("")
|
||||
{}
|
||||
|
||||
/// @dev Adds properties of both FillResults instances.
|
||||
/// Modifies the first FillResults instance specified.
|
||||
/// Note that this function has been modified from the original
|
||||
// internal version to return the FillResults.
|
||||
/// @param totalFillResults Fill results instance that will be added onto.
|
||||
/// @param singleFillResults Fill results instance that will be added to totalFillResults.
|
||||
/// @return newTotalFillResults The result of adding singleFillResults to totalFilResults.
|
||||
function publicAddFillResults(FillResults memory totalFillResults, FillResults memory singleFillResults)
|
||||
public
|
||||
pure
|
||||
returns (FillResults memory)
|
||||
{
|
||||
addFillResults(totalFillResults, singleFillResults);
|
||||
return totalFillResults;
|
||||
}
|
||||
|
||||
/// @dev Calculates amounts filled and fees paid by maker and taker.
|
||||
/// @param order to be filled.
|
||||
/// @param takerAssetFilledAmount Amount of takerAsset that will be filled.
|
||||
/// @return fillResults Amounts filled and fees paid by maker and taker.
|
||||
function publicCalculateFillResults(
|
||||
Order memory order,
|
||||
uint256 takerAssetFilledAmount
|
||||
)
|
||||
public
|
||||
pure
|
||||
returns (FillResults memory fillResults)
|
||||
{
|
||||
return calculateFillResults(order, takerAssetFilledAmount);
|
||||
}
|
||||
|
||||
/// @dev Calculates partial value given a numerator and denominator.
|
||||
/// @param numerator Numerator.
|
||||
/// @param denominator Denominator.
|
||||
/// @param target Value to calculate partial of.
|
||||
/// @return Partial value of target.
|
||||
function publicGetPartialAmount(
|
||||
uint256 numerator,
|
||||
uint256 denominator,
|
||||
uint256 target
|
||||
)
|
||||
public
|
||||
pure
|
||||
returns (uint256 partialAmount)
|
||||
{
|
||||
return getPartialAmount(numerator, denominator, target);
|
||||
}
|
||||
|
||||
/// @dev Checks if rounding error > 0.1%.
|
||||
/// @param numerator Numerator.
|
||||
/// @param denominator Denominator.
|
||||
/// @param target Value to multiply with numerator/denominator.
|
||||
/// @return Rounding error is present.
|
||||
function publicIsRoundingError(
|
||||
uint256 numerator,
|
||||
uint256 denominator,
|
||||
uint256 target
|
||||
)
|
||||
public
|
||||
pure
|
||||
returns (bool isError)
|
||||
{
|
||||
return isRoundingError(numerator, denominator, target);
|
||||
}
|
||||
|
||||
/// @dev Updates state with results of a fill order.
|
||||
/// @param order that was filled.
|
||||
/// @param takerAddress Address of taker who filled the order.
|
||||
/// @param orderTakerAssetFilledAmount Amount of order already filled.
|
||||
/// @return fillResults Amounts filled and fees paid by maker and taker.
|
||||
function publicUpdateFilledState(
|
||||
Order memory order,
|
||||
address takerAddress,
|
||||
bytes32 orderHash,
|
||||
uint256 orderTakerAssetFilledAmount,
|
||||
FillResults memory fillResults
|
||||
)
|
||||
public
|
||||
{
|
||||
updateFilledState(
|
||||
order,
|
||||
takerAddress,
|
||||
orderHash,
|
||||
orderTakerAssetFilledAmount,
|
||||
fillResults
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,10 @@ import { BlockchainLifecycle } from '@0xproject/dev-utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { chaiSetup } from '../utils/chai_setup';
|
||||
import { CoreCombinatorialUtils, coreCombinatorialUtilsFactoryAsync } from '../utils/core_combinatorial_utils';
|
||||
import {
|
||||
FillOrderCombinatorialUtils,
|
||||
fillOrderCombinatorialUtilsFactoryAsync,
|
||||
} from '../utils/fill_order_combinatorial_utils';
|
||||
import {
|
||||
AllowanceAmountScenario,
|
||||
AssetDataScenario,
|
||||
@@ -47,11 +50,11 @@ const defaultFillScenario = {
|
||||
};
|
||||
|
||||
describe('FillOrder Tests', () => {
|
||||
let coreCombinatorialUtils: CoreCombinatorialUtils;
|
||||
let fillOrderCombinatorialUtils: FillOrderCombinatorialUtils;
|
||||
|
||||
before(async () => {
|
||||
await blockchainLifecycle.startAsync();
|
||||
coreCombinatorialUtils = await coreCombinatorialUtilsFactoryAsync(web3Wrapper, txDefaults);
|
||||
fillOrderCombinatorialUtils = await fillOrderCombinatorialUtilsFactoryAsync(web3Wrapper, txDefaults);
|
||||
});
|
||||
after(async () => {
|
||||
await blockchainLifecycle.revertAsync();
|
||||
@@ -67,19 +70,19 @@ describe('FillOrder Tests', () => {
|
||||
_.forEach(fillScenarios, fillScenario => {
|
||||
const description = `Combinatorial OrderFill: ${JSON.stringify(fillScenario)}`;
|
||||
it(description, async () => {
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const allFillScenarios = CoreCombinatorialUtils.generateFillOrderCombinations();
|
||||
const allFillScenarios = FillOrderCombinatorialUtils.generateFillOrderCombinations();
|
||||
describe('Combinatorially generated fills orders', () => test(allFillScenarios));
|
||||
|
||||
it('should transfer the correct amounts when makerAssetAmount === takerAssetAmount', async () => {
|
||||
const fillScenario = {
|
||||
...defaultFillScenario,
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
it('should transfer the correct amounts when makerAssetAmount > takerAssetAmount', async () => {
|
||||
const fillScenario = {
|
||||
@@ -89,7 +92,7 @@ describe('FillOrder Tests', () => {
|
||||
takerAssetAmountScenario: OrderAssetAmountScenario.Small,
|
||||
},
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
it('should transfer the correct amounts when makerAssetAmount < takerAssetAmount', async () => {
|
||||
const fillScenario = {
|
||||
@@ -99,7 +102,7 @@ describe('FillOrder Tests', () => {
|
||||
makerAssetAmountScenario: OrderAssetAmountScenario.Small,
|
||||
},
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
it('should transfer the correct amounts when taker is specified and order is claimed by taker', async () => {
|
||||
const fillScenario = {
|
||||
@@ -109,14 +112,14 @@ describe('FillOrder Tests', () => {
|
||||
takerScenario: TakerScenario.CorrectlySpecified,
|
||||
},
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
it('should fill remaining value if takerAssetFillAmount > remaining takerAssetAmount', async () => {
|
||||
const fillScenario = {
|
||||
...defaultFillScenario,
|
||||
takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount,
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
it('should throw when taker is specified and order is claimed by other', async () => {
|
||||
const fillScenario = {
|
||||
@@ -126,7 +129,7 @@ describe('FillOrder Tests', () => {
|
||||
takerScenario: TakerScenario.IncorrectlySpecified,
|
||||
},
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
|
||||
it('should throw if makerAssetAmount is 0', async () => {
|
||||
@@ -138,7 +141,7 @@ describe('FillOrder Tests', () => {
|
||||
},
|
||||
takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount,
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
|
||||
it('should throw if takerAssetAmount is 0', async () => {
|
||||
@@ -150,7 +153,7 @@ describe('FillOrder Tests', () => {
|
||||
},
|
||||
takerAssetFillAmountScenario: TakerAssetFillAmountScenario.GreaterThanRemainingFillableTakerAssetAmount,
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
|
||||
it('should throw if takerAssetFillAmount is 0', async () => {
|
||||
@@ -158,7 +161,7 @@ describe('FillOrder Tests', () => {
|
||||
...defaultFillScenario,
|
||||
takerAssetFillAmountScenario: TakerAssetFillAmountScenario.Zero,
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
|
||||
it('should throw if an order is expired', async () => {
|
||||
@@ -169,7 +172,7 @@ describe('FillOrder Tests', () => {
|
||||
expirationTimeSecondsScenario: ExpirationTimeSecondsScenario.InPast,
|
||||
},
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
|
||||
it('should throw if maker erc20Balances are too low to fill order', async () => {
|
||||
@@ -180,7 +183,7 @@ describe('FillOrder Tests', () => {
|
||||
traderAssetBalance: BalanceAmountScenario.TooLow,
|
||||
},
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
|
||||
it('should throw if taker erc20Balances are too low to fill order', async () => {
|
||||
@@ -191,7 +194,7 @@ describe('FillOrder Tests', () => {
|
||||
traderAssetBalance: BalanceAmountScenario.TooLow,
|
||||
},
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
|
||||
it('should throw if maker allowances are too low to fill order', async () => {
|
||||
@@ -202,7 +205,7 @@ describe('FillOrder Tests', () => {
|
||||
traderAssetAllowance: AllowanceAmountScenario.TooLow,
|
||||
},
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
|
||||
it('should throw if taker allowances are too low to fill order', async () => {
|
||||
@@ -213,7 +216,7 @@ describe('FillOrder Tests', () => {
|
||||
traderAssetAllowance: AllowanceAmountScenario.TooLow,
|
||||
},
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -228,7 +231,7 @@ describe('FillOrder Tests', () => {
|
||||
},
|
||||
takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
|
||||
it('should successfully fill order when makerAsset is ERC721 and takerAsset is ERC20', async () => {
|
||||
@@ -241,7 +244,7 @@ describe('FillOrder Tests', () => {
|
||||
},
|
||||
takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario, true);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario, true);
|
||||
});
|
||||
|
||||
it('should successfully fill order when makerAsset is ERC20 and takerAsset is ERC721', async () => {
|
||||
@@ -254,7 +257,7 @@ describe('FillOrder Tests', () => {
|
||||
},
|
||||
takerAssetFillAmountScenario: TakerAssetFillAmountScenario.ExactlyRemainingFillableTakerAssetAmount,
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
|
||||
it('should successfully fill order when makerAsset is ERC721 and approveAll is set for it', async () => {
|
||||
@@ -271,7 +274,7 @@ describe('FillOrder Tests', () => {
|
||||
traderAssetAllowance: AllowanceAmountScenario.Unlimited,
|
||||
},
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
|
||||
it('should successfully fill order when makerAsset and takerAsset are ERC721 and approveAll is set for them', async () => {
|
||||
@@ -292,7 +295,7 @@ describe('FillOrder Tests', () => {
|
||||
traderAssetAllowance: AllowanceAmountScenario.Unlimited,
|
||||
},
|
||||
};
|
||||
await coreCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
await fillOrderCombinatorialUtils.testFillOrderScenarioAsync(provider, fillScenario);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
305
packages/contracts/test/exchange/internal.ts
Normal file
305
packages/contracts/test/exchange/internal.ts
Normal file
@@ -0,0 +1,305 @@
|
||||
import { BlockchainLifecycle } from '@0xproject/dev-utils';
|
||||
import { Order, RevertReason, SignedOrder } from '@0xproject/types';
|
||||
import { BigNumber } from '@0xproject/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { TestExchangeInternalsContract } from '../../generated_contract_wrappers/test_exchange_internals';
|
||||
import { artifacts } from '../utils/artifacts';
|
||||
import {
|
||||
getInvalidOpcodeErrorMessageForCallAsync,
|
||||
getRevertReasonOrErrorMessageForSendTransactionAsync,
|
||||
} from '../utils/assertions';
|
||||
import { chaiSetup } from '../utils/chai_setup';
|
||||
import { bytes32Values, testCombinatoriallyWithReferenceFuncAsync, uint256Values } from '../utils/combinatorial_utils';
|
||||
import { constants } from '../utils/constants';
|
||||
import { FillResults } from '../utils/types';
|
||||
import { provider, txDefaults, web3Wrapper } from '../utils/web3_wrapper';
|
||||
|
||||
chaiSetup.configure();
|
||||
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);
|
||||
|
||||
const MAX_UINT256 = new BigNumber(2).pow(256).minus(1);
|
||||
|
||||
const emptyOrder: Order = {
|
||||
senderAddress: constants.NULL_ADDRESS,
|
||||
makerAddress: constants.NULL_ADDRESS,
|
||||
takerAddress: constants.NULL_ADDRESS,
|
||||
makerFee: new BigNumber(0),
|
||||
takerFee: new BigNumber(0),
|
||||
makerAssetAmount: new BigNumber(0),
|
||||
takerAssetAmount: new BigNumber(0),
|
||||
makerAssetData: '0x',
|
||||
takerAssetData: '0x',
|
||||
salt: new BigNumber(0),
|
||||
exchangeAddress: constants.NULL_ADDRESS,
|
||||
feeRecipientAddress: constants.NULL_ADDRESS,
|
||||
expirationTimeSeconds: new BigNumber(0),
|
||||
};
|
||||
|
||||
const emptySignedOrder: SignedOrder = {
|
||||
...emptyOrder,
|
||||
signature: '',
|
||||
};
|
||||
|
||||
const overflowErrorForCall = new Error(RevertReason.Uint256Overflow);
|
||||
|
||||
async function referenceGetPartialAmountAsync(
|
||||
numerator: BigNumber,
|
||||
denominator: BigNumber,
|
||||
target: BigNumber,
|
||||
): Promise<BigNumber> {
|
||||
const invalidOpcodeErrorForCall = new Error(await getInvalidOpcodeErrorMessageForCallAsync());
|
||||
const product = numerator.mul(target);
|
||||
if (product.greaterThan(MAX_UINT256)) {
|
||||
throw overflowErrorForCall;
|
||||
}
|
||||
if (denominator.eq(0)) {
|
||||
throw invalidOpcodeErrorForCall;
|
||||
}
|
||||
return product.dividedToIntegerBy(denominator);
|
||||
}
|
||||
|
||||
describe('Exchange core internal functions', () => {
|
||||
let testExchange: TestExchangeInternalsContract;
|
||||
let invalidOpcodeErrorForCall: Error | undefined;
|
||||
let overflowErrorForSendTransaction: Error | undefined;
|
||||
|
||||
before(async () => {
|
||||
await blockchainLifecycle.startAsync();
|
||||
});
|
||||
after(async () => {
|
||||
await blockchainLifecycle.revertAsync();
|
||||
});
|
||||
before(async () => {
|
||||
testExchange = await TestExchangeInternalsContract.deployFrom0xArtifactAsync(
|
||||
artifacts.TestExchangeInternals,
|
||||
provider,
|
||||
txDefaults,
|
||||
);
|
||||
overflowErrorForSendTransaction = new Error(
|
||||
await getRevertReasonOrErrorMessageForSendTransactionAsync(RevertReason.Uint256Overflow),
|
||||
);
|
||||
invalidOpcodeErrorForCall = new Error(await getInvalidOpcodeErrorMessageForCallAsync());
|
||||
});
|
||||
// Note(albrow): Don't forget to add beforeEach and afterEach calls to reset
|
||||
// the blockchain state for any tests which modify it!
|
||||
|
||||
describe('addFillResults', async () => {
|
||||
function makeFillResults(value: BigNumber): FillResults {
|
||||
return {
|
||||
makerAssetFilledAmount: value,
|
||||
takerAssetFilledAmount: value,
|
||||
makerFeePaid: value,
|
||||
takerFeePaid: value,
|
||||
};
|
||||
}
|
||||
async function referenceAddFillResultsAsync(
|
||||
totalValue: BigNumber,
|
||||
singleValue: BigNumber,
|
||||
): Promise<FillResults> {
|
||||
// Note(albrow): Here, each of totalFillResults and
|
||||
// singleFillResults will consist of fields with the same values.
|
||||
// This should be safe because none of the fields in a given
|
||||
// FillResults are ever used together in a mathemetical operation.
|
||||
// They are only used with the corresponding field from *the other*
|
||||
// FillResults, which are different.
|
||||
const totalFillResults = makeFillResults(totalValue);
|
||||
const singleFillResults = makeFillResults(singleValue);
|
||||
// HACK(albrow): _.mergeWith mutates the first argument! To
|
||||
// workaround this we use _.cloneDeep.
|
||||
return _.mergeWith(
|
||||
_.cloneDeep(totalFillResults),
|
||||
singleFillResults,
|
||||
(totalVal: BigNumber, singleVal: BigNumber) => {
|
||||
const newTotal = totalVal.add(singleVal);
|
||||
if (newTotal.greaterThan(MAX_UINT256)) {
|
||||
throw overflowErrorForCall;
|
||||
}
|
||||
return newTotal;
|
||||
},
|
||||
);
|
||||
}
|
||||
async function testAddFillResultsAsync(totalValue: BigNumber, singleValue: BigNumber): Promise<FillResults> {
|
||||
const totalFillResults = makeFillResults(totalValue);
|
||||
const singleFillResults = makeFillResults(singleValue);
|
||||
return testExchange.publicAddFillResults.callAsync(totalFillResults, singleFillResults);
|
||||
}
|
||||
await testCombinatoriallyWithReferenceFuncAsync(
|
||||
'addFillResults',
|
||||
referenceAddFillResultsAsync,
|
||||
testAddFillResultsAsync,
|
||||
[uint256Values, uint256Values],
|
||||
);
|
||||
});
|
||||
|
||||
describe('calculateFillResults', async () => {
|
||||
function makeOrder(
|
||||
makerAssetAmount: BigNumber,
|
||||
takerAssetAmount: BigNumber,
|
||||
makerFee: BigNumber,
|
||||
takerFee: BigNumber,
|
||||
): Order {
|
||||
return {
|
||||
...emptyOrder,
|
||||
makerAssetAmount,
|
||||
takerAssetAmount,
|
||||
makerFee,
|
||||
takerFee,
|
||||
};
|
||||
}
|
||||
async function referenceCalculateFillResultsAsync(
|
||||
orderTakerAssetAmount: BigNumber,
|
||||
takerAssetFilledAmount: BigNumber,
|
||||
otherAmount: BigNumber,
|
||||
): Promise<FillResults> {
|
||||
// Note(albrow): Here we are re-using the same value (otherAmount)
|
||||
// for order.makerAssetAmount, order.makerFee, and order.takerFee.
|
||||
// This should be safe because they are never used with each other
|
||||
// in any mathematical operation in either the reference TypeScript
|
||||
// implementation or the Solidity implementation of
|
||||
// calculateFillResults.
|
||||
return {
|
||||
makerAssetFilledAmount: await referenceGetPartialAmountAsync(
|
||||
takerAssetFilledAmount,
|
||||
orderTakerAssetAmount,
|
||||
otherAmount,
|
||||
),
|
||||
takerAssetFilledAmount,
|
||||
makerFeePaid: await referenceGetPartialAmountAsync(
|
||||
takerAssetFilledAmount,
|
||||
orderTakerAssetAmount,
|
||||
otherAmount,
|
||||
),
|
||||
takerFeePaid: await referenceGetPartialAmountAsync(
|
||||
takerAssetFilledAmount,
|
||||
orderTakerAssetAmount,
|
||||
otherAmount,
|
||||
),
|
||||
};
|
||||
}
|
||||
async function testCalculateFillResultsAsync(
|
||||
orderTakerAssetAmount: BigNumber,
|
||||
takerAssetFilledAmount: BigNumber,
|
||||
otherAmount: BigNumber,
|
||||
): Promise<FillResults> {
|
||||
const order = makeOrder(otherAmount, orderTakerAssetAmount, otherAmount, otherAmount);
|
||||
return testExchange.publicCalculateFillResults.callAsync(order, takerAssetFilledAmount);
|
||||
}
|
||||
await testCombinatoriallyWithReferenceFuncAsync(
|
||||
'calculateFillResults',
|
||||
referenceCalculateFillResultsAsync,
|
||||
testCalculateFillResultsAsync,
|
||||
[uint256Values, uint256Values, uint256Values],
|
||||
);
|
||||
});
|
||||
|
||||
describe('getPartialAmount', async () => {
|
||||
async function testGetPartialAmountAsync(
|
||||
numerator: BigNumber,
|
||||
denominator: BigNumber,
|
||||
target: BigNumber,
|
||||
): Promise<BigNumber> {
|
||||
return testExchange.publicGetPartialAmount.callAsync(numerator, denominator, target);
|
||||
}
|
||||
await testCombinatoriallyWithReferenceFuncAsync(
|
||||
'getPartialAmount',
|
||||
referenceGetPartialAmountAsync,
|
||||
testGetPartialAmountAsync,
|
||||
[uint256Values, uint256Values, uint256Values],
|
||||
);
|
||||
});
|
||||
|
||||
describe('isRoundingError', async () => {
|
||||
async function referenceIsRoundingErrorAsync(
|
||||
numerator: BigNumber,
|
||||
denominator: BigNumber,
|
||||
target: BigNumber,
|
||||
): Promise<boolean> {
|
||||
const product = numerator.mul(target);
|
||||
if (denominator.eq(0)) {
|
||||
throw invalidOpcodeErrorForCall;
|
||||
}
|
||||
const remainder = product.mod(denominator);
|
||||
if (remainder.eq(0)) {
|
||||
return false;
|
||||
}
|
||||
if (product.greaterThan(MAX_UINT256)) {
|
||||
throw overflowErrorForCall;
|
||||
}
|
||||
if (product.eq(0)) {
|
||||
throw invalidOpcodeErrorForCall;
|
||||
}
|
||||
const remainderTimes1000000 = remainder.mul('1000000');
|
||||
if (remainderTimes1000000.greaterThan(MAX_UINT256)) {
|
||||
throw overflowErrorForCall;
|
||||
}
|
||||
const errPercentageTimes1000000 = remainderTimes1000000.dividedToIntegerBy(product);
|
||||
return errPercentageTimes1000000.greaterThan('1000');
|
||||
}
|
||||
async function testIsRoundingErrorAsync(
|
||||
numerator: BigNumber,
|
||||
denominator: BigNumber,
|
||||
target: BigNumber,
|
||||
): Promise<boolean> {
|
||||
return testExchange.publicIsRoundingError.callAsync(numerator, denominator, target);
|
||||
}
|
||||
await testCombinatoriallyWithReferenceFuncAsync(
|
||||
'isRoundingError',
|
||||
referenceIsRoundingErrorAsync,
|
||||
testIsRoundingErrorAsync,
|
||||
[uint256Values, uint256Values, uint256Values],
|
||||
);
|
||||
});
|
||||
|
||||
describe('updateFilledState', async () => {
|
||||
// Note(albrow): Since updateFilledState modifies the state by calling
|
||||
// sendTransaction, we must reset the state after each test.
|
||||
beforeEach(async () => {
|
||||
await blockchainLifecycle.startAsync();
|
||||
});
|
||||
afterEach(async () => {
|
||||
await blockchainLifecycle.revertAsync();
|
||||
});
|
||||
async function referenceUpdateFilledStateAsync(
|
||||
takerAssetFilledAmount: BigNumber,
|
||||
orderTakerAssetFilledAmount: BigNumber,
|
||||
// tslint:disable-next-line:no-unused-variable
|
||||
orderHash: string,
|
||||
): Promise<BigNumber> {
|
||||
const totalFilledAmount = takerAssetFilledAmount.add(orderTakerAssetFilledAmount);
|
||||
if (totalFilledAmount.greaterThan(MAX_UINT256)) {
|
||||
throw overflowErrorForSendTransaction;
|
||||
}
|
||||
return totalFilledAmount;
|
||||
}
|
||||
async function testUpdateFilledStateAsync(
|
||||
takerAssetFilledAmount: BigNumber,
|
||||
orderTakerAssetFilledAmount: BigNumber,
|
||||
orderHash: string,
|
||||
): Promise<BigNumber> {
|
||||
const fillResults = {
|
||||
makerAssetFilledAmount: new BigNumber(0),
|
||||
takerAssetFilledAmount,
|
||||
makerFeePaid: new BigNumber(0),
|
||||
takerFeePaid: new BigNumber(0),
|
||||
};
|
||||
await web3Wrapper.awaitTransactionSuccessAsync(
|
||||
await testExchange.publicUpdateFilledState.sendTransactionAsync(
|
||||
emptySignedOrder,
|
||||
constants.NULL_ADDRESS,
|
||||
orderHash,
|
||||
orderTakerAssetFilledAmount,
|
||||
fillResults,
|
||||
),
|
||||
constants.AWAIT_TRANSACTION_MINED_MS,
|
||||
);
|
||||
return testExchange.filled.callAsync(orderHash);
|
||||
}
|
||||
await testCombinatoriallyWithReferenceFuncAsync(
|
||||
'updateFilledState',
|
||||
referenceUpdateFilledStateAsync,
|
||||
testUpdateFilledStateAsync,
|
||||
[uint256Values, uint256Values, bytes32Values],
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -67,6 +67,35 @@ describe('Exchange libs', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
// Note(albrow): These tests are designed to be supplemental to the
|
||||
// combinatorial tests in test/exchange/internal. They test specific edge
|
||||
// cases that are not covered by the combinatorial tests.
|
||||
describe('LibMath', () => {
|
||||
it('should return false if there is a rounding error of 0.1%', async () => {
|
||||
const numerator = new BigNumber(20);
|
||||
const denominator = new BigNumber(999);
|
||||
const target = new BigNumber(50);
|
||||
// rounding error = ((20*50/999) - floor(20*50/999)) / (20*50/999) = 0.1%
|
||||
const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target);
|
||||
expect(isRoundingError).to.be.false();
|
||||
});
|
||||
it('should return false if there is a rounding of 0.09%', async () => {
|
||||
const numerator = new BigNumber(20);
|
||||
const denominator = new BigNumber(9991);
|
||||
const target = new BigNumber(500);
|
||||
// rounding error = ((20*500/9991) - floor(20*500/9991)) / (20*500/9991) = 0.09%
|
||||
const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target);
|
||||
expect(isRoundingError).to.be.false();
|
||||
});
|
||||
it('should return true if there is a rounding error of 0.11%', async () => {
|
||||
const numerator = new BigNumber(20);
|
||||
const denominator = new BigNumber(9989);
|
||||
const target = new BigNumber(500);
|
||||
// rounding error = ((20*500/9989) - floor(20*500/9989)) / (20*500/9989) = 0.011%
|
||||
const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target);
|
||||
expect(isRoundingError).to.be.true();
|
||||
});
|
||||
});
|
||||
|
||||
describe('LibOrder', () => {
|
||||
describe('getOrderSchema', () => {
|
||||
@@ -93,96 +122,4 @@ describe('Exchange libs', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('LibMath', () => {
|
||||
describe('isRoundingError', () => {
|
||||
it('should return false if there is a rounding error of 0.1%', async () => {
|
||||
const numerator = new BigNumber(20);
|
||||
const denominator = new BigNumber(999);
|
||||
const target = new BigNumber(50);
|
||||
// rounding error = ((20*50/999) - floor(20*50/999)) / (20*50/999) = 0.1%
|
||||
const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target);
|
||||
expect(isRoundingError).to.be.false();
|
||||
});
|
||||
|
||||
it('should return false if there is a rounding of 0.09%', async () => {
|
||||
const numerator = new BigNumber(20);
|
||||
const denominator = new BigNumber(9991);
|
||||
const target = new BigNumber(500);
|
||||
// rounding error = ((20*500/9991) - floor(20*500/9991)) / (20*500/9991) = 0.09%
|
||||
const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target);
|
||||
expect(isRoundingError).to.be.false();
|
||||
});
|
||||
|
||||
it('should return true if there is a rounding error of 0.11%', async () => {
|
||||
const numerator = new BigNumber(20);
|
||||
const denominator = new BigNumber(9989);
|
||||
const target = new BigNumber(500);
|
||||
// rounding error = ((20*500/9989) - floor(20*500/9989)) / (20*500/9989) = 0.011%
|
||||
const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target);
|
||||
expect(isRoundingError).to.be.true();
|
||||
});
|
||||
|
||||
it('should return true if there is a rounding error > 0.1%', async () => {
|
||||
const numerator = new BigNumber(3);
|
||||
const denominator = new BigNumber(7);
|
||||
const target = new BigNumber(10);
|
||||
// rounding error = ((3*10/7) - floor(3*10/7)) / (3*10/7) = 6.67%
|
||||
const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target);
|
||||
expect(isRoundingError).to.be.true();
|
||||
});
|
||||
|
||||
it('should return false when there is no rounding error', async () => {
|
||||
const numerator = new BigNumber(1);
|
||||
const denominator = new BigNumber(2);
|
||||
const target = new BigNumber(10);
|
||||
|
||||
const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target);
|
||||
expect(isRoundingError).to.be.false();
|
||||
});
|
||||
|
||||
it('should return false when there is rounding error <= 0.1%', async () => {
|
||||
// randomly generated numbers
|
||||
const numerator = new BigNumber(76564);
|
||||
const denominator = new BigNumber(676373677);
|
||||
const target = new BigNumber(105762562);
|
||||
// rounding error = ((76564*105762562/676373677) - floor(76564*105762562/676373677)) /
|
||||
// (76564*105762562/676373677) = 0.0007%
|
||||
const isRoundingError = await libs.publicIsRoundingError.callAsync(numerator, denominator, target);
|
||||
expect(isRoundingError).to.be.false();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPartialAmount', () => {
|
||||
it('should return the numerator/denominator*target', async () => {
|
||||
const numerator = new BigNumber(1);
|
||||
const denominator = new BigNumber(2);
|
||||
const target = new BigNumber(10);
|
||||
|
||||
const partialAmount = await libs.publicGetPartialAmount.callAsync(numerator, denominator, target);
|
||||
const expectedPartialAmount = 5;
|
||||
expect(partialAmount).to.be.bignumber.equal(expectedPartialAmount);
|
||||
});
|
||||
|
||||
it('should round down', async () => {
|
||||
const numerator = new BigNumber(2);
|
||||
const denominator = new BigNumber(3);
|
||||
const target = new BigNumber(10);
|
||||
|
||||
const partialAmount = await libs.publicGetPartialAmount.callAsync(numerator, denominator, target);
|
||||
const expectedPartialAmount = 6;
|
||||
expect(partialAmount).to.be.bignumber.equal(expectedPartialAmount);
|
||||
});
|
||||
|
||||
it('should round .5 down', async () => {
|
||||
const numerator = new BigNumber(1);
|
||||
const denominator = new BigNumber(20);
|
||||
const target = new BigNumber(10);
|
||||
|
||||
const partialAmount = await libs.publicGetPartialAmount.callAsync(numerator, denominator, target);
|
||||
const expectedPartialAmount = 0;
|
||||
expect(partialAmount).to.be.bignumber.equal(expectedPartialAmount);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import * as MultiSigWalletWithTimeLock from '../../artifacts/MultiSigWalletWithT
|
||||
import * as TestAssetProxyDispatcher from '../../artifacts/TestAssetProxyDispatcher.json';
|
||||
import * as TestAssetProxyOwner from '../../artifacts/TestAssetProxyOwner.json';
|
||||
import * as TestConstants from '../../artifacts/TestConstants.json';
|
||||
import * as TestExchangeInternals from '../../artifacts/TestExchangeInternals.json';
|
||||
import * as TestLibBytes from '../../artifacts/TestLibBytes.json';
|
||||
import * as TestLibs from '../../artifacts/TestLibs.json';
|
||||
import * as TestSignatureValidator from '../../artifacts/TestSignatureValidator.json';
|
||||
@@ -46,6 +47,7 @@ export const artifacts = {
|
||||
TestConstants: (TestConstants as any) as ContractArtifact,
|
||||
TestLibBytes: (TestLibBytes as any) as ContractArtifact,
|
||||
TestLibs: (TestLibs as any) as ContractArtifact,
|
||||
TestExchangeInternals: (TestExchangeInternals as any) as ContractArtifact,
|
||||
TestSignatureValidator: (TestSignatureValidator as any) as ContractArtifact,
|
||||
Validator: (Validator as any) as ContractArtifact,
|
||||
Wallet: (Wallet as any) as ContractArtifact,
|
||||
|
||||
@@ -15,6 +15,14 @@ let nodeType: NodeType | undefined;
|
||||
// resolve with either a transaction receipt or a transaction hash.
|
||||
export type sendTransactionResult = Promise<TransactionReceipt | TransactionReceiptWithDecodedLogs | string>;
|
||||
|
||||
/**
|
||||
* Returns ganacheError if the backing Ethereum node is Ganache and gethError
|
||||
* if it is Geth.
|
||||
* @param ganacheError the error to be returned if the backing node is Ganache.
|
||||
* @param gethError the error to be returned if the backing node is Geth.
|
||||
* @returns either the given ganacheError or gethError depending on the backing
|
||||
* node.
|
||||
*/
|
||||
async function _getGanacheOrGethError(ganacheError: string, gethError: string): Promise<string> {
|
||||
if (_.isUndefined(nodeType)) {
|
||||
nodeType = await web3Wrapper.getNodeTypeAsync();
|
||||
@@ -41,6 +49,25 @@ async function _getContractCallFailedErrorMessageAsync(): Promise<string> {
|
||||
return _getGanacheOrGethError('revert', 'Contract call failed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the expected error message for an 'invalid opcode' resulting from a
|
||||
* contract call. The exact error message depends on the backing Ethereum node.
|
||||
*/
|
||||
export async function getInvalidOpcodeErrorMessageForCallAsync(): Promise<string> {
|
||||
return _getGanacheOrGethError('invalid opcode', 'Contract call failed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the expected error message for the given revert reason resulting from
|
||||
* a sendTransaction call. The exact error message depends on the backing
|
||||
* Ethereum node and whether it supports revert reasons.
|
||||
* @param reason a specific revert reason.
|
||||
* @returns the expected error message.
|
||||
*/
|
||||
export async function getRevertReasonOrErrorMessageForSendTransactionAsync(reason: RevertReason): Promise<string> {
|
||||
return _getGanacheOrGethError(reason, 'always failing transaction');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejects if the given Promise does not reject with an error indicating
|
||||
* insufficient funds.
|
||||
|
||||
113
packages/contracts/test/utils/combinatorial_utils.ts
Normal file
113
packages/contracts/test/utils/combinatorial_utils.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { BigNumber } from '@0xproject/utils';
|
||||
import * as combinatorics from 'js-combinatorics';
|
||||
|
||||
import { testWithReferenceFuncAsync } from './test_with_reference';
|
||||
|
||||
// A set of values corresponding to the uint256 type in Solidity. This set
|
||||
// contains some notable edge cases, including some values which will overflow
|
||||
// the uint256 type when used in different mathematical operations.
|
||||
export const uint256Values = [
|
||||
new BigNumber(0),
|
||||
new BigNumber(1),
|
||||
new BigNumber(2),
|
||||
// Non-trivial big number.
|
||||
new BigNumber(2).pow(64),
|
||||
// Max that does not overflow when squared.
|
||||
new BigNumber(2).pow(128).minus(1),
|
||||
// Min that does overflow when squared.
|
||||
new BigNumber(2).pow(128),
|
||||
// Max that does not overflow when doubled.
|
||||
new BigNumber(2).pow(255).minus(1),
|
||||
// Min that does overflow when doubled.
|
||||
new BigNumber(2).pow(255),
|
||||
// Max that does not overflow.
|
||||
new BigNumber(2).pow(256).minus(1),
|
||||
];
|
||||
|
||||
// A set of values corresponding to the bytes32 type in Solidity.
|
||||
export const bytes32Values = [
|
||||
// Min
|
||||
'0x0000000000000000000000000000000000000000000000000000000000000000',
|
||||
'0x0000000000000000000000000000000000000000000000000000000000000001',
|
||||
'0x0000000000000000000000000000000000000000000000000000000000000002',
|
||||
// Non-trivial big number.
|
||||
'0x000000000000f000000000000000000000000000000000000000000000000000',
|
||||
// Max
|
||||
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
||||
];
|
||||
|
||||
export async function testCombinatoriallyWithReferenceFuncAsync<P0, P1, R>(
|
||||
name: string,
|
||||
referenceFunc: (p0: P0, p1: P1) => Promise<R>,
|
||||
testFunc: (p0: P0, p1: P1) => Promise<R>,
|
||||
allValues: [P0[], P1[]],
|
||||
): Promise<void>;
|
||||
export async function testCombinatoriallyWithReferenceFuncAsync<P0, P1, P2, R>(
|
||||
name: string,
|
||||
referenceFunc: (p0: P0, p1: P1, p2: P2) => Promise<R>,
|
||||
testFunc: (p0: P0, p1: P1, p2: P2) => Promise<R>,
|
||||
allValues: [P0[], P1[], P2[]],
|
||||
): Promise<void>;
|
||||
export async function testCombinatoriallyWithReferenceFuncAsync<P0, P1, P2, P3, R>(
|
||||
name: string,
|
||||
referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise<R>,
|
||||
testFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise<R>,
|
||||
allValues: [P0[], P1[], P2[], P3[]],
|
||||
): Promise<void>;
|
||||
export async function testCombinatoriallyWithReferenceFuncAsync<P0, P1, P2, P3, P4, R>(
|
||||
name: string,
|
||||
referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise<R>,
|
||||
testFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise<R>,
|
||||
allValues: [P0[], P1[], P2[], P3[], P4[]],
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Uses combinatorics to test the behavior of a test function by comparing it to
|
||||
* the expected behavior (defined by a reference function) for a large number of
|
||||
* possible input values.
|
||||
*
|
||||
* First generates test cases by taking the cartesian product of the given
|
||||
* values. Each test case is a set of N values corresponding to the N arguments
|
||||
* for the test func and the reference func. For each test case, first the
|
||||
* reference function will be called to obtain an "expected result", or if the
|
||||
* reference function throws/rejects, an "expected error". Next, the test
|
||||
* function will be called to obtain an "actual result", or if the test function
|
||||
* throws/rejects, an "actual error". Each test case passes if at least one of
|
||||
* the following conditions is met:
|
||||
*
|
||||
* 1) Neither the reference function or the test function throw and the
|
||||
* "expected result" equals the "actual result".
|
||||
*
|
||||
* 2) Both the reference function and the test function throw and the "actual
|
||||
* error" message *contains* the "expected error" message.
|
||||
*
|
||||
* The first test case which does not meet one of these conditions will cause
|
||||
* the entire test to fail and this function will throw/reject.
|
||||
*
|
||||
* @param referenceFuncAsync a reference function implemented in pure
|
||||
* JavaScript/TypeScript which accepts N arguments and returns the "expected
|
||||
* result" or "expected error" for a given test case.
|
||||
* @param testFuncAsync a test function which, e.g., makes a call or sends a
|
||||
* transaction to a contract. It accepts the same N arguments returns the
|
||||
* "actual result" or "actual error" for a given test case.
|
||||
* @param values an array of N arrays. Each inner array is a set of possible
|
||||
* values which are passed into both the reference function and the test
|
||||
* function.
|
||||
* @return A Promise that resolves if the test passes and rejects if the test
|
||||
* fails, according to the rules described above.
|
||||
*/
|
||||
export async function testCombinatoriallyWithReferenceFuncAsync(
|
||||
name: string,
|
||||
referenceFuncAsync: (...args: any[]) => Promise<any>,
|
||||
testFuncAsync: (...args: any[]) => Promise<any>,
|
||||
allValues: any[],
|
||||
): Promise<void> {
|
||||
const testCases = combinatorics.cartesianProduct(...allValues);
|
||||
let counter = 0;
|
||||
testCases.forEach(async testCase => {
|
||||
counter += 1;
|
||||
it(`${name} ${counter}/${testCases.length}`, async () => {
|
||||
await testWithReferenceFuncAsync(referenceFuncAsync, testFuncAsync, testCase as any);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -46,16 +46,16 @@ chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
/**
|
||||
* Instantiates a new instance of CoreCombinatorialUtils. Since this method has some
|
||||
* Instantiates a new instance of FillOrderCombinatorialUtils. Since this method has some
|
||||
* required async setup, a factory method is required.
|
||||
* @param web3Wrapper Web3Wrapper instance
|
||||
* @param txDefaults Default Ethereum tx options
|
||||
* @return CoreCombinatorialUtils instance
|
||||
* @return FillOrderCombinatorialUtils instance
|
||||
*/
|
||||
export async function coreCombinatorialUtilsFactoryAsync(
|
||||
export async function fillOrderCombinatorialUtilsFactoryAsync(
|
||||
web3Wrapper: Web3Wrapper,
|
||||
txDefaults: Partial<TxData>,
|
||||
): Promise<CoreCombinatorialUtils> {
|
||||
): Promise<FillOrderCombinatorialUtils> {
|
||||
const accounts = await web3Wrapper.getAvailableAddressesAsync();
|
||||
const userAddresses = _.slice(accounts, 0, 5);
|
||||
const [ownerAddress, makerAddress, takerAddress] = userAddresses;
|
||||
@@ -123,7 +123,7 @@ export async function coreCombinatorialUtilsFactoryAsync(
|
||||
exchangeContract.address,
|
||||
);
|
||||
|
||||
const coreCombinatorialUtils = new CoreCombinatorialUtils(
|
||||
const fillOrderCombinatorialUtils = new FillOrderCombinatorialUtils(
|
||||
orderFactory,
|
||||
ownerAddress,
|
||||
makerAddress,
|
||||
@@ -133,10 +133,10 @@ export async function coreCombinatorialUtilsFactoryAsync(
|
||||
exchangeWrapper,
|
||||
assetWrapper,
|
||||
);
|
||||
return coreCombinatorialUtils;
|
||||
return fillOrderCombinatorialUtils;
|
||||
}
|
||||
|
||||
export class CoreCombinatorialUtils {
|
||||
export class FillOrderCombinatorialUtils {
|
||||
public orderFactory: OrderFactoryFromScenario;
|
||||
public ownerAddress: string;
|
||||
public makerAddress: string;
|
||||
@@ -240,7 +240,7 @@ export class CoreCombinatorialUtils {
|
||||
// AllowanceAmountScenario.TooLow,
|
||||
// AllowanceAmountScenario.Unlimited,
|
||||
];
|
||||
const fillScenarioArrays = CoreCombinatorialUtils._getAllCombinations([
|
||||
const fillScenarioArrays = FillOrderCombinatorialUtils._getAllCombinations([
|
||||
takerScenarios,
|
||||
feeRecipientScenarios,
|
||||
makerAssetAmountScenario,
|
||||
@@ -309,7 +309,7 @@ export class CoreCombinatorialUtils {
|
||||
} else {
|
||||
const result = [];
|
||||
const restOfArrays = arrays.slice(1);
|
||||
const allCombinationsOfRemaining = CoreCombinatorialUtils._getAllCombinations(restOfArrays); // recur with the rest of array
|
||||
const allCombinationsOfRemaining = FillOrderCombinatorialUtils._getAllCombinations(restOfArrays); // recur with the rest of array
|
||||
// tslint:disable:prefer-for-of
|
||||
for (let i = 0; i < allCombinationsOfRemaining.length; i++) {
|
||||
for (let j = 0; j < arrays[0].length; j++) {
|
||||
119
packages/contracts/test/utils/test_with_reference.ts
Normal file
119
packages/contracts/test/utils/test_with_reference.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import * as chai from 'chai';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { chaiSetup } from './chai_setup';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
export async function testWithReferenceFuncAsync<P0, R>(
|
||||
referenceFunc: (p0: P0) => Promise<R>,
|
||||
testFunc: (p0: P0) => Promise<R>,
|
||||
values: [P0],
|
||||
): Promise<void>;
|
||||
export async function testWithReferenceFuncAsync<P0, P1, R>(
|
||||
referenceFunc: (p0: P0, p1: P1) => Promise<R>,
|
||||
testFunc: (p0: P0, p1: P1) => Promise<R>,
|
||||
values: [P0, P1],
|
||||
): Promise<void>;
|
||||
export async function testWithReferenceFuncAsync<P0, P1, P2, R>(
|
||||
referenceFunc: (p0: P0, p1: P1, p2: P2) => Promise<R>,
|
||||
testFunc: (p0: P0, p1: P1, p2: P2) => Promise<R>,
|
||||
values: [P0, P1, P2],
|
||||
): Promise<void>;
|
||||
export async function testWithReferenceFuncAsync<P0, P1, P2, P3, R>(
|
||||
referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise<R>,
|
||||
testFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise<R>,
|
||||
values: [P0, P1, P2, P3],
|
||||
): Promise<void>;
|
||||
export async function testWithReferenceFuncAsync<P0, P1, P2, P3, P4, R>(
|
||||
referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise<R>,
|
||||
testFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise<R>,
|
||||
values: [P0, P1, P2, P3, P4],
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Tests the behavior of a test function by comparing it to the expected
|
||||
* behavior (defined by a reference function).
|
||||
*
|
||||
* First the reference function will be called to obtain an "expected result",
|
||||
* or if the reference function throws/rejects, an "expected error". Next, the
|
||||
* test function will be called to obtain an "actual result", or if the test
|
||||
* function throws/rejects, an "actual error". The test passes if at least one
|
||||
* of the following conditions is met:
|
||||
*
|
||||
* 1) Neither the reference function or the test function throw and the
|
||||
* "expected result" equals the "actual result".
|
||||
*
|
||||
* 2) Both the reference function and the test function throw and the "actual
|
||||
* error" message *contains* the "expected error" message.
|
||||
*
|
||||
* @param referenceFuncAsync a reference function implemented in pure
|
||||
* JavaScript/TypeScript which accepts N arguments and returns the "expected
|
||||
* result" or throws/rejects with the "expected error".
|
||||
* @param testFuncAsync a test function which, e.g., makes a call or sends a
|
||||
* transaction to a contract. It accepts the same N arguments returns the
|
||||
* "actual result" or throws/rejects with the "actual error".
|
||||
* @param values an array of N values, where each value corresponds in-order to
|
||||
* an argument to both the test function and the reference function.
|
||||
* @return A Promise that resolves if the test passes and rejects if the test
|
||||
* fails, according to the rules described above.
|
||||
*/
|
||||
export async function testWithReferenceFuncAsync(
|
||||
referenceFuncAsync: (...args: any[]) => Promise<any>,
|
||||
testFuncAsync: (...args: any[]) => Promise<any>,
|
||||
values: any[],
|
||||
): Promise<void> {
|
||||
let expectedResult: any;
|
||||
let expectedErr: string | undefined;
|
||||
try {
|
||||
expectedResult = await referenceFuncAsync(...values);
|
||||
} catch (e) {
|
||||
expectedErr = e.message;
|
||||
}
|
||||
let actualResult: any | undefined;
|
||||
try {
|
||||
actualResult = await testFuncAsync(...values);
|
||||
if (!_.isUndefined(expectedErr)) {
|
||||
throw new Error(
|
||||
`Expected error containing ${expectedErr} but got no error\n\tTest case: ${_getTestCaseString(
|
||||
referenceFuncAsync,
|
||||
values,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (_.isUndefined(expectedErr)) {
|
||||
throw new Error(`${e.message}\n\tTest case: ${_getTestCaseString(referenceFuncAsync, values)}`);
|
||||
} else {
|
||||
expect(e.message).to.contain(
|
||||
expectedErr,
|
||||
`${e.message}\n\tTest case: ${_getTestCaseString(referenceFuncAsync, values)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!_.isUndefined(actualResult) && !_.isUndefined(expectedResult)) {
|
||||
expect(actualResult).to.deep.equal(
|
||||
expectedResult,
|
||||
`Test case: ${_getTestCaseString(referenceFuncAsync, values)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function _getTestCaseString(referenceFuncAsync: (...args: any[]) => Promise<any>, values: any[]): string {
|
||||
const paramNames = _getParameterNames(referenceFuncAsync);
|
||||
return JSON.stringify(_.zipObject(paramNames, values));
|
||||
}
|
||||
|
||||
// Source: https://stackoverflow.com/questions/1007981/how-to-get-function-parameter-names-values-dynamically
|
||||
function _getParameterNames(func: (...args: any[]) => any): string[] {
|
||||
return _.toString(func)
|
||||
.replace(/[/][/].*$/gm, '') // strip single-line comments
|
||||
.replace(/\s+/g, '') // strip white space
|
||||
.replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments
|
||||
.split('){', 1)[0]
|
||||
.replace(/^[^(]*[(]/, '') // extract the parameters
|
||||
.replace(/=[^,]+/g, '') // strip any ES6 defaults
|
||||
.split(',')
|
||||
.filter(Boolean); // split & filter [""]
|
||||
}
|
||||
@@ -1,4 +1,14 @@
|
||||
[
|
||||
{
|
||||
"version": "1.0.1-rc.3",
|
||||
"changes": [
|
||||
{
|
||||
"note":
|
||||
"Updated to use latest orderFactory interface, fixed `feeRecipient` spelling error in public interface",
|
||||
"pr": 936
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.0.1-rc.2",
|
||||
"changes": [
|
||||
|
||||
@@ -61,7 +61,7 @@ export class FillScenarios {
|
||||
makerAddress: string,
|
||||
takerAddress: string,
|
||||
fillableAmount: BigNumber,
|
||||
feeRecepientAddress: string,
|
||||
feeRecipientAddress: string,
|
||||
expirationTimeSeconds?: BigNumber,
|
||||
): Promise<SignedOrder> {
|
||||
return this._createAsymmetricFillableSignedOrderWithFeesAsync(
|
||||
@@ -73,7 +73,7 @@ export class FillScenarios {
|
||||
takerAddress,
|
||||
fillableAmount,
|
||||
fillableAmount,
|
||||
feeRecepientAddress,
|
||||
feeRecipientAddress,
|
||||
expirationTimeSeconds,
|
||||
);
|
||||
}
|
||||
@@ -88,7 +88,7 @@ export class FillScenarios {
|
||||
): Promise<SignedOrder> {
|
||||
const makerFee = new BigNumber(0);
|
||||
const takerFee = new BigNumber(0);
|
||||
const feeRecepientAddress = constants.NULL_ADDRESS;
|
||||
const feeRecipientAddress = constants.NULL_ADDRESS;
|
||||
return this._createAsymmetricFillableSignedOrderWithFeesAsync(
|
||||
makerAssetData,
|
||||
takerAssetData,
|
||||
@@ -98,7 +98,7 @@ export class FillScenarios {
|
||||
takerAddress,
|
||||
makerFillableAmount,
|
||||
takerFillableAmount,
|
||||
feeRecepientAddress,
|
||||
feeRecipientAddress,
|
||||
expirationTimeSeconds,
|
||||
);
|
||||
}
|
||||
@@ -148,7 +148,7 @@ export class FillScenarios {
|
||||
takerAddress: string,
|
||||
makerFillableAmount: BigNumber,
|
||||
takerFillableAmount: BigNumber,
|
||||
feeRecepientAddress: string,
|
||||
feeRecipientAddress: string,
|
||||
expirationTimeSeconds?: BigNumber,
|
||||
): Promise<SignedOrder> {
|
||||
const decodedMakerAssetData = assetDataUtils.decodeAssetDataOrThrow(makerAssetData);
|
||||
@@ -194,17 +194,19 @@ export class FillScenarios {
|
||||
const signedOrder = await orderFactory.createSignedOrderAsync(
|
||||
this._web3Wrapper.getProvider(),
|
||||
makerAddress,
|
||||
takerAddress,
|
||||
senderAddress,
|
||||
makerFee,
|
||||
takerFee,
|
||||
makerFillableAmount,
|
||||
makerAssetData,
|
||||
takerFillableAmount,
|
||||
takerAssetData,
|
||||
this._exchangeAddress,
|
||||
feeRecepientAddress,
|
||||
expirationTimeSeconds,
|
||||
{
|
||||
takerAddress,
|
||||
senderAddress,
|
||||
makerFee,
|
||||
takerFee,
|
||||
feeRecipientAddress,
|
||||
expirationTimeSeconds,
|
||||
},
|
||||
);
|
||||
return signedOrder;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
[
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"version": "1.0.1-rc.4",
|
||||
"changes": [
|
||||
{
|
||||
"note": "Upgrade Relayer API schemas for relayer API V1"
|
||||
"note": "Change hexSchema to match `0x`",
|
||||
"pr": 937
|
||||
},
|
||||
{
|
||||
"note": "Upgrade Relayer API schemas for relayer API V2",
|
||||
"pr": 916
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ export const addressSchema = {
|
||||
export const hexSchema = {
|
||||
id: '/hexSchema',
|
||||
type: 'string',
|
||||
pattern: '^0x([0-9a-f][0-9a-f])+$',
|
||||
pattern: '^0x(([0-9a-f][0-9a-f])+)?$',
|
||||
};
|
||||
|
||||
export const numberSchema = {
|
||||
|
||||
@@ -96,7 +96,7 @@ describe('Schema', () => {
|
||||
validateAgainstSchema(testCases, hexSchema);
|
||||
});
|
||||
it('should fail for invalid hex string', () => {
|
||||
const testCases = ['0x', '0', '0xzzzzzzB11a196601eD2ce54B665CaFEca0347D42'];
|
||||
const testCases = ['0', '0xzzzzzzB11a196601eD2ce54B665CaFEca0347D42'];
|
||||
const shouldFail = true;
|
||||
validateAgainstSchema(testCases, hexSchema, shouldFail);
|
||||
});
|
||||
|
||||
@@ -44468,11 +44468,5 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"networks": {
|
||||
"50": {
|
||||
"address": "0x34d402f14d58e001d8efbe6585051bf9706aa064",
|
||||
"links": {},
|
||||
"constructorArgs": "[[\"0x5409ed021d9299bf6814279a6a1411a7e866a631\",\"0x6ecbe1db9ef729cbe972c83fb886247691fb6beb\"],[\"0x1dc4c1cefef38a777b15aa20260a54e584b16c48\",\"0x1d7022f5b17d2f8b695918fb48fa1089c9f85401\"],\"2\",\"0\"]"
|
||||
}
|
||||
}
|
||||
"networks": {}
|
||||
}
|
||||
@@ -252,11 +252,5 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"networks": {
|
||||
"50": {
|
||||
"address": "0x1dc4c1cefef38a777b15aa20260a54e584b16c48",
|
||||
"links": {},
|
||||
"constructorArgs": "[]"
|
||||
}
|
||||
}
|
||||
"networks": {}
|
||||
}
|
||||
@@ -252,11 +252,5 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"networks": {
|
||||
"50": {
|
||||
"address": "0x1d7022f5b17d2f8b695918fb48fa1089c9f85401",
|
||||
"links": {},
|
||||
"constructorArgs": "[]"
|
||||
}
|
||||
}
|
||||
"networks": {}
|
||||
}
|
||||
379
packages/migrations/artifacts/2.0.0/ERC721Token.json
vendored
379
packages/migrations/artifacts/2.0.0/ERC721Token.json
vendored
File diff suppressed because one or more lines are too long
@@ -2229,11 +2229,5 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"networks": {
|
||||
"50": {
|
||||
"address": "0x48bacb9266a570d521063ef5dd96e61686dbe788",
|
||||
"links": {},
|
||||
"constructorArgs": "[\"0xf47261b0000000000000000000000000871dd7c2b4b25e1aa18728e9d5f2af4c4e431f5c\"]"
|
||||
}
|
||||
}
|
||||
}
|
||||
"networks": {}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -326,11 +326,5 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"networks": {
|
||||
"50": {
|
||||
"address": "0x0b1ba0af832d7c05fd64161e0db78e85978e8082",
|
||||
"links": {},
|
||||
"constructorArgs": "[]"
|
||||
}
|
||||
}
|
||||
"networks": {}
|
||||
}
|
||||
@@ -10028,4 +10028,4 @@
|
||||
"constructorArgs": "[]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
[
|
||||
{
|
||||
"version": "1.0.1-rc.3",
|
||||
"changes": [
|
||||
{
|
||||
"note":
|
||||
"Added a synchronous `createOrder` method in `orderFactory`, updated public interfaces to support some optional parameters",
|
||||
"pr": 936
|
||||
},
|
||||
{
|
||||
"note": "Added marketUtils",
|
||||
"pr": 937
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "1.0.1-rc.2",
|
||||
"changes": [
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BigNumber } from '@0xproject/utils';
|
||||
|
||||
export const constants = {
|
||||
NULL_ADDRESS: '0x0000000000000000000000000000000000000000',
|
||||
NULL_BYTES: '0x',
|
||||
// tslint:disable-next-line:custom-no-magic-numbers
|
||||
UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1),
|
||||
TESTRPC_NETWORK_ID: 50,
|
||||
@@ -10,4 +11,6 @@ export const constants = {
|
||||
ERC721_ASSET_DATA_MINIMUM_BYTE_LENGTH: 53,
|
||||
SELECTOR_LENGTH: 4,
|
||||
BASE_16: 16,
|
||||
INFINITE_TIMESTAMP_SEC: new BigNumber(2524604400), // Close to infinite
|
||||
ZERO_AMOUNT: new BigNumber(0),
|
||||
};
|
||||
|
||||
@@ -13,7 +13,15 @@ export { orderFactory } from './order_factory';
|
||||
export { constants } from './constants';
|
||||
export { crypto } from './crypto';
|
||||
export { generatePseudoRandomSalt } from './salt';
|
||||
export { OrderError, MessagePrefixType, MessagePrefixOpts, EIP712Parameter, EIP712Schema, EIP712Types } from './types';
|
||||
export {
|
||||
CreateOrderOpts,
|
||||
OrderError,
|
||||
MessagePrefixType,
|
||||
MessagePrefixOpts,
|
||||
EIP712Parameter,
|
||||
EIP712Schema,
|
||||
EIP712Types,
|
||||
} from './types';
|
||||
export { AbstractBalanceAndProxyAllowanceFetcher } from './abstract/abstract_balance_and_proxy_allowance_fetcher';
|
||||
export { AbstractOrderFilledCancelledFetcher } from './abstract/abstract_order_filled_cancelled_fetcher';
|
||||
export { BalanceAndProxyAllowanceLazyStore } from './store/balance_and_proxy_allowance_lazy_store';
|
||||
@@ -24,3 +32,4 @@ export { assetDataUtils } from './asset_data_utils';
|
||||
export { EIP712Utils } from './eip712_utils';
|
||||
export { OrderValidationUtils } from './order_validation_utils';
|
||||
export { ExchangeTransferSimulator } from './exchange_transfer_simulator';
|
||||
export { marketUtils } from './market_utils';
|
||||
|
||||
133
packages/order-utils/src/market_utils.ts
Normal file
133
packages/order-utils/src/market_utils.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { schemas } from '@0xproject/json-schemas';
|
||||
import { SignedOrder } from '@0xproject/types';
|
||||
import { BigNumber } from '@0xproject/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { assert } from './assert';
|
||||
import { constants } from './constants';
|
||||
|
||||
export const marketUtils = {
|
||||
/**
|
||||
* Takes an array of orders and returns a subset of those orders that has enough makerAssetAmount (taking into account on-chain balances,
|
||||
* allowances, and partial fills) in order to fill the input makerAssetFillAmount plus slippageBufferAmount. Iterates from first order to last.
|
||||
* Sort the input by ascending rate in order to get the subset of orders that will cost the least ETH.
|
||||
* @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify the same makerAsset.
|
||||
* All orders should specify WETH as the takerAsset.
|
||||
* @param remainingFillableMakerAssetAmounts An array of BigNumbers corresponding to the signedOrders parameter.
|
||||
* You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups
|
||||
* for these values.
|
||||
* @param makerAssetFillAmount The amount of makerAsset desired to be filled.
|
||||
* @param slippageBufferAmount An additional amount of makerAsset to be covered by the result in case of trade collisions or partial fills.
|
||||
* @return Resulting orders and remaining fill amount that could not be covered by the input.
|
||||
*/
|
||||
findOrdersThatCoverMakerAssetFillAmount(
|
||||
signedOrders: SignedOrder[],
|
||||
remainingFillableMakerAssetAmounts: BigNumber[],
|
||||
makerAssetFillAmount: BigNumber,
|
||||
slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT,
|
||||
): { resultOrders: SignedOrder[]; remainingFillAmount: BigNumber } {
|
||||
assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
|
||||
_.forEach(remainingFillableMakerAssetAmounts, (amount, index) =>
|
||||
assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount),
|
||||
);
|
||||
assert.isValidBaseUnitAmount('makerAssetFillAmount', makerAssetFillAmount);
|
||||
assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount);
|
||||
assert.assert(
|
||||
signedOrders.length === remainingFillableMakerAssetAmounts.length,
|
||||
'Expected signedOrders.length to equal remainingFillableMakerAssetAmounts.length',
|
||||
);
|
||||
// calculate total amount of makerAsset needed to be filled
|
||||
const totalFillAmount = makerAssetFillAmount.plus(slippageBufferAmount);
|
||||
// iterate through the signedOrders input from left to right until we have enough makerAsset to fill totalFillAmount
|
||||
const result = _.reduce(
|
||||
signedOrders,
|
||||
({ resultOrders, remainingFillAmount }, order, index) => {
|
||||
if (remainingFillAmount.lessThanOrEqualTo(constants.ZERO_AMOUNT)) {
|
||||
return { resultOrders, remainingFillAmount: constants.ZERO_AMOUNT };
|
||||
} else {
|
||||
const makerAssetAmountAvailable = remainingFillableMakerAssetAmounts[index];
|
||||
// if there is no makerAssetAmountAvailable do not append order to resultOrders
|
||||
// if we have exceeded the total amount we want to fill set remainingFillAmount to 0
|
||||
return {
|
||||
resultOrders: makerAssetAmountAvailable.gt(constants.ZERO_AMOUNT)
|
||||
? _.concat(resultOrders, order)
|
||||
: resultOrders,
|
||||
remainingFillAmount: BigNumber.max(
|
||||
constants.ZERO_AMOUNT,
|
||||
remainingFillAmount.minus(makerAssetAmountAvailable),
|
||||
),
|
||||
};
|
||||
}
|
||||
},
|
||||
{ resultOrders: [] as SignedOrder[], remainingFillAmount: totalFillAmount },
|
||||
);
|
||||
return result;
|
||||
},
|
||||
/**
|
||||
* Takes an array of orders and an array of feeOrders. Returns a subset of the feeOrders that has enough ZRX (taking into account
|
||||
* on-chain balances, allowances, and partial fills) in order to fill the takerFees required by signedOrders plus a
|
||||
* slippageBufferAmount. Iterates from first feeOrder to last. Sort the feeOrders by ascending rate in order to get the subset of
|
||||
* feeOrders that will cost the least ETH.
|
||||
* @param signedOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as
|
||||
* the makerAsset and WETH as the takerAsset.
|
||||
* @param remainingFillableMakerAssetAmounts An array of BigNumbers corresponding to the signedOrders parameter.
|
||||
* You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups
|
||||
* for these values.
|
||||
* @param signedFeeOrders An array of objects that conform to the SignedOrder interface. All orders should specify ZRX as
|
||||
* the makerAsset and WETH as the takerAsset.
|
||||
* @param remainingFillableFeeAmounts An array of BigNumbers corresponding to the signedFeeOrders parameter.
|
||||
* You can use OrderStateUtils @0xproject/order-utils to perform blockchain lookups
|
||||
* for these values.
|
||||
* @param slippageBufferAmount An additional amount of fee to be covered by the result in case of trade collisions or partial fills.
|
||||
* @return Resulting orders and remaining fee amount that could not be covered by the input.
|
||||
*/
|
||||
findFeeOrdersThatCoverFeesForTargetOrders(
|
||||
signedOrders: SignedOrder[],
|
||||
remainingFillableMakerAssetAmounts: BigNumber[],
|
||||
signedFeeOrders: SignedOrder[],
|
||||
remainingFillableFeeAmounts: BigNumber[],
|
||||
slippageBufferAmount: BigNumber = constants.ZERO_AMOUNT,
|
||||
): { resultOrders: SignedOrder[]; remainingFeeAmount: BigNumber } {
|
||||
assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema);
|
||||
_.forEach(remainingFillableMakerAssetAmounts, (amount, index) =>
|
||||
assert.isValidBaseUnitAmount(`remainingFillableMakerAssetAmount[${index}]`, amount),
|
||||
);
|
||||
assert.doesConformToSchema('signedFeeOrders', signedFeeOrders, schemas.signedOrdersSchema);
|
||||
_.forEach(remainingFillableFeeAmounts, (amount, index) =>
|
||||
assert.isValidBaseUnitAmount(`remainingFillableFeeAmounts[${index}]`, amount),
|
||||
);
|
||||
assert.isValidBaseUnitAmount('slippageBufferAmount', slippageBufferAmount);
|
||||
assert.assert(
|
||||
signedOrders.length === remainingFillableMakerAssetAmounts.length,
|
||||
'Expected signedOrders.length to equal remainingFillableMakerAssetAmounts.length',
|
||||
);
|
||||
assert.assert(
|
||||
signedOrders.length === remainingFillableMakerAssetAmounts.length,
|
||||
'Expected signedFeeOrders.length to equal remainingFillableFeeAmounts.length',
|
||||
);
|
||||
// calculate total amount of ZRX needed to fill signedOrders
|
||||
const totalFeeAmount = _.reduce(
|
||||
signedOrders,
|
||||
(accFees, order, index) => {
|
||||
const makerAssetAmountAvailable = remainingFillableMakerAssetAmounts[index];
|
||||
const feeToFillMakerAssetAmountAvailable = makerAssetAmountAvailable
|
||||
.mul(order.takerFee)
|
||||
.div(order.makerAssetAmount);
|
||||
return accFees.plus(feeToFillMakerAssetAmountAvailable);
|
||||
},
|
||||
constants.ZERO_AMOUNT,
|
||||
);
|
||||
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
|
||||
signedFeeOrders,
|
||||
remainingFillableFeeAmounts,
|
||||
totalFeeAmount,
|
||||
slippageBufferAmount,
|
||||
);
|
||||
return {
|
||||
resultOrders,
|
||||
remainingFeeAmount: remainingFillAmount,
|
||||
};
|
||||
// TODO: add more orders here to cover rounding
|
||||
// https://github.com/0xProject/0x-protocol-specification/blob/master/v2/forwarding-contract-specification.md#over-buying-zrx
|
||||
},
|
||||
};
|
||||
@@ -1,49 +1,63 @@
|
||||
import { ECSignature, SignedOrder } from '@0xproject/types';
|
||||
import { ECSignature, Order, SignedOrder } from '@0xproject/types';
|
||||
import { BigNumber } from '@0xproject/utils';
|
||||
import { Provider } from 'ethereum-types';
|
||||
import * as ethUtil from 'ethereumjs-util';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { constants } from './constants';
|
||||
import { orderHashUtils } from './order_hash';
|
||||
import { generatePseudoRandomSalt } from './salt';
|
||||
import { ecSignOrderHashAsync } from './signature_utils';
|
||||
import { MessagePrefixType } from './types';
|
||||
import { CreateOrderOpts, MessagePrefixType } from './types';
|
||||
|
||||
export const orderFactory = {
|
||||
async createSignedOrderAsync(
|
||||
provider: Provider,
|
||||
createOrder(
|
||||
makerAddress: string,
|
||||
takerAddress: string,
|
||||
senderAddress: string,
|
||||
makerFee: BigNumber,
|
||||
takerFee: BigNumber,
|
||||
makerAssetAmount: BigNumber,
|
||||
makerAssetData: string,
|
||||
takerAssetAmount: BigNumber,
|
||||
takerAssetData: string,
|
||||
exchangeAddress: string,
|
||||
feeRecipientAddress: string,
|
||||
expirationTimeSecondsIfExists?: BigNumber,
|
||||
): Promise<SignedOrder> {
|
||||
const defaultExpirationUnixTimestampSec = new BigNumber(2524604400); // Close to infinite
|
||||
const expirationTimeSeconds = _.isUndefined(expirationTimeSecondsIfExists)
|
||||
? defaultExpirationUnixTimestampSec
|
||||
: expirationTimeSecondsIfExists;
|
||||
createOrderOpts: CreateOrderOpts = generateDefaultCreateOrderOpts(),
|
||||
): Order {
|
||||
const defaultCreateOrderOpts = generateDefaultCreateOrderOpts();
|
||||
const order = {
|
||||
makerAddress,
|
||||
takerAddress,
|
||||
senderAddress,
|
||||
makerFee,
|
||||
takerFee,
|
||||
makerAssetAmount,
|
||||
takerAssetAmount,
|
||||
makerAssetData,
|
||||
takerAssetData,
|
||||
salt: generatePseudoRandomSalt(),
|
||||
exchangeAddress,
|
||||
feeRecipientAddress,
|
||||
expirationTimeSeconds,
|
||||
takerAddress: createOrderOpts.takerAddress || defaultCreateOrderOpts.takerAddress,
|
||||
senderAddress: createOrderOpts.senderAddress || defaultCreateOrderOpts.senderAddress,
|
||||
makerFee: createOrderOpts.makerFee || defaultCreateOrderOpts.makerFee,
|
||||
takerFee: createOrderOpts.takerFee || defaultCreateOrderOpts.takerFee,
|
||||
feeRecipientAddress: createOrderOpts.feeRecipientAddress || defaultCreateOrderOpts.feeRecipientAddress,
|
||||
salt: createOrderOpts.salt || defaultCreateOrderOpts.salt,
|
||||
expirationTimeSeconds:
|
||||
createOrderOpts.expirationTimeSeconds || defaultCreateOrderOpts.expirationTimeSeconds,
|
||||
};
|
||||
return order;
|
||||
},
|
||||
async createSignedOrderAsync(
|
||||
provider: Provider,
|
||||
makerAddress: string,
|
||||
makerAssetAmount: BigNumber,
|
||||
makerAssetData: string,
|
||||
takerAssetAmount: BigNumber,
|
||||
takerAssetData: string,
|
||||
exchangeAddress: string,
|
||||
createOrderOpts?: CreateOrderOpts,
|
||||
): Promise<SignedOrder> {
|
||||
const order = orderFactory.createOrder(
|
||||
makerAddress,
|
||||
makerAssetAmount,
|
||||
makerAssetData,
|
||||
takerAssetAmount,
|
||||
takerAssetData,
|
||||
exchangeAddress,
|
||||
createOrderOpts,
|
||||
);
|
||||
const orderHash = orderHashUtils.getOrderHashHex(order);
|
||||
const messagePrefixOpts = {
|
||||
prefixType: MessagePrefixType.EthSign,
|
||||
@@ -56,6 +70,26 @@ export const orderFactory = {
|
||||
},
|
||||
};
|
||||
|
||||
function generateDefaultCreateOrderOpts(): {
|
||||
takerAddress: string;
|
||||
senderAddress: string;
|
||||
makerFee: BigNumber;
|
||||
takerFee: BigNumber;
|
||||
feeRecipientAddress: string;
|
||||
salt: BigNumber;
|
||||
expirationTimeSeconds: BigNumber;
|
||||
} {
|
||||
return {
|
||||
takerAddress: constants.NULL_ADDRESS,
|
||||
senderAddress: constants.NULL_ADDRESS,
|
||||
makerFee: constants.ZERO_AMOUNT,
|
||||
takerFee: constants.ZERO_AMOUNT,
|
||||
feeRecipientAddress: constants.NULL_ADDRESS,
|
||||
salt: generatePseudoRandomSalt(),
|
||||
expirationTimeSeconds: constants.INFINITE_TIMESTAMP_SEC,
|
||||
};
|
||||
}
|
||||
|
||||
function getVRSHexString(ecSignature: ECSignature): string {
|
||||
const ETH_SIGN_SIGNATURE_TYPE = '03';
|
||||
const vrs = `${intToHex(ecSignature.v)}${ethUtil.stripHexPrefix(ecSignature.r)}${ethUtil.stripHexPrefix(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { BigNumber } from '@0xproject/utils';
|
||||
|
||||
export enum OrderError {
|
||||
InvalidSignature = 'INVALID_SIGNATURE',
|
||||
}
|
||||
@@ -51,3 +53,13 @@ export enum EIP712Types {
|
||||
String = 'string',
|
||||
Uint256 = 'uint256',
|
||||
}
|
||||
|
||||
export interface CreateOrderOpts {
|
||||
takerAddress?: string;
|
||||
senderAddress?: string;
|
||||
makerFee?: BigNumber;
|
||||
takerFee?: BigNumber;
|
||||
feeRecipientAddress?: string;
|
||||
salt?: BigNumber;
|
||||
expirationTimeSeconds?: BigNumber;
|
||||
}
|
||||
|
||||
280
packages/order-utils/test/market_utils_test.ts
Normal file
280
packages/order-utils/test/market_utils_test.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import { BigNumber } from '@0xproject/utils';
|
||||
import * as chai from 'chai';
|
||||
import 'mocha';
|
||||
|
||||
import { constants, marketUtils } from '../src';
|
||||
|
||||
import { chaiSetup } from './utils/chai_setup';
|
||||
import { testOrderFactory } from './utils/test_order_factory';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
// tslint:disable: no-unused-expression
|
||||
describe('marketUtils', () => {
|
||||
describe('#findOrdersThatCoverMakerAssetFillAmount', () => {
|
||||
describe('no orders', () => {
|
||||
it('returns empty and unchanged remainingFillAmount', async () => {
|
||||
const fillAmount = new BigNumber(10);
|
||||
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
|
||||
[],
|
||||
[],
|
||||
fillAmount,
|
||||
);
|
||||
expect(resultOrders).to.be.empty;
|
||||
expect(remainingFillAmount).to.be.bignumber.equal(fillAmount);
|
||||
});
|
||||
});
|
||||
describe('orders are completely fillable', () => {
|
||||
// generate three signed orders each with 10 units of makerAsset, 30 total
|
||||
const makerAssetAmount = new BigNumber(10);
|
||||
const inputOrders = testOrderFactory.generateTestSignedOrders(
|
||||
{
|
||||
makerAssetAmount,
|
||||
},
|
||||
3,
|
||||
);
|
||||
// generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount
|
||||
const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount];
|
||||
it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => {
|
||||
// try to fill 20 units of makerAsset
|
||||
// include 10 units of slippageBufferAmount
|
||||
const fillAmount = new BigNumber(20);
|
||||
const slippageBufferAmount = new BigNumber(10);
|
||||
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
|
||||
inputOrders,
|
||||
remainingFillableMakerAssetAmounts,
|
||||
fillAmount,
|
||||
slippageBufferAmount,
|
||||
);
|
||||
expect(resultOrders).to.be.deep.equal(inputOrders);
|
||||
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
|
||||
});
|
||||
it('returns input orders and zero remainingFillAmount when input has more than requested fill amount', async () => {
|
||||
// try to fill 15 units of makerAsset
|
||||
// include 10 units of slippageBufferAmount
|
||||
const fillAmount = new BigNumber(15);
|
||||
const slippageBufferAmount = new BigNumber(10);
|
||||
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
|
||||
inputOrders,
|
||||
remainingFillableMakerAssetAmounts,
|
||||
fillAmount,
|
||||
slippageBufferAmount,
|
||||
);
|
||||
expect(resultOrders).to.be.deep.equal(inputOrders);
|
||||
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
|
||||
});
|
||||
it('returns input orders and non-zero remainingFillAmount when input has less than requested fill amount', async () => {
|
||||
// try to fill 30 units of makerAsset
|
||||
// include 5 units of slippageBufferAmount
|
||||
const fillAmount = new BigNumber(30);
|
||||
const slippageBufferAmount = new BigNumber(5);
|
||||
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
|
||||
inputOrders,
|
||||
remainingFillableMakerAssetAmounts,
|
||||
fillAmount,
|
||||
slippageBufferAmount,
|
||||
);
|
||||
expect(resultOrders).to.be.deep.equal(inputOrders);
|
||||
expect(remainingFillAmount).to.be.bignumber.equal(new BigNumber(5));
|
||||
});
|
||||
it('returns first order and zero remainingFillAmount when requested fill amount is exactly covered by the first order', async () => {
|
||||
// try to fill 10 units of makerAsset
|
||||
const fillAmount = new BigNumber(10);
|
||||
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
|
||||
inputOrders,
|
||||
remainingFillableMakerAssetAmounts,
|
||||
fillAmount,
|
||||
);
|
||||
expect(resultOrders).to.be.deep.equal([inputOrders[0]]);
|
||||
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
|
||||
});
|
||||
it('returns first two orders and zero remainingFillAmount when requested fill amount is over covered by the first two order', async () => {
|
||||
// try to fill 15 units of makerAsset
|
||||
const fillAmount = new BigNumber(15);
|
||||
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
|
||||
inputOrders,
|
||||
remainingFillableMakerAssetAmounts,
|
||||
fillAmount,
|
||||
);
|
||||
expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1]]);
|
||||
expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
|
||||
});
|
||||
});
|
||||
describe('orders are partially fillable', () => {
|
||||
// generate three signed orders each with 10 units of makerAsset, 30 total
|
||||
const makerAssetAmount = new BigNumber(10);
|
||||
const inputOrders = testOrderFactory.generateTestSignedOrders(
|
||||
{
|
||||
makerAssetAmount,
|
||||
},
|
||||
3,
|
||||
);
|
||||
// generate remainingFillableMakerAssetAmounts that cover different partial fill scenarios
|
||||
// 1. order is completely filled already
|
||||
// 2. order is partially fillable
|
||||
// 3. order is completely fillable
|
||||
const remainingFillableMakerAssetAmounts = [constants.ZERO_AMOUNT, new BigNumber(5), makerAssetAmount];
|
||||
it('returns last two orders and non-zero remainingFillAmount when trying to fill original makerAssetAmounts', async () => {
|
||||
// try to fill 30 units of makerAsset
|
||||
const fillAmount = new BigNumber(30);
|
||||
const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverMakerAssetFillAmount(
|
||||
inputOrders,
|
||||
remainingFillableMakerAssetAmounts,
|
||||
fillAmount,
|
||||
);
|
||||
expect(resultOrders).to.be.deep.equal([inputOrders[1], inputOrders[2]]);
|
||||
expect(remainingFillAmount).to.be.bignumber.equal(new BigNumber(15));
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#findFeeOrdersThatCoverFeesForTargetOrders', () => {
|
||||
// generate three signed fee orders each with 10 units of ZRX, 30 total
|
||||
const zrxAmount = new BigNumber(10);
|
||||
const inputFeeOrders = testOrderFactory.generateTestSignedOrders(
|
||||
{
|
||||
makerAssetAmount: zrxAmount,
|
||||
},
|
||||
3,
|
||||
);
|
||||
// generate remainingFillableFeeAmounts that equal the zrxAmount
|
||||
const remainingFillableFeeAmounts = [zrxAmount, zrxAmount, zrxAmount];
|
||||
describe('no target orders', () => {
|
||||
it('returns empty and zero remainingFeeAmount', async () => {
|
||||
const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
|
||||
[],
|
||||
[],
|
||||
inputFeeOrders,
|
||||
remainingFillableFeeAmounts,
|
||||
);
|
||||
expect(resultOrders).to.be.empty;
|
||||
expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
|
||||
});
|
||||
});
|
||||
describe('no fee orders', () => {
|
||||
// generate three signed orders each with 10 units of makerAsset, 30 total
|
||||
// each signed order requires 10 units of takerFee
|
||||
const makerAssetAmount = new BigNumber(10);
|
||||
const takerFee = new BigNumber(10);
|
||||
const inputOrders = testOrderFactory.generateTestSignedOrders(
|
||||
{
|
||||
makerAssetAmount,
|
||||
takerFee,
|
||||
},
|
||||
3,
|
||||
);
|
||||
// generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount
|
||||
const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount];
|
||||
it('returns empty and non-zero remainingFeeAmount', async () => {
|
||||
const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
|
||||
inputOrders,
|
||||
remainingFillableMakerAssetAmounts,
|
||||
[],
|
||||
[],
|
||||
);
|
||||
expect(resultOrders).to.be.empty;
|
||||
expect(remainingFeeAmount).to.be.bignumber.equal(new BigNumber(30));
|
||||
});
|
||||
});
|
||||
describe('target orders have no fees', () => {
|
||||
// generate three signed orders each with 10 units of makerAsset, 30 total
|
||||
const makerAssetAmount = new BigNumber(10);
|
||||
const inputOrders = testOrderFactory.generateTestSignedOrders(
|
||||
{
|
||||
makerAssetAmount,
|
||||
},
|
||||
3,
|
||||
);
|
||||
// generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount
|
||||
const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount];
|
||||
it('returns empty and zero remainingFeeAmount', async () => {
|
||||
const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
|
||||
inputOrders,
|
||||
remainingFillableMakerAssetAmounts,
|
||||
inputFeeOrders,
|
||||
remainingFillableFeeAmounts,
|
||||
);
|
||||
expect(resultOrders).to.be.empty;
|
||||
expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
|
||||
});
|
||||
});
|
||||
describe('target orders require fees and are completely fillable', () => {
|
||||
// generate three signed orders each with 10 units of makerAsset, 30 total
|
||||
// each signed order requires 10 units of takerFee
|
||||
const makerAssetAmount = new BigNumber(10);
|
||||
const takerFee = new BigNumber(10);
|
||||
const inputOrders = testOrderFactory.generateTestSignedOrders(
|
||||
{
|
||||
makerAssetAmount,
|
||||
takerFee,
|
||||
},
|
||||
3,
|
||||
);
|
||||
// generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount
|
||||
const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount];
|
||||
it('returns input fee orders and zero remainingFeeAmount', async () => {
|
||||
const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
|
||||
inputOrders,
|
||||
remainingFillableMakerAssetAmounts,
|
||||
inputFeeOrders,
|
||||
remainingFillableFeeAmounts,
|
||||
);
|
||||
expect(resultOrders).to.be.deep.equal(inputFeeOrders);
|
||||
expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
|
||||
});
|
||||
});
|
||||
describe('target orders require fees and are partially fillable', () => {
|
||||
// generate three signed orders each with 10 units of makerAsset, 30 total
|
||||
// each signed order requires 10 units of takerFee
|
||||
const makerAssetAmount = new BigNumber(10);
|
||||
const takerFee = new BigNumber(10);
|
||||
const inputOrders = testOrderFactory.generateTestSignedOrders(
|
||||
{
|
||||
makerAssetAmount,
|
||||
takerFee,
|
||||
},
|
||||
3,
|
||||
);
|
||||
// generate remainingFillableMakerAssetAmounts that cover different partial fill scenarios
|
||||
// 1. order is completely filled already
|
||||
// 2. order is partially fillable
|
||||
// 3. order is completely fillable
|
||||
const remainingFillableMakerAssetAmounts = [constants.ZERO_AMOUNT, new BigNumber(5), makerAssetAmount];
|
||||
it('returns first two input fee orders and zero remainingFeeAmount', async () => {
|
||||
const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
|
||||
inputOrders,
|
||||
remainingFillableMakerAssetAmounts,
|
||||
inputFeeOrders,
|
||||
remainingFillableFeeAmounts,
|
||||
);
|
||||
expect(resultOrders).to.be.deep.equal([inputFeeOrders[0], inputFeeOrders[1]]);
|
||||
expect(remainingFeeAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT);
|
||||
});
|
||||
});
|
||||
describe('target orders require more fees than available', () => {
|
||||
// generate three signed orders each with 10 units of makerAsset, 30 total
|
||||
// each signed order requires 20 units of takerFee
|
||||
const makerAssetAmount = new BigNumber(10);
|
||||
const takerFee = new BigNumber(20);
|
||||
const inputOrders = testOrderFactory.generateTestSignedOrders(
|
||||
{
|
||||
makerAssetAmount,
|
||||
takerFee,
|
||||
},
|
||||
3,
|
||||
);
|
||||
// generate remainingFillableMakerAssetAmounts that equal the makerAssetAmount
|
||||
const remainingFillableMakerAssetAmounts = [makerAssetAmount, makerAssetAmount, makerAssetAmount];
|
||||
it('returns input fee orders and non-zero remainingFeeAmount', async () => {
|
||||
const { resultOrders, remainingFeeAmount } = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(
|
||||
inputOrders,
|
||||
remainingFillableMakerAssetAmounts,
|
||||
inputFeeOrders,
|
||||
remainingFillableFeeAmounts,
|
||||
);
|
||||
expect(resultOrders).to.be.deep.equal(inputFeeOrders);
|
||||
expect(remainingFeeAmount).to.be.bignumber.equal(new BigNumber(30));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
32
packages/order-utils/test/utils/test_order_factory.ts
Normal file
32
packages/order-utils/test/utils/test_order_factory.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Order, SignedOrder } from '@0xproject/types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { constants, orderFactory } from '../../src';
|
||||
|
||||
const BASE_TEST_ORDER: Order = orderFactory.createOrder(
|
||||
constants.NULL_ADDRESS,
|
||||
constants.ZERO_AMOUNT,
|
||||
constants.NULL_ADDRESS,
|
||||
constants.ZERO_AMOUNT,
|
||||
constants.NULL_ADDRESS,
|
||||
constants.NULL_ADDRESS,
|
||||
);
|
||||
const BASE_TEST_SIGNED_ORDER: SignedOrder = {
|
||||
...BASE_TEST_ORDER,
|
||||
signature: constants.NULL_BYTES,
|
||||
};
|
||||
|
||||
export const testOrderFactory = {
|
||||
generateTestSignedOrder(partialOrder: Partial<SignedOrder>): SignedOrder {
|
||||
return transformObject(BASE_TEST_SIGNED_ORDER, partialOrder);
|
||||
},
|
||||
generateTestSignedOrders(partialOrder: Partial<SignedOrder>, numOrders: number): SignedOrder[] {
|
||||
const baseTestOrders = _.map(_.range(numOrders), () => BASE_TEST_SIGNED_ORDER);
|
||||
return _.map(baseTestOrders, order => transformObject(order, partialOrder));
|
||||
},
|
||||
};
|
||||
|
||||
function transformObject<T>(input: T, transformation: Partial<T>): T {
|
||||
const copy = _.cloneDeep(input);
|
||||
return _.assign(copy, transformation);
|
||||
}
|
||||
@@ -1,4 +1,13 @@
|
||||
[
|
||||
{
|
||||
"version": "1.0.5",
|
||||
"changes": [
|
||||
{
|
||||
"note": "Fix a bug where RelativeFSResolver would crash when trying to read a directory",
|
||||
"pr": 909
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"timestamp": 1532619515,
|
||||
"version": "1.0.4",
|
||||
|
||||
@@ -14,7 +14,7 @@ export class RelativeFSResolver extends Resolver {
|
||||
// tslint:disable-next-line:prefer-function-over-method
|
||||
public resolveIfExists(importPath: string): ContractSource | undefined {
|
||||
const filePath = path.join(this._contractsDir, importPath);
|
||||
if (fs.existsSync(filePath)) {
|
||||
if (fs.existsSync(filePath) && !fs.lstatSync(filePath).isDirectory()) {
|
||||
const fileContent = fs.readFileSync(filePath).toString();
|
||||
return {
|
||||
source: fileContent,
|
||||
|
||||
@@ -213,6 +213,7 @@ export enum RevertReason {
|
||||
ValueGreaterThanZero = 'VALUE_GREATER_THAN_ZERO',
|
||||
InvalidMsgValue = 'INVALID_MSG_VALUE',
|
||||
InsufficientEthRemaining = 'INSUFFICIENT_ETH_REMAINING',
|
||||
Uint256Overflow = 'UINT256_OVERFLOW',
|
||||
}
|
||||
|
||||
export enum StatusCodes {
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
[
|
||||
{
|
||||
"version": "1.0.5",
|
||||
"changes": [
|
||||
{
|
||||
"note": "Increased BigNumber decimal precision from 20 to 78",
|
||||
"pr": 807
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"timestamp": 1532619515,
|
||||
"version": "1.0.4",
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { BigNumber } from 'bignumber.js';
|
||||
|
||||
// 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({
|
||||
// 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
|
||||
EXPONENTIAL_AT: 1000,
|
||||
// Note(albrow): This is the lowest value for which
|
||||
// `x.div(y).floor() === x.divToInt(y)`
|
||||
// for all values of x and y <= MAX_UINT256, where MAX_UINT256 is the
|
||||
// maximum number represented by the uint256 type in Solidity (2^256-1).
|
||||
DECIMAL_PLACES: 78,
|
||||
});
|
||||
|
||||
export { BigNumber };
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"react-copy-to-clipboard": "^4.2.3",
|
||||
"react-document-title": "^2.0.3",
|
||||
"react-dom": "15.6.1",
|
||||
"react-helmet": "^5.2.0",
|
||||
"react-popper": "^1.0.0-beta.6",
|
||||
"react-redux": "^5.0.3",
|
||||
"react-router-dom": "^4.1.1",
|
||||
@@ -75,6 +76,7 @@
|
||||
"@types/react": "16.3.13",
|
||||
"@types/react-copy-to-clipboard": "^4.2.0",
|
||||
"@types/react-dom": "^16.0.3",
|
||||
"@types/react-helmet": "^5.0.6",
|
||||
"@types/react-redux": "^4.4.37",
|
||||
"@types/react-router-dom": "^4.0.4",
|
||||
"@types/react-scroll": "0.0.31",
|
||||
|
||||
160
packages/website/ts/components/inputs/allowance_state_toggle.tsx
Normal file
160
packages/website/ts/components/inputs/allowance_state_toggle.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { colors } from '@0xproject/react-shared';
|
||||
import { BigNumber, logUtils } from '@0xproject/utils';
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import ReactTooltip = require('react-tooltip');
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { AllowanceState, AllowanceStateView } from 'ts/components/ui/allowance_state_view';
|
||||
import { Container } from 'ts/components/ui/container';
|
||||
import { PointerDirection } from 'ts/components/ui/pointer';
|
||||
import { Text } from 'ts/components/ui/text';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { BalanceErrs, Token, TokenState } from 'ts/types';
|
||||
import { analytics } from 'ts/utils/analytics';
|
||||
import { errorReporter } from 'ts/utils/error_reporter';
|
||||
import { utils } from 'ts/utils/utils';
|
||||
|
||||
export interface AllowanceStateToggleProps {
|
||||
networkId: number;
|
||||
blockchain: Blockchain;
|
||||
dispatcher: Dispatcher;
|
||||
token: Token;
|
||||
tokenState: TokenState;
|
||||
userAddress: string;
|
||||
onErrorOccurred?: (errType: BalanceErrs) => void;
|
||||
refetchTokenStateAsync: () => Promise<void>;
|
||||
tooltipDirection?: PointerDirection;
|
||||
}
|
||||
|
||||
export interface AllowanceStateToggleState {
|
||||
allowanceState: AllowanceState;
|
||||
prevTokenState: TokenState;
|
||||
loadingMessage?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_ALLOWANCE_AMOUNT_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1);
|
||||
|
||||
export class AllowanceStateToggle extends React.Component<AllowanceStateToggleProps, AllowanceStateToggleState> {
|
||||
public static defaultProps = {
|
||||
onErrorOccurred: _.noop.bind(_),
|
||||
tooltipDirection: PointerDirection.Right,
|
||||
};
|
||||
private static _getAllowanceState(tokenState: TokenState): AllowanceState {
|
||||
if (!tokenState.isLoaded) {
|
||||
return AllowanceState.Loading;
|
||||
}
|
||||
if (tokenState.allowance.gt(0)) {
|
||||
return AllowanceState.Unlocked;
|
||||
}
|
||||
return AllowanceState.Locked;
|
||||
}
|
||||
constructor(props: AllowanceStateToggleProps) {
|
||||
super(props);
|
||||
const tokenState = props.tokenState;
|
||||
this.state = {
|
||||
allowanceState: AllowanceStateToggle._getAllowanceState(tokenState),
|
||||
prevTokenState: tokenState,
|
||||
};
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const tooltipId = `tooltip-id-${this.props.token.symbol}`;
|
||||
return (
|
||||
<Container cursor="pointer">
|
||||
<ReactTooltip id={tooltipId} effect="solid" offset={{ top: 3 }}>
|
||||
{this._getTooltipContent()}
|
||||
</ReactTooltip>
|
||||
<div
|
||||
data-tip={true}
|
||||
data-for={tooltipId}
|
||||
data-place={this.props.tooltipDirection}
|
||||
onClick={this._onToggleAllowanceAsync.bind(this)}
|
||||
>
|
||||
<AllowanceStateView allowanceState={this.state.allowanceState} />
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
public componentWillReceiveProps(nextProps: AllowanceStateToggleProps): void {
|
||||
const nextTokenState = nextProps.tokenState;
|
||||
const prevTokenState = this.state.prevTokenState;
|
||||
if (
|
||||
!nextTokenState.allowance.eq(prevTokenState.allowance) ||
|
||||
nextTokenState.isLoaded !== prevTokenState.isLoaded
|
||||
) {
|
||||
const tokenState = nextProps.tokenState;
|
||||
this.setState({
|
||||
prevTokenState: tokenState,
|
||||
allowanceState: AllowanceStateToggle._getAllowanceState(nextTokenState),
|
||||
});
|
||||
}
|
||||
}
|
||||
private _getTooltipContent(): React.ReactNode {
|
||||
const symbol = this.props.token.symbol;
|
||||
switch (this.state.allowanceState) {
|
||||
case AllowanceState.Loading:
|
||||
return (
|
||||
<Text noWrap={true} fontColor={colors.white}>
|
||||
{this.state.loadingMessage || 'Loading...'}
|
||||
</Text>
|
||||
);
|
||||
case AllowanceState.Locked:
|
||||
return (
|
||||
<Text noWrap={true} fontColor={colors.white}>
|
||||
Click to enable <b>{symbol}</b> for trading
|
||||
</Text>
|
||||
);
|
||||
case AllowanceState.Unlocked:
|
||||
return (
|
||||
<Text noWrap={true} fontColor={colors.white}>
|
||||
<b>{symbol}</b> is available for trading
|
||||
</Text>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
private async _onToggleAllowanceAsync(): Promise<void> {
|
||||
// Close all tooltips
|
||||
ReactTooltip.hide();
|
||||
if (this.props.userAddress === '') {
|
||||
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let newAllowanceAmountInBaseUnits = new BigNumber(0);
|
||||
if (!this._isAllowanceSet()) {
|
||||
newAllowanceAmountInBaseUnits = DEFAULT_ALLOWANCE_AMOUNT_IN_BASE_UNITS;
|
||||
}
|
||||
const isUnlockingToken = newAllowanceAmountInBaseUnits.gt(0);
|
||||
this.setState({
|
||||
allowanceState: AllowanceState.Loading,
|
||||
loadingMessage: `${isUnlockingToken ? 'Unlocking' : 'Locking'} ${this.props.token.symbol}`,
|
||||
});
|
||||
const logData = {
|
||||
tokenSymbol: this.props.token.symbol,
|
||||
newAllowance: newAllowanceAmountInBaseUnits.toNumber(),
|
||||
};
|
||||
try {
|
||||
await this.props.blockchain.setProxyAllowanceAsync(this.props.token, newAllowanceAmountInBaseUnits);
|
||||
analytics.track('Set Allowances Success', logData);
|
||||
await this.props.refetchTokenStateAsync();
|
||||
} catch (err) {
|
||||
analytics.track('Set Allowance Failure', logData);
|
||||
this.setState({
|
||||
allowanceState: AllowanceStateToggle._getAllowanceState(this.state.prevTokenState),
|
||||
});
|
||||
const errMsg = `${err}`;
|
||||
if (utils.didUserDenyWeb3Request(errMsg)) {
|
||||
return;
|
||||
}
|
||||
logUtils.log(`Unexpected error encountered: ${err}`);
|
||||
logUtils.log(err.stack);
|
||||
this.props.onErrorOccurred(BalanceErrs.allowanceSettingFailed);
|
||||
errorReporter.report(err);
|
||||
}
|
||||
}
|
||||
private _isAllowanceSet(): boolean {
|
||||
return !this.props.tokenState.allowance.eq(0);
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
import { Styles } from '@0xproject/react-shared';
|
||||
import { BigNumber, logUtils } from '@0xproject/utils';
|
||||
import * as _ from 'lodash';
|
||||
import Toggle from 'material-ui/Toggle';
|
||||
import * as React from 'react';
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { colors } from 'ts/style/colors';
|
||||
import { BalanceErrs, Token, TokenState } from 'ts/types';
|
||||
import { analytics } from 'ts/utils/analytics';
|
||||
import { errorReporter } from 'ts/utils/error_reporter';
|
||||
import { utils } from 'ts/utils/utils';
|
||||
|
||||
const DEFAULT_ALLOWANCE_AMOUNT_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1);
|
||||
|
||||
interface AllowanceToggleProps {
|
||||
networkId: number;
|
||||
blockchain: Blockchain;
|
||||
dispatcher: Dispatcher;
|
||||
token: Token;
|
||||
tokenState: TokenState;
|
||||
userAddress: string;
|
||||
isDisabled?: boolean;
|
||||
onErrorOccurred?: (errType: BalanceErrs) => void;
|
||||
refetchTokenStateAsync: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface AllowanceToggleState {
|
||||
isSpinnerVisible: boolean;
|
||||
prevAllowance: BigNumber;
|
||||
}
|
||||
|
||||
const styles: Styles = {
|
||||
baseThumbStyle: {
|
||||
height: 10,
|
||||
width: 10,
|
||||
top: 6,
|
||||
backgroundColor: colors.white,
|
||||
boxShadow: `0px 0px 0px ${colors.allowanceToggleShadow}`,
|
||||
},
|
||||
offThumbStyle: {
|
||||
left: 4,
|
||||
},
|
||||
onThumbStyle: {
|
||||
left: 25,
|
||||
},
|
||||
baseTrackStyle: {
|
||||
width: 25,
|
||||
},
|
||||
offTrackStyle: {
|
||||
backgroundColor: colors.grey300,
|
||||
},
|
||||
onTrackStyle: {
|
||||
backgroundColor: colors.mediumBlue,
|
||||
},
|
||||
};
|
||||
|
||||
export class AllowanceToggle extends React.Component<AllowanceToggleProps, AllowanceToggleState> {
|
||||
public static defaultProps = {
|
||||
onErrorOccurred: _.noop.bind(_),
|
||||
isDisabled: false,
|
||||
};
|
||||
constructor(props: AllowanceToggleProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isSpinnerVisible: false,
|
||||
prevAllowance: props.tokenState.allowance,
|
||||
};
|
||||
}
|
||||
public componentWillReceiveProps(nextProps: AllowanceToggleProps): void {
|
||||
if (!nextProps.tokenState.allowance.eq(this.state.prevAllowance)) {
|
||||
this.setState({
|
||||
isSpinnerVisible: false,
|
||||
prevAllowance: nextProps.tokenState.allowance,
|
||||
});
|
||||
}
|
||||
}
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<div className="flex">
|
||||
<div>
|
||||
<Toggle
|
||||
disabled={this.state.isSpinnerVisible || this.props.isDisabled}
|
||||
toggled={this._isAllowanceSet()}
|
||||
onToggle={this._onToggleAllowanceAsync.bind(this)}
|
||||
thumbStyle={{ ...styles.baseThumbStyle, ...styles.offThumbStyle }}
|
||||
thumbSwitchedStyle={{ ...styles.baseThumbStyle, ...styles.onThumbStyle }}
|
||||
trackStyle={{ ...styles.baseTrackStyle, ...styles.offTrackStyle }}
|
||||
trackSwitchedStyle={{ ...styles.baseTrackStyle, ...styles.onTrackStyle }}
|
||||
/>
|
||||
</div>
|
||||
{this.state.isSpinnerVisible && (
|
||||
<div className="pl1" style={{ paddingTop: 3 }}>
|
||||
<i className="zmdi zmdi-spinner zmdi-hc-spin" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private async _onToggleAllowanceAsync(): Promise<void> {
|
||||
if (this.props.userAddress === '') {
|
||||
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isSpinnerVisible: true,
|
||||
});
|
||||
|
||||
let newAllowanceAmountInBaseUnits = new BigNumber(0);
|
||||
if (!this._isAllowanceSet()) {
|
||||
newAllowanceAmountInBaseUnits = DEFAULT_ALLOWANCE_AMOUNT_IN_BASE_UNITS;
|
||||
}
|
||||
const logData = {
|
||||
tokenSymbol: this.props.token.symbol,
|
||||
newAllowance: newAllowanceAmountInBaseUnits.toNumber(),
|
||||
};
|
||||
try {
|
||||
await this.props.blockchain.setProxyAllowanceAsync(this.props.token, newAllowanceAmountInBaseUnits);
|
||||
analytics.track('Set Allowances Success', logData);
|
||||
await this.props.refetchTokenStateAsync();
|
||||
} catch (err) {
|
||||
analytics.track('Set Allowance Failure', logData);
|
||||
this.setState({
|
||||
isSpinnerVisible: false,
|
||||
});
|
||||
const errMsg = `${err}`;
|
||||
if (utils.didUserDenyWeb3Request(errMsg)) {
|
||||
return;
|
||||
}
|
||||
logUtils.log(`Unexpected error encountered: ${err}`);
|
||||
logUtils.log(err.stack);
|
||||
this.props.onErrorOccurred(BalanceErrs.allowanceSettingFailed);
|
||||
errorReporter.report(err);
|
||||
}
|
||||
}
|
||||
private _isAllowanceSet(): boolean {
|
||||
return !this.props.tokenState.allowance.eq(0);
|
||||
}
|
||||
}
|
||||
25
packages/website/ts/components/meta_tags.tsx
Normal file
25
packages/website/ts/components/meta_tags.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
export interface MetaTagsProps {
|
||||
title: string;
|
||||
description: string;
|
||||
imgSrc?: string;
|
||||
}
|
||||
|
||||
export const MetaTags: React.StatelessComponent<MetaTagsProps> = ({ title, description, imgSrc }) => (
|
||||
<Helmet>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content={imgSrc} />
|
||||
<meta name="twitter:site" content="@0xproject" />
|
||||
<meta name="twitter:image" content={imgSrc} />
|
||||
</Helmet>
|
||||
);
|
||||
|
||||
MetaTags.defaultProps = {
|
||||
imgSrc: '/images/og_image.png',
|
||||
};
|
||||
@@ -24,7 +24,7 @@ export const OnboardingTooltip: React.StatelessComponent<OnboardingTooltipProps>
|
||||
);
|
||||
};
|
||||
OnboardingTooltip.defaultProps = {
|
||||
pointerDisplay: 'left',
|
||||
pointerDisplay: PointerDirection.Left,
|
||||
};
|
||||
|
||||
OnboardingTooltip.displayName = 'OnboardingTooltip';
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
WrapEthOnboardingStep2,
|
||||
WrapEthOnboardingStep3,
|
||||
} from 'ts/components/onboarding/wrap_eth_onboarding_step';
|
||||
import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle';
|
||||
import { AllowanceStateToggle } from 'ts/containers/inputs/allowance_state_toggle';
|
||||
import { BrowserType, ProviderType, ScreenWidths, Token, TokenByAddress, TokenStateByAddress } from 'ts/types';
|
||||
import { analytics } from 'ts/utils/analytics';
|
||||
import { utils } from 'ts/utils/utils';
|
||||
@@ -149,8 +149,8 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp
|
||||
title: 'Step 3: Unlock Tokens',
|
||||
content: (
|
||||
<SetAllowancesOnboardingStep
|
||||
zrxAllowanceToggle={this._renderZrxAllowanceToggle()}
|
||||
ethAllowanceToggle={this._renderEthAllowanceToggle()}
|
||||
zrxAllowanceToggle={this._renderZrxAllowanceStateToggle()}
|
||||
ethAllowanceToggle={this._renderEthAllowanceStateToggle()}
|
||||
doesUserHaveAllowancesForWethAndZrx={this._doesUserHaveAllowancesForWethAndZrx()}
|
||||
/>
|
||||
),
|
||||
@@ -243,15 +243,15 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp
|
||||
stepIndex: this.props.stepIndex,
|
||||
});
|
||||
}
|
||||
private _renderZrxAllowanceToggle(): React.ReactNode {
|
||||
private _renderZrxAllowanceStateToggle(): React.ReactNode {
|
||||
const zrxToken = utils.getZrxToken(this.props.tokenByAddress);
|
||||
return this._renderAllowanceToggle(zrxToken);
|
||||
return this._renderAllowanceStateToggle(zrxToken);
|
||||
}
|
||||
private _renderEthAllowanceToggle(): React.ReactNode {
|
||||
private _renderEthAllowanceStateToggle(): React.ReactNode {
|
||||
const ethToken = utils.getEthToken(this.props.tokenByAddress);
|
||||
return this._renderAllowanceToggle(ethToken);
|
||||
return this._renderAllowanceStateToggle(ethToken);
|
||||
}
|
||||
private _renderAllowanceToggle(token: Token): React.ReactNode {
|
||||
private _renderAllowanceStateToggle(token: Token): React.ReactNode {
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
@@ -260,10 +260,9 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<AllowanceToggle
|
||||
<AllowanceStateToggle
|
||||
token={token}
|
||||
tokenState={tokenStateIfExists}
|
||||
isDisabled={!tokenStateIfExists.isLoaded}
|
||||
blockchain={this.props.blockchain}
|
||||
// tslint:disable-next-line:jsx-no-lambda
|
||||
refetchTokenStateAsync={async () => this.props.refetchTokenStateAsync(token.address)}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { PortalDisclaimerDialog } from 'ts/components/dialogs/portal_disclaimer_
|
||||
import { EthWrappers } from 'ts/components/eth_wrappers';
|
||||
import { FillOrder } from 'ts/components/fill_order';
|
||||
import { AssetPicker } from 'ts/components/generate_order/asset_picker';
|
||||
import { MetaTags } from 'ts/components/meta_tags';
|
||||
import { BackButton } from 'ts/components/portal/back_button';
|
||||
import { Loading } from 'ts/components/portal/loading';
|
||||
import { Menu, MenuTheme } from 'ts/components/portal/menu';
|
||||
@@ -24,6 +25,7 @@ import { TradeHistory } from 'ts/components/trade_history/trade_history';
|
||||
import { Container } from 'ts/components/ui/container';
|
||||
import { FlashMessage } from 'ts/components/ui/flash_message';
|
||||
import { Image } from 'ts/components/ui/image';
|
||||
import { PointerDirection } from 'ts/components/ui/pointer';
|
||||
import { Text } from 'ts/components/ui/text';
|
||||
import { Wallet } from 'ts/components/wallet/wallet';
|
||||
import { GenerateOrderForm } from 'ts/containers/generate_order_form';
|
||||
@@ -107,6 +109,8 @@ const LEFT_COLUMN_WIDTH = 346;
|
||||
const MENU_PADDING_LEFT = 185;
|
||||
const LARGE_LAYOUT_MAX_WIDTH = 1200;
|
||||
const SIDE_PADDING = 20;
|
||||
const DOCUMENT_TITLE = '0x Portal';
|
||||
const DOCUMENT_DESCRIPTION = 'Learn about and trade on 0x Relayers';
|
||||
|
||||
export class Portal extends React.Component<PortalProps, PortalState> {
|
||||
private _blockchain: Blockchain;
|
||||
@@ -225,7 +229,8 @@ export class Portal extends React.Component<PortalProps, PortalState> {
|
||||
: TokenVisibility.TRACKED;
|
||||
return (
|
||||
<Container>
|
||||
<DocumentTitle title="0x Portal" />
|
||||
<MetaTags title={DOCUMENT_TITLE} description={DOCUMENT_DESCRIPTION} />
|
||||
<DocumentTitle title={DOCUMENT_TITLE} />
|
||||
<TopBar
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
@@ -355,6 +360,9 @@ export class Portal extends React.Component<PortalProps, PortalState> {
|
||||
onAddToken={this._onAddToken.bind(this)}
|
||||
onRemoveToken={this._onRemoveToken.bind(this)}
|
||||
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this)}
|
||||
toggleTooltipDirection={
|
||||
this.props.isPortalOnboardingShowing ? PointerDirection.Left : PointerDirection.Right
|
||||
}
|
||||
/>
|
||||
</Container>
|
||||
{!isMobile && <Container marginTop="8px">{this._renderStartOnboarding()}</Container>}
|
||||
|
||||
@@ -24,7 +24,7 @@ import { SendButton } from 'ts/components/send_button';
|
||||
import { HelpTooltip } from 'ts/components/ui/help_tooltip';
|
||||
import { LifeCycleRaisedButton } from 'ts/components/ui/lifecycle_raised_button';
|
||||
import { TokenIcon } from 'ts/components/ui/token_icon';
|
||||
import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle';
|
||||
import { AllowanceStateToggle } from 'ts/containers/inputs/allowance_state_toggle';
|
||||
import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import {
|
||||
@@ -372,14 +372,15 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
)}
|
||||
</TableRowColumn>
|
||||
<TableRowColumn>
|
||||
<AllowanceToggle
|
||||
blockchain={this.props.blockchain}
|
||||
token={token}
|
||||
tokenState={tokenState}
|
||||
onErrorOccurred={this._onErrorOccurred.bind(this)}
|
||||
isDisabled={!tokenState.isLoaded}
|
||||
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)}
|
||||
/>
|
||||
<div className="flex justify-center">
|
||||
<AllowanceStateToggle
|
||||
blockchain={this.props.blockchain}
|
||||
token={token}
|
||||
tokenState={tokenState}
|
||||
onErrorOccurred={this._onErrorOccurred.bind(this)}
|
||||
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)}
|
||||
/>
|
||||
</div>
|
||||
</TableRowColumn>
|
||||
{utils.isTestNetwork(this.props.networkId) && (
|
||||
<TableRowColumn style={{ paddingLeft: actionPaddingX, paddingRight: actionPaddingX }}>
|
||||
|
||||
51
packages/website/ts/components/ui/allowance_state_view.tsx
Normal file
51
packages/website/ts/components/ui/allowance_state_view.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { colors } from '@0xproject/react-shared';
|
||||
import * as React from 'react';
|
||||
import { Container } from 'ts/components/ui/container';
|
||||
import { Spinner } from 'ts/components/ui/spinner';
|
||||
|
||||
export enum AllowanceState {
|
||||
Locked,
|
||||
Unlocked,
|
||||
Loading,
|
||||
}
|
||||
|
||||
export interface AllowanceStateViewProps {
|
||||
allowanceState: AllowanceState;
|
||||
}
|
||||
|
||||
export const AllowanceStateView: React.StatelessComponent<AllowanceStateViewProps> = ({ allowanceState }) => {
|
||||
switch (allowanceState) {
|
||||
case AllowanceState.Locked:
|
||||
return renderLock();
|
||||
case AllowanceState.Unlocked:
|
||||
return renderCheck();
|
||||
case AllowanceState.Loading:
|
||||
return (
|
||||
<Container position="relative" top="3px" left="5px">
|
||||
<Spinner size={18} strokeSize={2} />
|
||||
</Container>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const renderCheck = (color: string = colors.lightGreen) => (
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8.5" cy="8.5" r="8.5" fill={color} />
|
||||
<path
|
||||
d="M2.5 4.5L1.79289 5.20711L2.5 5.91421L3.20711 5.20711L2.5 4.5ZM-0.707107 2.70711L1.79289 5.20711L3.20711 3.79289L0.707107 1.29289L-0.707107 2.70711ZM3.20711 5.20711L7.70711 0.707107L6.29289 -0.707107L1.79289 3.79289L3.20711 5.20711Z"
|
||||
transform="translate(5 6.5)"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const renderLock = () => (
|
||||
<svg width="12" height="15" viewBox="0 0 12 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M6 0C3.51604 0 1.48688 2.0495 1.48688 4.55837V5.86581C0.664723 5.86581 -3.33647e-08 6.53719 -3.33647e-08 7.36759V13.3217C-3.33647e-08 14.1521 0.664723 14.8235 1.48688 14.8235H10.5131C11.3353 14.8235 12 14.1521 12 13.3217V7.36759C12 6.53719 11.3353 5.86581 10.5131 5.86581V4.55837C10.5131 2.0495 8.48396 0 6 0ZM8.93878 5.86581H3.06122V4.55837C3.06122 2.9329 4.37318 1.59013 6 1.59013C7.62682 1.59013 8.93878 2.9329 8.93878 4.55837V5.86581Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -32,8 +32,10 @@ export interface ContainerProps {
|
||||
bottom?: string;
|
||||
zIndex?: number;
|
||||
Tag?: ContainerTag;
|
||||
cursor?: string;
|
||||
id?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
overflowX?: 'scroll' | 'hidden' | 'auto' | 'visible';
|
||||
}
|
||||
|
||||
export const Container: React.StatelessComponent<ContainerProps> = props => {
|
||||
|
||||
@@ -2,7 +2,12 @@ import { colors } from '@0xproject/react-shared';
|
||||
import * as React from 'react';
|
||||
import { styled } from 'ts/style/theme';
|
||||
|
||||
export type PointerDirection = 'top' | 'right' | 'bottom' | 'left';
|
||||
export enum PointerDirection {
|
||||
Top = 'top',
|
||||
Right = 'right',
|
||||
Bottom = 'bottom',
|
||||
Left = 'left',
|
||||
}
|
||||
|
||||
export interface PointerProps {
|
||||
className?: string;
|
||||
|
||||
54
packages/website/ts/components/ui/spinner.tsx
Normal file
54
packages/website/ts/components/ui/spinner.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { colors } from '@0xproject/react-shared';
|
||||
import * as React from 'react';
|
||||
import { styled } from 'ts/style/theme';
|
||||
|
||||
import { dash, rotate } from 'ts/style/keyframes';
|
||||
|
||||
interface SpinnerSvgProps {
|
||||
color: string;
|
||||
size: number;
|
||||
viewBox?: string;
|
||||
}
|
||||
|
||||
const SpinnerSvg: React.StatelessComponent<SpinnerSvgProps> = props => <svg {...props} />;
|
||||
|
||||
const StyledSpinner = styled(SpinnerSvg)`
|
||||
animation: ${rotate} 3s linear infinite;
|
||||
margin: ${props => `-${props.size / 2}px 0 0 -${props.size / 2}px`};
|
||||
margin-top: ${props => `-${props.size / 2}px`};
|
||||
margin-left: ${props => `-${props.size / 2}px`};
|
||||
margin-bottom: 0px;
|
||||
margin-right: 0px;
|
||||
size: ${props => `${props.size}px`};
|
||||
height: ${props => `${props.size}px`};
|
||||
|
||||
& .path {
|
||||
stroke: ${props => props.color};
|
||||
stroke-linecap: round;
|
||||
animation: ${dash} 2.5s ease-in-out infinite;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface SpinnerProps {
|
||||
size?: number;
|
||||
strokeSize?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export const Spinner: React.StatelessComponent<SpinnerProps> = ({ size, strokeSize, color }) => {
|
||||
const c = size / 2;
|
||||
const r = c - strokeSize;
|
||||
return (
|
||||
<StyledSpinner color={color} size={size} viewBox={`0 0 ${size} ${size}`}>
|
||||
<circle className="path" cx={c} cy={c} r={r} fill="none" strokeWidth={strokeSize} />
|
||||
</StyledSpinner>
|
||||
);
|
||||
};
|
||||
|
||||
Spinner.defaultProps = {
|
||||
size: 50,
|
||||
color: colors.mediumBlue,
|
||||
strokeSize: 4,
|
||||
};
|
||||
|
||||
Spinner.displayName = 'Spinner';
|
||||
@@ -19,6 +19,7 @@ export interface TextProps {
|
||||
textDecorationLine?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
hoverColor?: string;
|
||||
noWrap?: boolean;
|
||||
}
|
||||
|
||||
const PlainText: React.StatelessComponent<TextProps> = ({ children, className, onClick, Tag }) => (
|
||||
@@ -39,6 +40,7 @@ export const Text = styled(PlainText)`
|
||||
${props => (props.minHeight ? `min-height: ${props.minHeight}` : '')};
|
||||
${props => (props.onClick ? 'cursor: pointer' : '')};
|
||||
transition: color 0.5s ease;
|
||||
${props => (props.noWrap ? 'white-space: nowrap' : '')};
|
||||
&:hover {
|
||||
${props => (props.onClick ? `color: ${props.hoverColor || darken(0.3, props.fontColor)}` : '')};
|
||||
}
|
||||
@@ -53,6 +55,7 @@ Text.defaultProps = {
|
||||
lineHeight: '1.5em',
|
||||
textDecorationLine: 'none',
|
||||
Tag: 'div',
|
||||
noWrap: false,
|
||||
};
|
||||
|
||||
Text.displayName = 'Text';
|
||||
|
||||
@@ -14,6 +14,7 @@ import { DropDown, DropdownMouseEvent } from 'ts/components/ui/drop_down';
|
||||
import { IconButton } from 'ts/components/ui/icon_button';
|
||||
import { Identicon } from 'ts/components/ui/identicon';
|
||||
import { Island } from 'ts/components/ui/island';
|
||||
import { PointerDirection } from 'ts/components/ui/pointer';
|
||||
import {
|
||||
CopyAddressSimpleMenuItem,
|
||||
DifferentWalletSimpleMenuItem,
|
||||
@@ -28,7 +29,7 @@ import { NullTokenRow } from 'ts/components/wallet/null_token_row';
|
||||
import { PlaceHolder } from 'ts/components/wallet/placeholder';
|
||||
import { StandardIconRow } from 'ts/components/wallet/standard_icon_row';
|
||||
import { WrapEtherItem } from 'ts/components/wallet/wrap_ether_item';
|
||||
import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle';
|
||||
import { AllowanceStateToggle } from 'ts/containers/inputs/allowance_state_toggle';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { colors } from 'ts/style/colors';
|
||||
import {
|
||||
@@ -67,6 +68,7 @@ export interface WalletProps {
|
||||
onRemoveToken: () => void;
|
||||
refetchTokenStateAsync: (tokenAddress: string) => Promise<void>;
|
||||
style: React.CSSProperties;
|
||||
toggleTooltipDirection?: PointerDirection;
|
||||
}
|
||||
|
||||
interface WalletState {
|
||||
@@ -74,14 +76,14 @@ interface WalletState {
|
||||
isHoveringSidebar: boolean;
|
||||
}
|
||||
|
||||
interface AllowanceToggleConfig {
|
||||
interface AllowanceStateToggleConfig {
|
||||
token: Token;
|
||||
tokenState: TokenState;
|
||||
}
|
||||
|
||||
interface AccessoryItemConfig {
|
||||
wrappedEtherDirection?: Side;
|
||||
allowanceToggleConfig?: AllowanceToggleConfig;
|
||||
allowanceStateToggleConfig?: AllowanceStateToggleConfig;
|
||||
}
|
||||
|
||||
const ETHER_ICON_PATH = '/images/ether.png';
|
||||
@@ -89,7 +91,8 @@ const ICON_DIMENSION = 28;
|
||||
const BODY_ITEM_KEY = 'BODY';
|
||||
const HEADER_ITEM_KEY = 'HEADER';
|
||||
const ETHER_ITEM_KEY = 'ETHER';
|
||||
const NO_ALLOWANCE_TOGGLE_SPACE_WIDTH = 56;
|
||||
const WRAP_ROW_ALLOWANCE_TOGGLE_WIDTH = 67;
|
||||
const ALLOWANCE_TOGGLE_WIDTH = 56;
|
||||
const PLACEHOLDER_COLOR = colors.grey300;
|
||||
const LOADING_ROWS_COUNT = 6;
|
||||
|
||||
@@ -338,7 +341,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
|
||||
);
|
||||
const accessoryItemConfig: AccessoryItemConfig = {
|
||||
wrappedEtherDirection,
|
||||
allowanceToggleConfig: {
|
||||
allowanceStateToggleConfig: {
|
||||
token,
|
||||
tokenState,
|
||||
},
|
||||
@@ -393,13 +396,15 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
|
||||
}
|
||||
private _renderAccessoryItems(config: AccessoryItemConfig): React.ReactElement<{}> {
|
||||
const shouldShowWrappedEtherAction = !_.isUndefined(config.wrappedEtherDirection);
|
||||
const shouldShowToggle = !_.isUndefined(config.allowanceToggleConfig);
|
||||
const shouldShowToggle = !_.isUndefined(config.allowanceStateToggleConfig);
|
||||
// if we don't have a toggle, we still want some space to the right of the "wrap" button so that it aligns with
|
||||
// the "unwrap" button in the row below
|
||||
const toggle = shouldShowToggle ? (
|
||||
this._renderAllowanceToggle(config.allowanceToggleConfig)
|
||||
) : (
|
||||
<div style={{ width: NO_ALLOWANCE_TOGGLE_SPACE_WIDTH }} />
|
||||
const isWrapEtherRow = shouldShowWrappedEtherAction && config.wrappedEtherDirection === Side.Deposit;
|
||||
const width = isWrapEtherRow ? WRAP_ROW_ALLOWANCE_TOGGLE_WIDTH : ALLOWANCE_TOGGLE_WIDTH;
|
||||
const toggle = (
|
||||
<Container className="flex justify-center" width={width}>
|
||||
{shouldShowToggle && this._renderAllowanceToggle(config.allowanceStateToggleConfig)}
|
||||
</Container>
|
||||
);
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
@@ -410,14 +415,14 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private _renderAllowanceToggle(config: AllowanceToggleConfig): React.ReactNode {
|
||||
private _renderAllowanceToggle(config: AllowanceStateToggleConfig): React.ReactNode {
|
||||
// TODO: Error handling
|
||||
return (
|
||||
<AllowanceToggle
|
||||
<AllowanceStateToggle
|
||||
blockchain={this.props.blockchain}
|
||||
token={config.token}
|
||||
tokenState={config.tokenState}
|
||||
isDisabled={!config.tokenState.isLoaded}
|
||||
tooltipDirection={this.props.toggleTooltipDirection}
|
||||
refetchTokenStateAsync={async () => this.props.refetchTokenStateAsync(config.token.address)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,19 +2,20 @@ import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { PointerDirection } from 'ts/components/ui/pointer';
|
||||
import { State } from 'ts/redux/reducer';
|
||||
import { BalanceErrs, Token, TokenState } from 'ts/types';
|
||||
|
||||
import { AllowanceToggle as AllowanceToggleComponent } from 'ts/components/inputs/allowance_toggle';
|
||||
import { AllowanceStateToggle as AllowanceStateToggleComponent } from 'ts/components/inputs/allowance_state_toggle';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
|
||||
interface AllowanceToggleProps {
|
||||
interface AllowanceStateToggleProps {
|
||||
blockchain: Blockchain;
|
||||
onErrorOccurred?: (errType: BalanceErrs) => void;
|
||||
token: Token;
|
||||
tokenState: TokenState;
|
||||
isDisabled?: boolean;
|
||||
refetchTokenStateAsync: () => Promise<void>;
|
||||
tooltipDirection?: PointerDirection;
|
||||
}
|
||||
|
||||
interface ConnectedState {
|
||||
@@ -26,7 +27,7 @@ interface ConnectedDispatch {
|
||||
dispatcher: Dispatcher;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: State, _ownProps: AllowanceToggleProps): ConnectedState => ({
|
||||
const mapStateToProps = (state: State, _ownProps: AllowanceStateToggleProps): ConnectedState => ({
|
||||
networkId: state.networkId,
|
||||
userAddress: state.userAddress,
|
||||
});
|
||||
@@ -35,7 +36,7 @@ const mapDispatchTopProps = (dispatch: Dispatch<State>): ConnectedDispatch => ({
|
||||
dispatcher: new Dispatcher(dispatch),
|
||||
});
|
||||
|
||||
export const AllowanceToggle: React.ComponentClass<AllowanceToggleProps> = connect(
|
||||
export const AllowanceStateToggle: React.ComponentClass<AllowanceStateToggleProps> = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchTopProps,
|
||||
)(AllowanceToggleComponent);
|
||||
)(AllowanceStateToggleComponent);
|
||||
@@ -4,6 +4,7 @@ import { render } from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter as Router, Redirect, Route, Switch } from 'react-router-dom';
|
||||
import * as injectTapEventPlugin from 'react-tap-event-plugin';
|
||||
import { MetaTags } from 'ts/components/meta_tags';
|
||||
import { About } from 'ts/containers/about';
|
||||
import { FAQ } from 'ts/containers/faq';
|
||||
import { Jobs } from 'ts/containers/jobs';
|
||||
@@ -65,73 +66,85 @@ const LazyEthereumTypesDocumentation = createLazyComponent('Documentation', asyn
|
||||
System.import<any>(/* webpackChunkName: "ethereumTypesDocs" */ 'ts/containers/ethereum_types_documentation'),
|
||||
);
|
||||
|
||||
render(
|
||||
<Router>
|
||||
<div>
|
||||
<MuiThemeProvider muiTheme={muiTheme}>
|
||||
<Provider store={store}>
|
||||
<div>
|
||||
<Switch>
|
||||
<Route exact={true} path="/" component={Landing as any} />
|
||||
<Redirect from="/otc" to={`${WebsitePaths.Portal}`} />
|
||||
<Route path={WebsitePaths.Careers} component={Jobs as any} />
|
||||
<Route path={WebsitePaths.Portal} component={LazyPortal} />
|
||||
<Route path={WebsitePaths.FAQ} component={FAQ as any} />
|
||||
<Route path={WebsitePaths.About} component={About as any} />
|
||||
<Route path={WebsitePaths.Wiki} component={Wiki as any} />
|
||||
<Route path={`${WebsitePaths.ZeroExJs}/:version?`} component={LazyZeroExJSDocumentation} />
|
||||
<Route path={`${WebsitePaths.Connect}/:version?`} component={LazyConnectDocumentation} />
|
||||
<Route
|
||||
path={`${WebsitePaths.SolCompiler}/:version?`}
|
||||
component={LazySolCompilerDocumentation}
|
||||
/>
|
||||
<Route path={`${WebsitePaths.SolCov}/:version?`} component={LazySolCovDocumentation} />
|
||||
<Route
|
||||
path={`${WebsitePaths.JSONSchemas}/:version?`}
|
||||
component={LazyJSONSchemasDocumentation}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsitePaths.Subproviders}/:version?`}
|
||||
component={LazySubprovidersDocumentation}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsitePaths.OrderUtils}/:version?`}
|
||||
component={LazyOrderUtilsDocumentation}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsitePaths.Web3Wrapper}/:version?`}
|
||||
component={LazyWeb3WrapperDocumentation}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsitePaths.SmartContracts}/:version?`}
|
||||
component={LazySmartContractsDocumentation}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsitePaths.EthereumTypes}/:version?`}
|
||||
component={LazyEthereumTypesDocumentation}
|
||||
/>
|
||||
const DOCUMENT_TITLE = '0x: The Protocol for Trading Tokens';
|
||||
const DOCUMENT_DESCRIPTION = 'An Open Protocol For Decentralized Exchange On The Ethereum Blockchain';
|
||||
|
||||
{/* Legacy endpoints */}
|
||||
<Route
|
||||
path={`${WebsiteLegacyPaths.ZeroExJs}/:version?`}
|
||||
component={LazyZeroExJSDocumentation}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsiteLegacyPaths.Web3Wrapper}/:version?`}
|
||||
component={LazyWeb3WrapperDocumentation}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsiteLegacyPaths.Deployer}/:version?`}
|
||||
component={LazySolCompilerDocumentation}
|
||||
/>
|
||||
<Route path={WebsiteLegacyPaths.Jobs} component={Jobs as any} />
|
||||
<Route path={`${WebsitePaths.Docs}`} component={LazyZeroExJSDocumentation} />
|
||||
<Route component={NotFound as any} />
|
||||
</Switch>
|
||||
</div>
|
||||
</Provider>
|
||||
</MuiThemeProvider>
|
||||
</div>
|
||||
</Router>,
|
||||
render(
|
||||
<div>
|
||||
<MetaTags title={DOCUMENT_TITLE} description={DOCUMENT_DESCRIPTION} />
|
||||
<Router>
|
||||
<div>
|
||||
<MuiThemeProvider muiTheme={muiTheme}>
|
||||
<Provider store={store}>
|
||||
<div>
|
||||
<Switch>
|
||||
<Route exact={true} path="/" component={Landing as any} />
|
||||
<Redirect from="/otc" to={`${WebsitePaths.Portal}`} />
|
||||
<Route path={WebsitePaths.Careers} component={Jobs as any} />
|
||||
<Route path={WebsitePaths.Portal} component={LazyPortal} />
|
||||
<Route path={WebsitePaths.FAQ} component={FAQ as any} />
|
||||
<Route path={WebsitePaths.About} component={About as any} />
|
||||
<Route path={WebsitePaths.Wiki} component={Wiki as any} />
|
||||
<Route
|
||||
path={`${WebsitePaths.ZeroExJs}/:version?`}
|
||||
component={LazyZeroExJSDocumentation}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsitePaths.Connect}/:version?`}
|
||||
component={LazyConnectDocumentation}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsitePaths.SolCompiler}/:version?`}
|
||||
component={LazySolCompilerDocumentation}
|
||||
/>
|
||||
<Route path={`${WebsitePaths.SolCov}/:version?`} component={LazySolCovDocumentation} />
|
||||
<Route
|
||||
path={`${WebsitePaths.JSONSchemas}/:version?`}
|
||||
component={LazyJSONSchemasDocumentation}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsitePaths.Subproviders}/:version?`}
|
||||
component={LazySubprovidersDocumentation}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsitePaths.OrderUtils}/:version?`}
|
||||
component={LazyOrderUtilsDocumentation}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsitePaths.Web3Wrapper}/:version?`}
|
||||
component={LazyWeb3WrapperDocumentation}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsitePaths.SmartContracts}/:version?`}
|
||||
component={LazySmartContractsDocumentation}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsitePaths.EthereumTypes}/:version?`}
|
||||
component={LazyEthereumTypesDocumentation}
|
||||
/>
|
||||
|
||||
{/* Legacy endpoints */}
|
||||
<Route
|
||||
path={`${WebsiteLegacyPaths.ZeroExJs}/:version?`}
|
||||
component={LazyZeroExJSDocumentation}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsiteLegacyPaths.Web3Wrapper}/:version?`}
|
||||
component={LazyWeb3WrapperDocumentation}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsiteLegacyPaths.Deployer}/:version?`}
|
||||
component={LazySolCompilerDocumentation}
|
||||
/>
|
||||
<Route path={WebsiteLegacyPaths.Jobs} component={Jobs as any} />
|
||||
<Route path={`${WebsitePaths.Docs}`} component={LazyZeroExJSDocumentation} />
|
||||
<Route component={NotFound as any} />
|
||||
</Switch>
|
||||
</div>
|
||||
</Provider>
|
||||
</MuiThemeProvider>
|
||||
</div>
|
||||
</Router>
|
||||
</div>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
|
||||
@@ -4,7 +4,9 @@ import * as React from 'react';
|
||||
import * as DocumentTitle from 'react-document-title';
|
||||
|
||||
import { Footer } from 'ts/components/footer';
|
||||
import { MetaTags } from 'ts/components/meta_tags';
|
||||
import { TopBar } from 'ts/components/top_bar/top_bar';
|
||||
import { Container } from 'ts/components/ui/container';
|
||||
import { Benefits } from 'ts/pages/jobs/benefits';
|
||||
import { Join0x } from 'ts/pages/jobs/join_0x';
|
||||
import { Mission } from 'ts/pages/jobs/mission';
|
||||
@@ -16,6 +18,8 @@ import { utils } from 'ts/utils/utils';
|
||||
|
||||
const OPEN_POSITIONS_HASH = 'positions';
|
||||
const THROTTLE_TIMEOUT = 100;
|
||||
const DOCUMENT_TITLE = 'Careers at 0x';
|
||||
const DOCUMENT_DESCRIPTION = 'Join 0x in creating a tokenized world where all value can flow freely';
|
||||
|
||||
export interface JobsProps {
|
||||
location: Location;
|
||||
@@ -39,8 +43,9 @@ export class Jobs extends React.Component<JobsProps, JobsState> {
|
||||
}
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<div>
|
||||
<DocumentTitle title="Careers at 0x" />
|
||||
<Container overflowX="hidden">
|
||||
<MetaTags title={DOCUMENT_TITLE} description={DOCUMENT_DESCRIPTION} />
|
||||
<DocumentTitle title={DOCUMENT_TITLE} />
|
||||
<TopBar
|
||||
blockchainIsLoaded={false}
|
||||
location={this.props.location}
|
||||
@@ -52,7 +57,7 @@ export class Jobs extends React.Component<JobsProps, JobsState> {
|
||||
<Benefits screenWidth={this.props.screenWidth} />
|
||||
<OpenPositions hash={OPEN_POSITIONS_HASH} screenWidth={this.props.screenWidth} />
|
||||
<Footer translate={this.props.translate} dispatcher={this.props.dispatcher} />
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
private _onJoin0xCallToActionClick(): void {
|
||||
|
||||
@@ -20,10 +20,10 @@ export const Join0x = (props: Join0xProps) => (
|
||||
className="mx-auto inline-block align-middle py4"
|
||||
style={{ lineHeight: '44px', textAlign: 'center', position: 'relative' }}
|
||||
>
|
||||
<Container className="sm-hide xs-hide md-hide" position="absolute" left="100%" marginLeft="80px">
|
||||
<Container className="sm-hide xs-hide" position="absolute" left="100%" marginLeft="80px">
|
||||
<Image src="images/jobs/hero-dots-right.svg" width="400px" />
|
||||
</Container>
|
||||
<Container className="sm-hide xs-hide md-hide" position="absolute" right="100%" marginRight="80px">
|
||||
<Container className="sm-hide xs-hide" position="absolute" right="100%" marginRight="80px">
|
||||
<Image src="images/jobs/hero-dots-left.svg" width="400px" />
|
||||
</Container>
|
||||
<div className="h2 sm-center sm-pt3" style={{ fontFamily: 'Roboto Mono' }}>
|
||||
|
||||
22
packages/website/ts/style/keyframes.ts
Normal file
22
packages/website/ts/style/keyframes.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { keyframes } from 'ts/style/theme';
|
||||
|
||||
export const rotate = keyframes`
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
`;
|
||||
|
||||
export const dash = keyframes`
|
||||
0% {
|
||||
stroke-dasharray: 1, 150;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
50% {
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: -35;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: -124;
|
||||
}
|
||||
`;
|
||||
25
yarn.lock
25
yarn.lock
@@ -1002,6 +1002,10 @@
|
||||
version "0.4.30"
|
||||
resolved "https://registry.yarnpkg.com/@types/istanbul/-/istanbul-0.4.30.tgz#073159320ab3296b2cfeb481f756a1f8f4c9c8e4"
|
||||
|
||||
"@types/js-combinatorics@^0.5.29":
|
||||
version "0.5.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/js-combinatorics/-/js-combinatorics-0.5.29.tgz#47a7819a0b6925b6dc4bd2c2278a7e6329b29387"
|
||||
|
||||
"@types/jsonschema@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/jsonschema/-/jsonschema-1.1.1.tgz#08703dfe074010e8e829123111594af731f57b1a"
|
||||
@@ -1146,6 +1150,12 @@
|
||||
"@types/node" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-helmet@^5.0.6":
|
||||
version "5.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-5.0.6.tgz#49607cbb72e1bb7dcefa9174cb591434d3b6f0af"
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-redux@^4.4.37":
|
||||
version "4.4.47"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-4.4.47.tgz#12af1677116e08d413fe2620d0a85560c8a0536e"
|
||||
@@ -7628,6 +7638,10 @@ js-base64@^2.1.9:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582"
|
||||
|
||||
js-combinatorics@^0.5.3:
|
||||
version "0.5.3"
|
||||
resolved "https://registry.yarnpkg.com/js-combinatorics/-/js-combinatorics-0.5.3.tgz#5da5a1c4632ec59fdf8d49dccfe59ef088122b15"
|
||||
|
||||
js-scrypt@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/js-scrypt/-/js-scrypt-0.2.0.tgz#7a62b701b4616e70ad0cde544627aabb99d7fe39"
|
||||
@@ -10981,6 +10995,15 @@ react-event-listener@^0.4.5:
|
||||
prop-types "^15.5.4"
|
||||
warning "^3.0.0"
|
||||
|
||||
react-helmet@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-5.2.0.tgz#a81811df21313a6d55c5f058c4aeba5d6f3d97a7"
|
||||
dependencies:
|
||||
deep-equal "^1.0.1"
|
||||
object-assign "^4.1.1"
|
||||
prop-types "^15.5.4"
|
||||
react-side-effect "^1.1.0"
|
||||
|
||||
react-highlight@0xproject/react-highlight:
|
||||
version "0.10.0"
|
||||
resolved "https://codeload.github.com/0xproject/react-highlight/tar.gz/83bbb4a09801abd341e2b9041cd884885a4a2098"
|
||||
@@ -11088,7 +11111,7 @@ react-scroll@^1.5.2:
|
||||
lodash.throttle "^4.1.1"
|
||||
prop-types "^15.5.8"
|
||||
|
||||
react-side-effect@^1.0.2:
|
||||
react-side-effect@^1.0.2, react-side-effect@^1.1.0:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-1.1.5.tgz#f26059e50ed9c626d91d661b9f3c8bb38cd0ff2d"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user