Merge pull request #211 from 0xProject/feature/orderWatcherLocalStateStore
Order watcher local state store
This commit is contained in:
@@ -62,7 +62,7 @@
|
||||
"chai-as-promised": "^7.1.0",
|
||||
"chai-as-promised-typescript-typings": "0.0.3",
|
||||
"chai-bignumber": "^2.0.1",
|
||||
"chai-typescript-typings": "^0.0.0",
|
||||
"chai-typescript-typings": "^0.0.1",
|
||||
"copyfiles": "^1.2.0",
|
||||
"coveralls": "^3.0.0",
|
||||
"dirty-chai": "^2.0.1",
|
||||
|
||||
@@ -205,9 +205,8 @@ export class ZeroEx {
|
||||
const etherTokenContractAddressIfExists = _.isUndefined(config) ? undefined : config.etherTokenContractAddress;
|
||||
this.etherToken = new EtherTokenWrapper(this._web3Wrapper, this.token, etherTokenContractAddressIfExists);
|
||||
const orderWatcherConfig = _.isUndefined(config) ? undefined : config.orderWatcherConfig;
|
||||
const orderStateUtils = new OrderStateUtils(this.token, this.exchange);
|
||||
this.orderStateWatcher = new OrderStateWatcher(
|
||||
this._web3Wrapper, this._abiDecoder, orderStateUtils, orderWatcherConfig,
|
||||
this._web3Wrapper, this._abiDecoder, this.token, this.exchange, orderWatcherConfig,
|
||||
);
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -28,10 +28,8 @@ export class EventWatcher {
|
||||
private _pollingIntervalMs: number;
|
||||
private _intervalIdIfExists?: NodeJS.Timer;
|
||||
private _lastEvents: Web3.LogEntry[] = [];
|
||||
private _numConfirmations: number;
|
||||
constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number, numConfirmations: number) {
|
||||
constructor(web3Wrapper: Web3Wrapper, pollingIntervalMs: undefined|number) {
|
||||
this._web3Wrapper = web3Wrapper;
|
||||
this._numConfirmations = numConfirmations;
|
||||
this._pollingIntervalMs = _.isUndefined(pollingIntervalMs) ?
|
||||
DEFAULT_EVENT_POLLING_INTERVAL :
|
||||
pollingIntervalMs;
|
||||
@@ -67,16 +65,9 @@ export class EventWatcher {
|
||||
this._lastEvents = pendingEvents;
|
||||
}
|
||||
private async _getEventsAsync(): Promise<Web3.LogEntry[]> {
|
||||
let latestBlock: BlockParamLiteral|number;
|
||||
if (this._numConfirmations === 0) {
|
||||
latestBlock = BlockParamLiteral.Pending;
|
||||
} else {
|
||||
const currentBlock = await this._web3Wrapper.getBlockNumberAsync();
|
||||
latestBlock = currentBlock - this._numConfirmations;
|
||||
}
|
||||
const eventFilter = {
|
||||
fromBlock: latestBlock,
|
||||
toBlock: latestBlock,
|
||||
fromBlock: BlockParamLiteral.Pending,
|
||||
toBlock: BlockParamLiteral.Pending,
|
||||
};
|
||||
const events = await this._web3Wrapper.getLogsAsync(eventFilter);
|
||||
return events;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as _ from 'lodash';
|
||||
import {schemas} from '0x-json-schemas';
|
||||
import * as ethUtil from 'ethereumjs-util';
|
||||
import {ZeroEx} from '../0x';
|
||||
import {EventWatcher} from './event_watcher';
|
||||
import {assert} from '../utils/assert';
|
||||
@@ -15,13 +14,22 @@ import {
|
||||
Web3Provider,
|
||||
BlockParamLiteral,
|
||||
LogWithDecodedArgs,
|
||||
ContractEventArgs,
|
||||
OnOrderStateChangeCallback,
|
||||
OrderStateWatcherConfig,
|
||||
ApprovalContractEventArgs,
|
||||
TransferContractEventArgs,
|
||||
LogFillContractEventArgs,
|
||||
LogCancelContractEventArgs,
|
||||
ExchangeEvents,
|
||||
TokenEvents,
|
||||
ZeroExError,
|
||||
} from '../types';
|
||||
import {Web3Wrapper} from '../web3_wrapper';
|
||||
import {TokenWrapper} from '../contract_wrappers/token_wrapper';
|
||||
import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper';
|
||||
import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store';
|
||||
import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store';
|
||||
|
||||
const DEFAULT_NUM_CONFIRMATIONS = 0;
|
||||
|
||||
@@ -44,26 +52,26 @@ interface OrderByOrderHash {
|
||||
export class OrderStateWatcher {
|
||||
private _orderByOrderHash: OrderByOrderHash = {};
|
||||
private _dependentOrderHashes: DependentOrderHashes = {};
|
||||
private _web3Wrapper: Web3Wrapper;
|
||||
private _callbackIfExistsAsync?: OnOrderStateChangeCallback;
|
||||
private _eventWatcher: EventWatcher;
|
||||
private _web3Wrapper: Web3Wrapper;
|
||||
private _abiDecoder: AbiDecoder;
|
||||
private _orderStateUtils: OrderStateUtils;
|
||||
private _numConfirmations: number;
|
||||
private _orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore;
|
||||
private _balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore;
|
||||
constructor(
|
||||
web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, orderStateUtils: OrderStateUtils,
|
||||
web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, token: TokenWrapper, exchange: ExchangeWrapper,
|
||||
config?: OrderStateWatcherConfig,
|
||||
) {
|
||||
this._web3Wrapper = web3Wrapper;
|
||||
const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.pollingIntervalMs;
|
||||
this._numConfirmations = _.isUndefined(config) ?
|
||||
DEFAULT_NUM_CONFIRMATIONS
|
||||
: config.numConfirmations;
|
||||
this._eventWatcher = new EventWatcher(
|
||||
this._web3Wrapper, eventPollingIntervalMs, this._numConfirmations,
|
||||
);
|
||||
this._abiDecoder = abiDecoder;
|
||||
this._orderStateUtils = orderStateUtils;
|
||||
this._web3Wrapper = web3Wrapper;
|
||||
const eventPollingIntervalMs = _.isUndefined(config) ? undefined : config.eventPollingIntervalMs;
|
||||
this._eventWatcher = new EventWatcher(web3Wrapper, eventPollingIntervalMs);
|
||||
this._balanceAndProxyAllowanceLazyStore = new BalanceAndProxyAllowanceLazyStore(token);
|
||||
this._orderFilledCancelledLazyStore = new OrderFilledCancelledLazyStore(exchange);
|
||||
this._orderStateUtils = new OrderStateUtils(
|
||||
this._balanceAndProxyAllowanceLazyStore, this._orderFilledCancelledLazyStore,
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Add an order to the orderStateWatcher. Before the order is added, it's
|
||||
@@ -108,6 +116,11 @@ export class OrderStateWatcher {
|
||||
* Ends an orderStateWatcher subscription.
|
||||
*/
|
||||
public unsubscribe(): void {
|
||||
if (_.isUndefined(this._callbackIfExistsAsync)) {
|
||||
throw new Error(ZeroExError.SubscriptionNotFound);
|
||||
}
|
||||
this._balanceAndProxyAllowanceLazyStore.deleteAll();
|
||||
this._orderFilledCancelledLazyStore.deleteAll();
|
||||
delete this._callbackIfExistsAsync;
|
||||
this._eventWatcher.unsubscribe();
|
||||
}
|
||||
@@ -117,45 +130,68 @@ export class OrderStateWatcher {
|
||||
if (!isLogDecoded) {
|
||||
return; // noop
|
||||
}
|
||||
// Unfortunately blockNumber is returned as a hex-encoded string, so we
|
||||
// convert it to a number here.
|
||||
const blockNumberBuff = ethUtil.toBuffer(maybeDecodedLog.blockNumber);
|
||||
const blockNumber = ethUtil.bufferToInt(blockNumberBuff);
|
||||
|
||||
const decodedLog = maybeDecodedLog as LogWithDecodedArgs<any>;
|
||||
const decodedLog = maybeDecodedLog as LogWithDecodedArgs<ContractEventArgs>;
|
||||
let makerToken: string;
|
||||
let makerAddress: string;
|
||||
let orderHashesSet: Set<string>;
|
||||
switch (decodedLog.event) {
|
||||
case TokenEvents.Approval:
|
||||
{
|
||||
// Invalidate cache
|
||||
const args = decodedLog.args as ApprovalContractEventArgs;
|
||||
this._balanceAndProxyAllowanceLazyStore.deleteProxyAllowance(decodedLog.address, args._owner);
|
||||
// Revalidate orders
|
||||
makerToken = decodedLog.address;
|
||||
makerAddress = decodedLog.args._owner;
|
||||
makerAddress = args._owner;
|
||||
orderHashesSet = _.get(this._dependentOrderHashes, [makerAddress, makerToken]);
|
||||
if (!_.isUndefined(orderHashesSet)) {
|
||||
const orderHashes = Array.from(orderHashesSet);
|
||||
await this._emitRevalidateOrdersAsync(orderHashes, blockNumber);
|
||||
await this._emitRevalidateOrdersAsync(orderHashes);
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
case TokenEvents.Transfer:
|
||||
{
|
||||
// Invalidate cache
|
||||
const args = decodedLog.args as TransferContractEventArgs;
|
||||
this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._from);
|
||||
this._balanceAndProxyAllowanceLazyStore.deleteBalance(decodedLog.address, args._to);
|
||||
// Revalidate orders
|
||||
makerToken = decodedLog.address;
|
||||
makerAddress = decodedLog.args._from;
|
||||
makerAddress = args._from;
|
||||
orderHashesSet = _.get(this._dependentOrderHashes, [makerAddress, makerToken]);
|
||||
if (!_.isUndefined(orderHashesSet)) {
|
||||
const orderHashes = Array.from(orderHashesSet);
|
||||
await this._emitRevalidateOrdersAsync(orderHashes, blockNumber);
|
||||
await this._emitRevalidateOrdersAsync(orderHashes);
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
case ExchangeEvents.LogFill:
|
||||
case ExchangeEvents.LogCancel:
|
||||
const orderHash = decodedLog.args.orderHash;
|
||||
{
|
||||
// Invalidate cache
|
||||
const args = decodedLog.args as LogFillContractEventArgs;
|
||||
this._orderFilledCancelledLazyStore.deleteFilledTakerAmount(args.orderHash);
|
||||
// Revalidate orders
|
||||
const orderHash = args.orderHash;
|
||||
const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]);
|
||||
if (isOrderWatched) {
|
||||
await this._emitRevalidateOrdersAsync([orderHash], blockNumber);
|
||||
await this._emitRevalidateOrdersAsync([orderHash]);
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
case ExchangeEvents.LogCancel:
|
||||
{
|
||||
// Invalidate cache
|
||||
const args = decodedLog.args as LogCancelContractEventArgs;
|
||||
this._orderFilledCancelledLazyStore.deleteCancelledTakerAmount(args.orderHash);
|
||||
// Revalidate orders
|
||||
const orderHash = args.orderHash;
|
||||
const isOrderWatched = !_.isUndefined(this._orderByOrderHash[orderHash]);
|
||||
if (isOrderWatched) {
|
||||
await this._emitRevalidateOrdersAsync([orderHash]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ExchangeEvents.LogError:
|
||||
return; // noop
|
||||
|
||||
@@ -163,22 +199,16 @@ export class OrderStateWatcher {
|
||||
throw utils.spawnSwitchErr('decodedLog.event', decodedLog.event);
|
||||
}
|
||||
}
|
||||
private async _emitRevalidateOrdersAsync(orderHashes: string[], blockNumber: number): Promise<void> {
|
||||
const defaultBlock = this._numConfirmations === 0 ?
|
||||
BlockParamLiteral.Pending :
|
||||
blockNumber;
|
||||
const methodOpts = {
|
||||
defaultBlock,
|
||||
};
|
||||
|
||||
private async _emitRevalidateOrdersAsync(orderHashes: string[]): Promise<void> {
|
||||
for (const orderHash of orderHashes) {
|
||||
const signedOrder = this._orderByOrderHash[orderHash] as SignedOrder;
|
||||
const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder, methodOpts);
|
||||
if (!_.isUndefined(this._callbackIfExistsAsync)) {
|
||||
await this._callbackIfExistsAsync(orderState);
|
||||
} else {
|
||||
// Most of these calls will never reach the network because the data is fetched from stores
|
||||
// and only updated when cache is invalidated
|
||||
const orderState = await this._orderStateUtils.getOrderStateAsync(signedOrder);
|
||||
if (_.isUndefined(this._callbackIfExistsAsync)) {
|
||||
break; // Unsubscribe was called
|
||||
}
|
||||
await this._callbackIfExistsAsync(orderState);
|
||||
}
|
||||
}
|
||||
private addToDependentOrderHashes(signedOrder: SignedOrder, orderHash: string) {
|
||||
|
||||
82
src/stores/balance_proxy_allowance_lazy_store.ts
Normal file
82
src/stores/balance_proxy_allowance_lazy_store.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as Web3 from 'web3';
|
||||
import {BigNumber} from 'bignumber.js';
|
||||
import {TokenWrapper} from '../contract_wrappers/token_wrapper';
|
||||
import {BlockParamLiteral} from '../types';
|
||||
|
||||
/**
|
||||
* Copy on read store for balances/proxyAllowances of tokens/accounts
|
||||
*/
|
||||
export class BalanceAndProxyAllowanceLazyStore {
|
||||
private token: TokenWrapper;
|
||||
private balance: {
|
||||
[tokenAddress: string]: {
|
||||
[userAddress: string]: BigNumber,
|
||||
},
|
||||
};
|
||||
private proxyAllowance: {
|
||||
[tokenAddress: string]: {
|
||||
[userAddress: string]: BigNumber,
|
||||
},
|
||||
};
|
||||
constructor(token: TokenWrapper) {
|
||||
this.token = token;
|
||||
this.balance = {};
|
||||
this.proxyAllowance = {};
|
||||
}
|
||||
public async getBalanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> {
|
||||
if (_.isUndefined(this.balance[tokenAddress]) || _.isUndefined(this.balance[tokenAddress][userAddress])) {
|
||||
const methodOpts = {
|
||||
defaultBlock: BlockParamLiteral.Pending,
|
||||
};
|
||||
const balance = await this.token.getBalanceAsync(tokenAddress, userAddress, methodOpts);
|
||||
this.setBalance(tokenAddress, userAddress, balance);
|
||||
}
|
||||
const cachedBalance = this.balance[tokenAddress][userAddress];
|
||||
return cachedBalance;
|
||||
}
|
||||
public setBalance(tokenAddress: string, userAddress: string, balance: BigNumber): void {
|
||||
if (_.isUndefined(this.balance[tokenAddress])) {
|
||||
this.balance[tokenAddress] = {};
|
||||
}
|
||||
this.balance[tokenAddress][userAddress] = balance;
|
||||
}
|
||||
public deleteBalance(tokenAddress: string, userAddress: string): void {
|
||||
if (!_.isUndefined(this.balance[tokenAddress])) {
|
||||
delete this.balance[tokenAddress][userAddress];
|
||||
if (_.isEmpty(this.balance[tokenAddress])) {
|
||||
delete this.balance[tokenAddress];
|
||||
}
|
||||
}
|
||||
}
|
||||
public async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> {
|
||||
if (_.isUndefined(this.proxyAllowance[tokenAddress]) ||
|
||||
_.isUndefined(this.proxyAllowance[tokenAddress][userAddress])) {
|
||||
const methodOpts = {
|
||||
defaultBlock: BlockParamLiteral.Pending,
|
||||
};
|
||||
const proxyAllowance = await this.token.getProxyAllowanceAsync(tokenAddress, userAddress, methodOpts);
|
||||
this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance);
|
||||
}
|
||||
const cachedProxyAllowance = this.proxyAllowance[tokenAddress][userAddress];
|
||||
return cachedProxyAllowance;
|
||||
}
|
||||
public setProxyAllowance(tokenAddress: string, userAddress: string, proxyAllowance: BigNumber): void {
|
||||
if (_.isUndefined(this.proxyAllowance[tokenAddress])) {
|
||||
this.proxyAllowance[tokenAddress] = {};
|
||||
}
|
||||
this.proxyAllowance[tokenAddress][userAddress] = proxyAllowance;
|
||||
}
|
||||
public deleteProxyAllowance(tokenAddress: string, userAddress: string): void {
|
||||
if (!_.isUndefined(this.proxyAllowance[tokenAddress])) {
|
||||
delete this.proxyAllowance[tokenAddress][userAddress];
|
||||
if (_.isEmpty(this.proxyAllowance[tokenAddress])) {
|
||||
delete this.proxyAllowance[tokenAddress];
|
||||
}
|
||||
}
|
||||
}
|
||||
public deleteAll(): void {
|
||||
this.balance = {};
|
||||
this.proxyAllowance = {};
|
||||
}
|
||||
}
|
||||
61
src/stores/order_filled_cancelled_lazy_store.ts
Normal file
61
src/stores/order_filled_cancelled_lazy_store.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as Web3 from 'web3';
|
||||
import {BigNumber} from 'bignumber.js';
|
||||
import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper';
|
||||
import {BlockParamLiteral} from '../types';
|
||||
|
||||
/**
|
||||
* Copy on read store for filled/cancelled taker amounts
|
||||
*/
|
||||
export class OrderFilledCancelledLazyStore {
|
||||
private exchange: ExchangeWrapper;
|
||||
private filledTakerAmount: {
|
||||
[orderHash: string]: BigNumber,
|
||||
};
|
||||
private cancelledTakerAmount: {
|
||||
[orderHash: string]: BigNumber,
|
||||
};
|
||||
constructor(exchange: ExchangeWrapper) {
|
||||
this.exchange = exchange;
|
||||
this.filledTakerAmount = {};
|
||||
this.cancelledTakerAmount = {};
|
||||
}
|
||||
public async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber> {
|
||||
if (_.isUndefined(this.filledTakerAmount[orderHash])) {
|
||||
const methodOpts = {
|
||||
defaultBlock: BlockParamLiteral.Pending,
|
||||
};
|
||||
const filledTakerAmount = await this.exchange.getFilledTakerAmountAsync(orderHash, methodOpts);
|
||||
this.setFilledTakerAmount(orderHash, filledTakerAmount);
|
||||
}
|
||||
const cachedFilled = this.filledTakerAmount[orderHash];
|
||||
return cachedFilled;
|
||||
}
|
||||
public setFilledTakerAmount(orderHash: string, filledTakerAmount: BigNumber): void {
|
||||
this.filledTakerAmount[orderHash] = filledTakerAmount;
|
||||
}
|
||||
public deleteFilledTakerAmount(orderHash: string): void {
|
||||
delete this.filledTakerAmount[orderHash];
|
||||
}
|
||||
public async getCancelledTakerAmountAsync(orderHash: string): Promise<BigNumber> {
|
||||
if (_.isUndefined(this.cancelledTakerAmount[orderHash])) {
|
||||
const methodOpts = {
|
||||
defaultBlock: BlockParamLiteral.Pending,
|
||||
};
|
||||
const cancelledTakerAmount = await this.exchange.getCanceledTakerAmountAsync(orderHash, methodOpts);
|
||||
this.setCancelledTakerAmount(orderHash, cancelledTakerAmount);
|
||||
}
|
||||
const cachedCancelled = this.cancelledTakerAmount[orderHash];
|
||||
return cachedCancelled;
|
||||
}
|
||||
public setCancelledTakerAmount(orderHash: string, cancelledTakerAmount: BigNumber): void {
|
||||
this.cancelledTakerAmount[orderHash] = cancelledTakerAmount;
|
||||
}
|
||||
public deleteCancelledTakerAmount(orderHash: string): void {
|
||||
delete this.cancelledTakerAmount[orderHash];
|
||||
}
|
||||
public deleteAll(): void {
|
||||
this.filledTakerAmount = {};
|
||||
this.cancelledTakerAmount = {};
|
||||
}
|
||||
}
|
||||
@@ -397,12 +397,10 @@ export interface JSONRPCPayload {
|
||||
}
|
||||
|
||||
/*
|
||||
* pollingIntervalMs: How often to poll the Ethereum node for new events.
|
||||
* numConfirmations: How many confirmed blocks deep you wish to listen for events at.
|
||||
* eventPollingIntervalMs: How often to poll the Ethereum node for new events
|
||||
*/
|
||||
export interface OrderStateWatcherConfig {
|
||||
pollingIntervalMs?: number;
|
||||
numConfirmations: number;
|
||||
eventPollingIntervalMs?: number;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as _ from 'lodash';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {ExchangeContractErrs, TradeSide, TransferType} from '../types';
|
||||
import {ExchangeContractErrs, TradeSide, TransferType, BlockParamLiteral} from '../types';
|
||||
import {TokenWrapper} from '../contract_wrappers/token_wrapper';
|
||||
import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store';
|
||||
|
||||
enum FailureReason {
|
||||
Balance = 'balance',
|
||||
@@ -31,58 +32,13 @@ const ERR_MSG_MAPPING = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Copy on read store for balances/proxyAllowances of tokens/accounts touched in trades
|
||||
*/
|
||||
export class BalanceAndProxyAllowanceLazyStore {
|
||||
protected _token: TokenWrapper;
|
||||
private _balance: {
|
||||
[tokenAddress: string]: {
|
||||
[userAddress: string]: BigNumber,
|
||||
},
|
||||
};
|
||||
private _proxyAllowance: {
|
||||
[tokenAddress: string]: {
|
||||
[userAddress: string]: BigNumber,
|
||||
},
|
||||
};
|
||||
export class ExchangeTransferSimulator {
|
||||
private store: BalanceAndProxyAllowanceLazyStore;
|
||||
private UNLIMITED_ALLOWANCE_IN_BASE_UNITS: BigNumber;
|
||||
constructor(token: TokenWrapper) {
|
||||
this._token = token;
|
||||
this._balance = {};
|
||||
this._proxyAllowance = {};
|
||||
this.store = new BalanceAndProxyAllowanceLazyStore(token);
|
||||
this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS = token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS;
|
||||
}
|
||||
protected async getBalanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> {
|
||||
if (_.isUndefined(this._balance[tokenAddress]) || _.isUndefined(this._balance[tokenAddress][userAddress])) {
|
||||
const balance = await this._token.getBalanceAsync(tokenAddress, userAddress);
|
||||
this.setBalance(tokenAddress, userAddress, balance);
|
||||
}
|
||||
const cachedBalance = this._balance[tokenAddress][userAddress];
|
||||
return cachedBalance;
|
||||
}
|
||||
protected setBalance(tokenAddress: string, userAddress: string, balance: BigNumber): void {
|
||||
if (_.isUndefined(this._balance[tokenAddress])) {
|
||||
this._balance[tokenAddress] = {};
|
||||
}
|
||||
this._balance[tokenAddress][userAddress] = balance;
|
||||
}
|
||||
protected async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> {
|
||||
if (_.isUndefined(this._proxyAllowance[tokenAddress]) ||
|
||||
_.isUndefined(this._proxyAllowance[tokenAddress][userAddress])) {
|
||||
const proxyAllowance = await this._token.getProxyAllowanceAsync(tokenAddress, userAddress);
|
||||
this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance);
|
||||
}
|
||||
const cachedProxyAllowance = this._proxyAllowance[tokenAddress][userAddress];
|
||||
return cachedProxyAllowance;
|
||||
}
|
||||
protected setProxyAllowance(tokenAddress: string, userAddress: string, proxyAllowance: BigNumber): void {
|
||||
if (_.isUndefined(this._proxyAllowance[tokenAddress])) {
|
||||
this._proxyAllowance[tokenAddress] = {};
|
||||
}
|
||||
this._proxyAllowance[tokenAddress][userAddress] = proxyAllowance;
|
||||
}
|
||||
}
|
||||
|
||||
export class ExchangeTransferSimulator extends BalanceAndProxyAllowanceLazyStore {
|
||||
/**
|
||||
* Simulates transferFrom call performed by a proxy
|
||||
* @param tokenAddress Address of the token to be transferred
|
||||
@@ -95,8 +51,8 @@ export class ExchangeTransferSimulator extends BalanceAndProxyAllowanceLazyStore
|
||||
public async transferFromAsync(tokenAddress: string, from: string, to: string,
|
||||
amountInBaseUnits: BigNumber, tradeSide: TradeSide,
|
||||
transferType: TransferType): Promise<void> {
|
||||
const balance = await this.getBalanceAsync(tokenAddress, from);
|
||||
const proxyAllowance = await this.getProxyAllowanceAsync(tokenAddress, from);
|
||||
const balance = await this.store.getBalanceAsync(tokenAddress, from);
|
||||
const proxyAllowance = await this.store.getProxyAllowanceAsync(tokenAddress, from);
|
||||
if (proxyAllowance.lessThan(amountInBaseUnits)) {
|
||||
this.throwValidationError(FailureReason.ProxyAllowance, tradeSide, transferType);
|
||||
}
|
||||
@@ -109,20 +65,20 @@ export class ExchangeTransferSimulator extends BalanceAndProxyAllowanceLazyStore
|
||||
}
|
||||
private async decreaseProxyAllowanceAsync(tokenAddress: string, userAddress: string,
|
||||
amountInBaseUnits: BigNumber): Promise<void> {
|
||||
const proxyAllowance = await this.getProxyAllowanceAsync(tokenAddress, userAddress);
|
||||
if (!proxyAllowance.eq(this._token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) {
|
||||
this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance.minus(amountInBaseUnits));
|
||||
const proxyAllowance = await this.store.getProxyAllowanceAsync(tokenAddress, userAddress);
|
||||
if (!proxyAllowance.eq(this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) {
|
||||
this.store.setProxyAllowance(tokenAddress, userAddress, proxyAllowance.minus(amountInBaseUnits));
|
||||
}
|
||||
}
|
||||
private async increaseBalanceAsync(tokenAddress: string, userAddress: string,
|
||||
amountInBaseUnits: BigNumber): Promise<void> {
|
||||
const balance = await this.getBalanceAsync(tokenAddress, userAddress);
|
||||
this.setBalance(tokenAddress, userAddress, balance.plus(amountInBaseUnits));
|
||||
const balance = await this.store.getBalanceAsync(tokenAddress, userAddress);
|
||||
this.store.setBalance(tokenAddress, userAddress, balance.plus(amountInBaseUnits));
|
||||
}
|
||||
private async decreaseBalanceAsync(tokenAddress: string, userAddress: string,
|
||||
amountInBaseUnits: BigNumber): Promise<void> {
|
||||
const balance = await this.getBalanceAsync(tokenAddress, userAddress);
|
||||
this.setBalance(tokenAddress, userAddress, balance.minus(amountInBaseUnits));
|
||||
const balance = await this.store.getBalanceAsync(tokenAddress, userAddress);
|
||||
this.store.setBalance(tokenAddress, userAddress, balance.minus(amountInBaseUnits));
|
||||
}
|
||||
private throwValidationError(failureReason: FailureReason, tradeSide: TradeSide,
|
||||
transferType: TransferType): Promise<never> {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as Web3 from 'web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {
|
||||
ExchangeContractErrs,
|
||||
@@ -14,16 +15,19 @@ import {TokenWrapper} from '../contract_wrappers/token_wrapper';
|
||||
import {ExchangeWrapper} from '../contract_wrappers/exchange_wrapper';
|
||||
import {utils} from '../utils/utils';
|
||||
import {constants} from '../utils/constants';
|
||||
import {OrderFilledCancelledLazyStore} from '../stores/order_filled_cancelled_lazy_store';
|
||||
import {BalanceAndProxyAllowanceLazyStore} from '../stores/balance_proxy_allowance_lazy_store';
|
||||
|
||||
export class OrderStateUtils {
|
||||
private tokenWrapper: TokenWrapper;
|
||||
private exchangeWrapper: ExchangeWrapper;
|
||||
constructor(tokenWrapper: TokenWrapper, exchangeWrapper: ExchangeWrapper) {
|
||||
this.tokenWrapper = tokenWrapper;
|
||||
this.exchangeWrapper = exchangeWrapper;
|
||||
private balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore;
|
||||
private orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore;
|
||||
constructor(balanceAndProxyAllowanceLazyStore: BalanceAndProxyAllowanceLazyStore,
|
||||
orderFilledCancelledLazyStore: OrderFilledCancelledLazyStore) {
|
||||
this.balanceAndProxyAllowanceLazyStore = balanceAndProxyAllowanceLazyStore;
|
||||
this.orderFilledCancelledLazyStore = orderFilledCancelledLazyStore;
|
||||
}
|
||||
public async getOrderStateAsync(signedOrder: SignedOrder, methodOpts?: MethodOpts): Promise<OrderState> {
|
||||
const orderRelevantState = await this.getOrderRelevantStateAsync(signedOrder, methodOpts);
|
||||
public async getOrderStateAsync(signedOrder: SignedOrder): Promise<OrderState> {
|
||||
const orderRelevantState = await this.getOrderRelevantStateAsync(signedOrder);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
try {
|
||||
this.validateIfOrderIsValid(signedOrder, orderRelevantState);
|
||||
@@ -42,26 +46,31 @@ export class OrderStateUtils {
|
||||
return orderState;
|
||||
}
|
||||
}
|
||||
public async getOrderRelevantStateAsync(
|
||||
signedOrder: SignedOrder, methodOpts?: MethodOpts): Promise<OrderRelevantState> {
|
||||
const zrxTokenAddress = await this.exchangeWrapper.getZRXTokenAddressAsync();
|
||||
public async getOrderRelevantStateAsync(signedOrder: SignedOrder): Promise<OrderRelevantState> {
|
||||
// HACK: We access the private property here but otherwise the interface will be less nice.
|
||||
// If we pass it from the instantiator - there is no opportunity to get it there
|
||||
// because JS doesn't support async constructors.
|
||||
// Moreover - it's cached under the hood so it's equivalent to an async constructor.
|
||||
const exchange = (this.orderFilledCancelledLazyStore as any).exchange as ExchangeWrapper;
|
||||
const zrxTokenAddress = await exchange.getZRXTokenAddressAsync();
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
const makerBalance = await this.tokenWrapper.getBalanceAsync(
|
||||
signedOrder.makerTokenAddress, signedOrder.maker, methodOpts,
|
||||
const makerBalance = await this.balanceAndProxyAllowanceLazyStore.getBalanceAsync(
|
||||
signedOrder.makerTokenAddress, signedOrder.maker,
|
||||
);
|
||||
const makerProxyAllowance = await this.tokenWrapper.getProxyAllowanceAsync(
|
||||
signedOrder.makerTokenAddress, signedOrder.maker, methodOpts,
|
||||
const makerProxyAllowance = await this.balanceAndProxyAllowanceLazyStore.getProxyAllowanceAsync(
|
||||
signedOrder.makerTokenAddress, signedOrder.maker,
|
||||
);
|
||||
const makerFeeBalance = await this.tokenWrapper.getBalanceAsync(
|
||||
zrxTokenAddress, signedOrder.maker, methodOpts,
|
||||
const makerFeeBalance = await this.balanceAndProxyAllowanceLazyStore.getBalanceAsync(
|
||||
zrxTokenAddress, signedOrder.maker,
|
||||
);
|
||||
const makerFeeProxyAllowance = await this.tokenWrapper.getProxyAllowanceAsync(
|
||||
zrxTokenAddress, signedOrder.maker, methodOpts,
|
||||
const makerFeeProxyAllowance = await this.balanceAndProxyAllowanceLazyStore.getProxyAllowanceAsync(
|
||||
zrxTokenAddress, signedOrder.maker,
|
||||
);
|
||||
const filledTakerTokenAmount = await this.exchangeWrapper.getFilledTakerAmountAsync(orderHash, methodOpts);
|
||||
const canceledTakerTokenAmount = await this.exchangeWrapper.getCanceledTakerAmountAsync(orderHash, methodOpts);
|
||||
const unavailableTakerTokenAmount =
|
||||
await this.exchangeWrapper.getUnavailableTakerAmountAsync(orderHash, methodOpts);
|
||||
const filledTakerTokenAmount = await this.orderFilledCancelledLazyStore.getFilledTakerAmountAsync(orderHash);
|
||||
const canceledTakerTokenAmount = await this.orderFilledCancelledLazyStore.getCancelledTakerAmountAsync(
|
||||
orderHash,
|
||||
);
|
||||
const unavailableTakerTokenAmount = await exchange.getUnavailableTakerAmountAsync(orderHash);
|
||||
const totalMakerTokenAmount = signedOrder.makerTokenAmount;
|
||||
const totalTakerTokenAmount = signedOrder.takerTokenAmount;
|
||||
const remainingTakerTokenAmount = totalTakerTokenAmount.minus(unavailableTakerTokenAmount);
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('EventWatcher', () => {
|
||||
web3 = web3Factory.create();
|
||||
const pollingIntervalMs = 10;
|
||||
web3Wrapper = new Web3Wrapper(web3.currentProvider);
|
||||
eventWatcher = new EventWatcher(web3Wrapper, pollingIntervalMs, numConfirmations);
|
||||
eventWatcher = new EventWatcher(web3Wrapper, pollingIntervalMs);
|
||||
});
|
||||
afterEach(() => {
|
||||
// clean up any stubs after the test has completed
|
||||
|
||||
@@ -59,11 +59,10 @@ describe('ExchangeTransferSimulator', () => {
|
||||
await exchangeTransferSimulator.transferFromAsync(
|
||||
exampleTokenAddress, sender, recipient, transferAmount, TradeSide.Taker, TransferType.Trade,
|
||||
);
|
||||
const senderBalance = await (exchangeTransferSimulator as any).getBalanceAsync(exampleTokenAddress, sender);
|
||||
const recipientBalance = await (exchangeTransferSimulator as any).getBalanceAsync(
|
||||
exampleTokenAddress, recipient);
|
||||
const senderProxyAllowance = await (exchangeTransferSimulator as any).getProxyAllowanceAsync(
|
||||
exampleTokenAddress, sender);
|
||||
const store = (exchangeTransferSimulator as any).store;
|
||||
const senderBalance = await store.getBalanceAsync(exampleTokenAddress, sender);
|
||||
const recipientBalance = await store.getBalanceAsync(exampleTokenAddress, recipient);
|
||||
const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleTokenAddress, sender);
|
||||
expect(senderBalance).to.be.bignumber.equal(0);
|
||||
expect(recipientBalance).to.be.bignumber.equal(transferAmount);
|
||||
expect(senderProxyAllowance).to.be.bignumber.equal(0);
|
||||
@@ -76,11 +75,10 @@ describe('ExchangeTransferSimulator', () => {
|
||||
await exchangeTransferSimulator.transferFromAsync(
|
||||
exampleTokenAddress, sender, recipient, transferAmount, TradeSide.Taker, TransferType.Trade,
|
||||
);
|
||||
const senderBalance = await (exchangeTransferSimulator as any).getBalanceAsync(exampleTokenAddress, sender);
|
||||
const recipientBalance = await (exchangeTransferSimulator as any).getBalanceAsync(
|
||||
exampleTokenAddress, recipient);
|
||||
const senderProxyAllowance = await (exchangeTransferSimulator as any).getProxyAllowanceAsync(
|
||||
exampleTokenAddress, sender);
|
||||
const store = (exchangeTransferSimulator as any).store;
|
||||
const senderBalance = await store.getBalanceAsync(exampleTokenAddress, sender);
|
||||
const recipientBalance = await store.getBalanceAsync(exampleTokenAddress, recipient);
|
||||
const senderProxyAllowance = await store.getProxyAllowanceAsync(exampleTokenAddress, sender);
|
||||
expect(senderBalance).to.be.bignumber.equal(0);
|
||||
expect(recipientBalance).to.be.bignumber.equal(transferAmount);
|
||||
expect(senderProxyAllowance).to.be.bignumber.equal(zeroEx.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS);
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ZeroExConfig,
|
||||
OrderState,
|
||||
SignedOrder,
|
||||
ZeroExError,
|
||||
OrderStateValid,
|
||||
OrderStateInvalid,
|
||||
ExchangeContractErrs,
|
||||
@@ -92,14 +93,10 @@ describe('OrderStateWatcher', () => {
|
||||
afterEach(async () => {
|
||||
zeroEx.orderStateWatcher.unsubscribe();
|
||||
});
|
||||
it('should fail when trying to subscribe twice', (done: DoneCallback) => {
|
||||
it('should fail when trying to subscribe twice', async () => {
|
||||
zeroEx.orderStateWatcher.subscribe(_.noop);
|
||||
try {
|
||||
zeroEx.orderStateWatcher.subscribe(_.noop);
|
||||
done(new Error('Expected the second subscription to fail'));
|
||||
} catch (err) {
|
||||
done();
|
||||
}
|
||||
expect(() => zeroEx.orderStateWatcher.subscribe(_.noop))
|
||||
.to.throw(ZeroExError.SubscriptionAlreadyPresent);
|
||||
});
|
||||
});
|
||||
describe('tests with cleanup', async () => {
|
||||
@@ -355,67 +352,5 @@ describe('OrderStateWatcher', () => {
|
||||
await zeroEx.exchange.cancelOrderAsync(signedOrder, cancelAmountInBaseUnits);
|
||||
})().catch(done);
|
||||
});
|
||||
describe('check numConfirmations behavior', () => {
|
||||
before(() => {
|
||||
const configs: ZeroExConfig = {
|
||||
orderWatcherConfig: {
|
||||
numConfirmations: 1,
|
||||
},
|
||||
};
|
||||
zeroEx = new ZeroEx(web3.currentProvider, configs);
|
||||
});
|
||||
it('should emit orderState when watching at 1 confirmation deep and event is one block deep',
|
||||
(done: DoneCallback) => {
|
||||
(async () => {
|
||||
fillScenarios = new FillScenarios(
|
||||
zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress,
|
||||
);
|
||||
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, fillableAmount,
|
||||
);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
expect(orderState.isValid).to.be.false();
|
||||
const invalidOrderState = orderState as OrderStateInvalid;
|
||||
expect(invalidOrderState.orderHash).to.be.equal(orderHash);
|
||||
expect(invalidOrderState.error).to.be.equal(ExchangeContractErrs.InsufficientMakerBalance);
|
||||
done();
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
|
||||
const anyRecipient = taker;
|
||||
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||
await zeroEx.token.transferAsync(makerToken.address, maker, anyRecipient, makerBalance);
|
||||
blockchainLifecycle.mineABlock();
|
||||
})().catch(done);
|
||||
});
|
||||
it('shouldn\'t emit orderState when watching at 1 confirmation deep and event is in mempool',
|
||||
(done: DoneCallback) => {
|
||||
(async () => {
|
||||
fillScenarios = new FillScenarios(
|
||||
zeroEx, userAddresses, tokens, zrxTokenAddress, exchangeContractAddress,
|
||||
);
|
||||
|
||||
signedOrder = await fillScenarios.createFillableSignedOrderAsync(
|
||||
makerToken.address, takerToken.address, maker, taker, fillableAmount,
|
||||
);
|
||||
const orderHash = ZeroEx.getOrderHashHex(signedOrder);
|
||||
zeroEx.orderStateWatcher.addOrder(signedOrder);
|
||||
const callback = reportCallbackErrors(done)((orderState: OrderState) => {
|
||||
throw new Error('OrderState callback fired when it shouldn\'t have');
|
||||
});
|
||||
zeroEx.orderStateWatcher.subscribe(callback);
|
||||
|
||||
const anyRecipient = taker;
|
||||
const makerBalance = await zeroEx.token.getBalanceAsync(makerToken.address, maker);
|
||||
await zeroEx.token.transferAsync(makerToken.address, maker, anyRecipient, makerBalance);
|
||||
setTimeout(() => {
|
||||
done();
|
||||
}, TIMEOUT_MS);
|
||||
})().catch(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1041,6 +1041,10 @@ chai-typescript-typings@^0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chai-typescript-typings/-/chai-typescript-typings-0.0.0.tgz#52e076d72cf29129c94ab1dba6e33ce3828a0724"
|
||||
|
||||
chai-typescript-typings@^0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/chai-typescript-typings/-/chai-typescript-typings-0.0.1.tgz#433dee303b0b2978ad0dd03129df0a5afb791274"
|
||||
|
||||
chai@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/chai/-/chai-4.0.2.tgz#2f7327c4de6f385dd7787999e2ab02697a32b83b"
|
||||
|
||||
Reference in New Issue
Block a user