diff options
author | Fabio Berger <me@fabioberger.com> | 2017-11-13 11:17:18 +0800 |
---|---|---|
committer | Fabio Berger <me@fabioberger.com> | 2017-11-13 11:17:18 +0800 |
commit | c4ee2d73865a1444c079b9e2836b7630a0adf03e (patch) | |
tree | b9c7794e7022fb189675d914f5fe58dcabd67dec /packages/0x.js/src/contract_wrappers | |
parent | a74ec0effa818a86233fe64cb0dad2c61bbb4bb6 (diff) | |
download | dexon-sol-tools-c4ee2d73865a1444c079b9e2836b7630a0adf03e.tar dexon-sol-tools-c4ee2d73865a1444c079b9e2836b7630a0adf03e.tar.gz dexon-sol-tools-c4ee2d73865a1444c079b9e2836b7630a0adf03e.tar.bz2 dexon-sol-tools-c4ee2d73865a1444c079b9e2836b7630a0adf03e.tar.lz dexon-sol-tools-c4ee2d73865a1444c079b9e2836b7630a0adf03e.tar.xz dexon-sol-tools-c4ee2d73865a1444c079b9e2836b7630a0adf03e.tar.zst dexon-sol-tools-c4ee2d73865a1444c079b9e2836b7630a0adf03e.zip |
Switch over to Lerna + Yarn Workspaces setup for a mono-repo approach
Diffstat (limited to 'packages/0x.js/src/contract_wrappers')
6 files changed, 1600 insertions, 0 deletions
diff --git a/packages/0x.js/src/contract_wrappers/contract_wrapper.ts b/packages/0x.js/src/contract_wrappers/contract_wrapper.ts new file mode 100644 index 000000000..7997b1647 --- /dev/null +++ b/packages/0x.js/src/contract_wrappers/contract_wrapper.ts @@ -0,0 +1,152 @@ +import * as _ from 'lodash'; +import * as Web3 from 'web3'; +import {BlockAndLogStreamer, Block} from 'ethereumjs-blockstream'; +import {Web3Wrapper} from '../web3_wrapper'; +import {AbiDecoder} from '../utils/abi_decoder'; +import { + ZeroExError, + InternalZeroExError, + Artifact, + LogWithDecodedArgs, + RawLog, + ContractEvents, + SubscriptionOpts, + IndexedFilterValues, + EventCallback, + BlockParamLiteral, + ContractEventArgs, +} from '../types'; +import {constants} from '../utils/constants'; +import {intervalUtils} from '../utils/interval_utils'; +import {filterUtils} from '../utils/filter_utils'; + +export class ContractWrapper { + protected _web3Wrapper: Web3Wrapper; + private _abiDecoder?: AbiDecoder; + private _blockAndLogStreamer: BlockAndLogStreamer|undefined; + private _blockAndLogStreamInterval: NodeJS.Timer; + private _filters: {[filterToken: string]: Web3.FilterObject}; + private _filterCallbacks: {[filterToken: string]: EventCallback<ContractEventArgs>}; + private _onLogAddedSubscriptionToken: string|undefined; + private _onLogRemovedSubscriptionToken: string|undefined; + constructor(web3Wrapper: Web3Wrapper, abiDecoder?: AbiDecoder) { + this._web3Wrapper = web3Wrapper; + this._abiDecoder = abiDecoder; + this._filters = {}; + this._filterCallbacks = {}; + this._blockAndLogStreamer = undefined; + this._onLogAddedSubscriptionToken = undefined; + this._onLogRemovedSubscriptionToken = undefined; + } + /** + * Cancels all existing subscriptions + */ + public unsubscribeAll(): void { + const filterTokens = _.keys(this._filterCallbacks); + _.each(filterTokens, filterToken => { + this._unsubscribe(filterToken); + }); + } + protected _unsubscribe(filterToken: string, err?: Error): void { + if (_.isUndefined(this._filters[filterToken])) { + throw new Error(ZeroExError.SubscriptionNotFound); + } + if (!_.isUndefined(err)) { + const callback = this._filterCallbacks[filterToken]; + callback(err, undefined); + } + delete this._filters[filterToken]; + delete this._filterCallbacks[filterToken]; + if (_.isEmpty(this._filters)) { + this._stopBlockAndLogStream(); + } + } + protected _subscribe<ArgsType extends ContractEventArgs>( + address: string, eventName: ContractEvents, indexFilterValues: IndexedFilterValues, abi: Web3.ContractAbi, + callback: EventCallback<ArgsType>): string { + const filter = filterUtils.getFilter(address, eventName, indexFilterValues, abi); + if (_.isUndefined(this._blockAndLogStreamer)) { + this._startBlockAndLogStream(); + } + const filterToken = filterUtils.generateUUID(); + this._filters[filterToken] = filter; + this._filterCallbacks[filterToken] = callback; + return filterToken; + } + protected async _getLogsAsync<ArgsType extends ContractEventArgs>( + address: string, eventName: ContractEvents, subscriptionOpts: SubscriptionOpts, + indexFilterValues: IndexedFilterValues, abi: Web3.ContractAbi): Promise<Array<LogWithDecodedArgs<ArgsType>>> { + const filter = filterUtils.getFilter(address, eventName, indexFilterValues, abi, subscriptionOpts); + const logs = await this._web3Wrapper.getLogsAsync(filter); + const logsWithDecodedArguments = _.map(logs, this._tryToDecodeLogOrNoop.bind(this)); + return logsWithDecodedArguments; + } + protected _tryToDecodeLogOrNoop<ArgsType extends ContractEventArgs>( + log: Web3.LogEntry): LogWithDecodedArgs<ArgsType>|RawLog { + if (_.isUndefined(this._abiDecoder)) { + throw new Error(InternalZeroExError.NoAbiDecoder); + } + const logWithDecodedArgs = this._abiDecoder.tryToDecodeLogOrNoop(log); + return logWithDecodedArgs; + } + protected async _instantiateContractIfExistsAsync<ContractType extends Web3.ContractInstance>( + artifact: Artifact, addressIfExists?: string): Promise<ContractType> { + const contractInstance = + await this._web3Wrapper.getContractInstanceFromArtifactAsync<ContractType>(artifact, addressIfExists); + return contractInstance; + } + private _onLogStateChanged<ArgsType extends ContractEventArgs>(removed: boolean, log: Web3.LogEntry): void { + _.forEach(this._filters, (filter: Web3.FilterObject, filterToken: string) => { + if (filterUtils.matchesFilter(log, filter)) { + const decodedLog = this._tryToDecodeLogOrNoop(log) as LogWithDecodedArgs<ArgsType>; + const logEvent = { + ...decodedLog, + removed, + }; + this._filterCallbacks[filterToken](null, logEvent); + } + }); + } + private _startBlockAndLogStream(): void { + this._blockAndLogStreamer = new BlockAndLogStreamer( + this._web3Wrapper.getBlockAsync.bind(this._web3Wrapper), + this._web3Wrapper.getLogsAsync.bind(this._web3Wrapper), + ); + const catchAllLogFilter = {}; + this._blockAndLogStreamer.addLogFilter(catchAllLogFilter); + this._blockAndLogStreamInterval = intervalUtils.setAsyncExcludingInterval( + this._reconcileBlockAsync.bind(this), constants.DEFAULT_BLOCK_POLLING_INTERVAL, + ); + let removed = false; + this._onLogAddedSubscriptionToken = this._blockAndLogStreamer.subscribeToOnLogAdded( + this._onLogStateChanged.bind(this, removed), + ); + removed = true; + this._onLogRemovedSubscriptionToken = this._blockAndLogStreamer.subscribeToOnLogRemoved( + this._onLogStateChanged.bind(this, removed), + ); + } + private _stopBlockAndLogStream(): void { + (this._blockAndLogStreamer as BlockAndLogStreamer).unsubscribeFromOnLogAdded( + this._onLogAddedSubscriptionToken as string); + (this._blockAndLogStreamer as BlockAndLogStreamer).unsubscribeFromOnLogRemoved( + this._onLogRemovedSubscriptionToken as string); + intervalUtils.clearAsyncExcludingInterval(this._blockAndLogStreamInterval); + delete this._blockAndLogStreamer; + } + private async _reconcileBlockAsync(): Promise<void> { + try { + const latestBlock = await this._web3Wrapper.getBlockAsync(BlockParamLiteral.Latest); + // We need to coerce to Block type cause Web3.Block includes types for mempool blocks + if (!_.isUndefined(this._blockAndLogStreamer)) { + // If we clear the interval while fetching the block - this._blockAndLogStreamer will be undefined + this._blockAndLogStreamer.reconcileNewBlock(latestBlock as any as Block); + } + } catch (err) { + const filterTokens = _.keys(this._filterCallbacks); + _.each(filterTokens, filterToken => { + this._unsubscribe(filterToken, err); + }); + } + } +} diff --git a/packages/0x.js/src/contract_wrappers/ether_token_wrapper.ts b/packages/0x.js/src/contract_wrappers/ether_token_wrapper.ts new file mode 100644 index 000000000..3cd2f0224 --- /dev/null +++ b/packages/0x.js/src/contract_wrappers/ether_token_wrapper.ts @@ -0,0 +1,87 @@ +import * as _ from 'lodash'; +import BigNumber from 'bignumber.js'; +import {Web3Wrapper} from '../web3_wrapper'; +import {ContractWrapper} from './contract_wrapper'; +import {TokenWrapper} from './token_wrapper'; +import {EtherTokenContract, ZeroExError} from '../types'; +import {assert} from '../utils/assert'; +import {artifacts} from '../artifacts'; + +/** + * This class includes all the functionality related to interacting with a wrapped Ether ERC20 token contract. + * The caller can convert ETH into the equivalent number of wrapped ETH ERC20 tokens and back. + */ +export class EtherTokenWrapper extends ContractWrapper { + private _etherTokenContractIfExists?: EtherTokenContract; + private _tokenWrapper: TokenWrapper; + private _contractAddressIfExists?: string; + constructor(web3Wrapper: Web3Wrapper, tokenWrapper: TokenWrapper, contractAddressIfExists?: string) { + super(web3Wrapper); + this._tokenWrapper = tokenWrapper; + this._contractAddressIfExists = contractAddressIfExists; + } + /** + * Deposit ETH into the Wrapped ETH smart contract and issues the equivalent number of wrapped ETH tokens + * to the depositor address. These wrapped ETH tokens can be used in 0x trades and are redeemable for 1-to-1 + * for ETH. + * @param amountInWei Amount of ETH in Wei the caller wishes to deposit. + * @param depositor The hex encoded user Ethereum address that would like to make the deposit. + * @return Transaction hash. + */ + public async depositAsync(amountInWei: BigNumber, depositor: string): Promise<string> { + assert.isValidBaseUnitAmount('amountInWei', amountInWei); + await assert.isSenderAddressAsync('depositor', depositor, this._web3Wrapper); + + const ethBalanceInWei = await this._web3Wrapper.getBalanceInWeiAsync(depositor); + assert.assert(ethBalanceInWei.gte(amountInWei), ZeroExError.InsufficientEthBalanceForDeposit); + + const wethContract = await this._getEtherTokenContractAsync(); + const txHash = await wethContract.deposit.sendTransactionAsync({ + from: depositor, + value: amountInWei, + }); + return txHash; + } + /** + * Withdraw ETH to the withdrawer's address from the wrapped ETH smart contract in exchange for the + * equivalent number of wrapped ETH tokens. + * @param amountInWei Amount of ETH in Wei the caller wishes to withdraw. + * @param withdrawer The hex encoded user Ethereum address that would like to make the withdrawl. + * @return Transaction hash. + */ + public async withdrawAsync(amountInWei: BigNumber, withdrawer: string): Promise<string> { + assert.isValidBaseUnitAmount('amountInWei', amountInWei); + await assert.isSenderAddressAsync('withdrawer', withdrawer, this._web3Wrapper); + + const wethContractAddress = await this.getContractAddressAsync(); + const WETHBalanceInBaseUnits = await this._tokenWrapper.getBalanceAsync(wethContractAddress, withdrawer); + assert.assert(WETHBalanceInBaseUnits.gte(amountInWei), ZeroExError.InsufficientWEthBalanceForWithdrawal); + + const wethContract = await this._getEtherTokenContractAsync(); + const txHash = await wethContract.withdraw.sendTransactionAsync(amountInWei, { + from: withdrawer, + }); + return txHash; + } + /** + * Retrieves the Wrapped Ether token contract address + * @return The Wrapped Ether token contract address + */ + public async getContractAddressAsync(): Promise<string> { + const wethContract = await this._getEtherTokenContractAsync(); + return wethContract.address; + } + private _invalidateContractInstance(): void { + delete this._etherTokenContractIfExists; + } + private async _getEtherTokenContractAsync(): Promise<EtherTokenContract> { + if (!_.isUndefined(this._etherTokenContractIfExists)) { + return this._etherTokenContractIfExists; + } + const contractInstance = await this._instantiateContractIfExistsAsync<EtherTokenContract>( + artifacts.EtherTokenArtifact, this._contractAddressIfExists, + ); + this._etherTokenContractIfExists = contractInstance as EtherTokenContract; + return this._etherTokenContractIfExists; + } +} diff --git a/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts b/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts new file mode 100644 index 000000000..fe0c5bc00 --- /dev/null +++ b/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts @@ -0,0 +1,866 @@ +import * as _ from 'lodash'; +import * as Web3 from 'web3'; +import BigNumber from 'bignumber.js'; +import {schemas} from '0x-json-schemas'; +import {Web3Wrapper} from '../web3_wrapper'; +import { + ECSignature, + ExchangeContract, + ExchangeContractErrCodes, + ExchangeContractErrs, + ZeroExError, + OrderValues, + OrderAddresses, + Order, + SignedOrder, + ExchangeEvents, + SubscriptionOpts, + IndexedFilterValues, + OrderCancellationRequest, + OrderFillRequest, + LogErrorContractEventArgs, + LogFillContractEventArgs, + LogCancelContractEventArgs, + LogWithDecodedArgs, + MethodOpts, + ValidateOrderFillableOpts, + OrderTransactionOpts, + RawLog, + EventCallback, + ExchangeContractEventArgs, + DecodedLogArgs, +} from '../types'; +import {assert} from '../utils/assert'; +import {utils} from '../utils/utils'; +import {OrderValidationUtils} from '../utils/order_validation_utils'; +import {ContractWrapper} from './contract_wrapper'; +import {TokenWrapper} from './token_wrapper'; +import {decorators} from '../utils/decorators'; +import {AbiDecoder} from '../utils/abi_decoder'; +import {ExchangeTransferSimulator} from '../utils/exchange_transfer_simulator'; +import {artifacts} from '../artifacts'; + +const SHOULD_VALIDATE_BY_DEFAULT = true; + +/** + * This class includes all the functionality related to calling methods and subscribing to + * events of the 0x Exchange smart contract. + */ +export class ExchangeWrapper extends ContractWrapper { + private _exchangeContractIfExists?: ExchangeContract; + private _orderValidationUtils: OrderValidationUtils; + private _tokenWrapper: TokenWrapper; + private _exchangeContractErrCodesToMsg = { + [ExchangeContractErrCodes.ERROR_FILL_EXPIRED]: ExchangeContractErrs.OrderFillExpired, + [ExchangeContractErrCodes.ERROR_CANCEL_EXPIRED]: ExchangeContractErrs.OrderFillExpired, + [ExchangeContractErrCodes.ERROR_FILL_NO_VALUE]: ExchangeContractErrs.OrderRemainingFillAmountZero, + [ExchangeContractErrCodes.ERROR_CANCEL_NO_VALUE]: ExchangeContractErrs.OrderRemainingFillAmountZero, + [ExchangeContractErrCodes.ERROR_FILL_TRUNCATION]: ExchangeContractErrs.OrderFillRoundingError, + [ExchangeContractErrCodes.ERROR_FILL_BALANCE_ALLOWANCE]: ExchangeContractErrs.FillBalanceAllowanceError, + }; + private _contractAddressIfExists?: string; + private static _getOrderAddressesAndValues(order: Order): [OrderAddresses, OrderValues] { + const orderAddresses: OrderAddresses = [ + order.maker, + order.taker, + order.makerTokenAddress, + order.takerTokenAddress, + order.feeRecipient, + ]; + const orderValues: OrderValues = [ + order.makerTokenAmount, + order.takerTokenAmount, + order.makerFee, + order.takerFee, + order.expirationUnixTimestampSec, + order.salt, + ]; + return [orderAddresses, orderValues]; + } + constructor(web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, + tokenWrapper: TokenWrapper, contractAddressIfExists?: string) { + super(web3Wrapper, abiDecoder); + this._tokenWrapper = tokenWrapper; + this._orderValidationUtils = new OrderValidationUtils(tokenWrapper, this); + this._contractAddressIfExists = contractAddressIfExists; + } + /** + * Returns the unavailable takerAmount of an order. Unavailable amount is defined as the total + * amount that has been filled or cancelled. The remaining takerAmount can be calculated by + * subtracting the unavailable amount from the total order takerAmount. + * @param orderHash The hex encoded orderHash for which you would like to retrieve the + * unavailable takerAmount. + * @param methodOpts Optional arguments this method accepts. + * @return The amount of the order (in taker tokens) that has either been filled or canceled. + */ + public async getUnavailableTakerAmountAsync(orderHash: string, + methodOpts?: MethodOpts): Promise<BigNumber> { + assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema); + + const exchangeContract = await this._getExchangeContractAsync(); + const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock; + let unavailableTakerTokenAmount = await exchangeContract.getUnavailableTakerTokenAmount.callAsync( + orderHash, defaultBlock, + ); + // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber + unavailableTakerTokenAmount = new BigNumber(unavailableTakerTokenAmount); + return unavailableTakerTokenAmount; + } + /** + * Retrieve the takerAmount of an order that has already been filled. + * @param orderHash The hex encoded orderHash for which you would like to retrieve the filled takerAmount. + * @param methodOpts Optional arguments this method accepts. + * @return The amount of the order (in taker tokens) that has already been filled. + */ + public async getFilledTakerAmountAsync(orderHash: string, methodOpts?: MethodOpts): Promise<BigNumber> { + assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema); + + const exchangeContract = await this._getExchangeContractAsync(); + const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock; + let fillAmountInBaseUnits = await exchangeContract.filled.callAsync(orderHash, defaultBlock); + // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber + fillAmountInBaseUnits = new BigNumber(fillAmountInBaseUnits); + return fillAmountInBaseUnits; + } + /** + * Retrieve the takerAmount of an order that has been cancelled. + * @param orderHash The hex encoded orderHash for which you would like to retrieve the + * cancelled takerAmount. + * @param methodOpts Optional arguments this method accepts. + * @return The amount of the order (in taker tokens) that has been cancelled. + */ + public async getCanceledTakerAmountAsync(orderHash: string, methodOpts?: MethodOpts): Promise<BigNumber> { + assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema); + + const exchangeContract = await this._getExchangeContractAsync(); + const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock; + let cancelledAmountInBaseUnits = await exchangeContract.cancelled.callAsync(orderHash, defaultBlock); + // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber + cancelledAmountInBaseUnits = new BigNumber(cancelledAmountInBaseUnits); + return cancelledAmountInBaseUnits; + } + /** + * Fills a signed order with an amount denominated in baseUnits of the taker token. + * Since the order in which transactions are included in the next block is indeterminate, race-conditions + * could arise where a users balance or allowance changes before the fillOrder executes. Because of this, + * we allow you to specify `shouldThrowOnInsufficientBalanceOrAllowance`. + * If false, the smart contract will not throw if the parties + * do not have sufficient balances/allowances, preserving gas costs. Setting it to true forgoes this check + * and causes the smart contract to throw (using all the gas supplied) instead. + * @param signedOrder An object that conforms to the SignedOrder interface. + * @param fillTakerTokenAmount The amount of the order (in taker tokens baseUnits) that + * you wish to fill. + * @param shouldThrowOnInsufficientBalanceOrAllowance Whether or not you wish for the contract call to throw + * if upon execution the tokens cannot be transferred. + * @param takerAddress The user Ethereum address who would like to fill this order. + * Must be available via the supplied Web3.Provider + * passed to 0x.js. + * @param orderTransactionOpts Optional arguments this method accepts. + * @return Transaction hash. + */ + @decorators.contractCallErrorHandler + public async fillOrderAsync(signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber, + shouldThrowOnInsufficientBalanceOrAllowance: boolean, + takerAddress: string, + orderTransactionOpts?: OrderTransactionOpts): Promise<string> { + assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); + assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); + + const exchangeInstance = await this._getExchangeContractAsync(); + const shouldValidate = _.isUndefined(orderTransactionOpts) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; + if (shouldValidate) { + const zrxTokenAddress = await this.getZRXTokenAddressAsync(); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper); + await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync( + exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress); + } + + const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(signedOrder); + + const gas = await exchangeInstance.fillOrder.estimateGasAsync( + orderAddresses, + orderValues, + fillTakerTokenAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + signedOrder.ecSignature.v, + signedOrder.ecSignature.r, + signedOrder.ecSignature.s, + { + from: takerAddress, + }, + ); + const txHash: string = await exchangeInstance.fillOrder.sendTransactionAsync( + orderAddresses, + orderValues, + fillTakerTokenAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + signedOrder.ecSignature.v, + signedOrder.ecSignature.r, + signedOrder.ecSignature.s, + { + from: takerAddress, + gas, + }, + ); + return txHash; + } + /** + * Sequentially and atomically fills signedOrders up to the specified takerTokenFillAmount. + * If the fill amount is reached - it succeeds and does not fill the rest of the orders. + * If fill amount is not reached - it fills as much of the fill amount as possible and succeeds. + * @param signedOrders The array of signedOrders that you would like to fill until + * takerTokenFillAmount is reached. + * @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill. + * @param shouldThrowOnInsufficientBalanceOrAllowance Whether or not you wish for the contract call to throw if + * upon execution any of the tokens cannot be transferred. + * If set to false, the call will continue to fill subsequent + * signedOrders even when some cannot be filled. + * @param takerAddress The user Ethereum address who would like to fill these + * orders. Must be available via the supplied Web3.Provider + * passed to 0x.js. + * @param orderTransactionOpts Optional arguments this method accepts. + * @return Transaction hash. + */ + @decorators.contractCallErrorHandler + public async fillOrdersUpToAsync(signedOrders: SignedOrder[], fillTakerTokenAmount: BigNumber, + shouldThrowOnInsufficientBalanceOrAllowance: boolean, + takerAddress: string, + orderTransactionOpts?: OrderTransactionOpts): Promise<string> { + assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); + const takerTokenAddresses = _.map(signedOrders, signedOrder => signedOrder.takerTokenAddress); + assert.hasAtMostOneUniqueValue(takerTokenAddresses, + ExchangeContractErrs.MultipleTakerTokensInFillUpToDisallowed); + const exchangeContractAddresses = _.map(signedOrders, signedOrder => signedOrder.exchangeContractAddress); + assert.hasAtMostOneUniqueValue(exchangeContractAddresses, + ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress); + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); + assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); + + const shouldValidate = _.isUndefined(orderTransactionOpts) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; + if (shouldValidate) { + const zrxTokenAddress = await this.getZRXTokenAddressAsync(); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper); + for (const signedOrder of signedOrders) { + await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync( + exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress); + } + } + + if (_.isEmpty(signedOrders)) { + throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem); + } + + const orderAddressesValuesAndSignatureArray = _.map(signedOrders, signedOrder => { + return [ + ...ExchangeWrapper._getOrderAddressesAndValues(signedOrder), + signedOrder.ecSignature.v, + signedOrder.ecSignature.r, + signedOrder.ecSignature.s, + ]; + }); + // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'( + const [orderAddressesArray, orderValuesArray, vArray, rArray, sArray] = _.unzip<any>( + orderAddressesValuesAndSignatureArray, + ); + + const exchangeInstance = await this._getExchangeContractAsync(); + const gas = await exchangeInstance.fillOrdersUpTo.estimateGasAsync( + orderAddressesArray, + orderValuesArray, + fillTakerTokenAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + vArray, + rArray, + sArray, + { + from: takerAddress, + }, + ); + const txHash = await exchangeInstance.fillOrdersUpTo.sendTransactionAsync( + orderAddressesArray, + orderValuesArray, + fillTakerTokenAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + vArray, + rArray, + sArray, + { + from: takerAddress, + gas, + }, + ); + return txHash; + } + /** + * Batch version of fillOrderAsync. + * Executes multiple fills atomically in a single transaction. + * If shouldThrowOnInsufficientBalanceOrAllowance is set to false, it will continue filling subsequent orders even + * when earlier ones fail. + * When shouldThrowOnInsufficientBalanceOrAllowance is set to true, if any fill fails, the entire batch fails. + * @param orderFillRequests An array of objects that conform to the + * OrderFillRequest interface. + * @param shouldThrowOnInsufficientBalanceOrAllowance Whether or not you wish for the contract call to throw + * if upon execution any of the tokens cannot be + * transferred. If set to false, the call will continue to + * fill subsequent signedOrders even when some + * cannot be filled. + * @param takerAddress The user Ethereum address who would like to fill + * these orders. Must be available via the supplied + * Web3.Provider passed to 0x.js. + * @param orderTransactionOpts Optional arguments this method accepts. + * @return Transaction hash. + */ + @decorators.contractCallErrorHandler + public async batchFillOrdersAsync(orderFillRequests: OrderFillRequest[], + shouldThrowOnInsufficientBalanceOrAllowance: boolean, + takerAddress: string, + orderTransactionOpts?: OrderTransactionOpts): Promise<string> { + assert.doesConformToSchema('orderFillRequests', orderFillRequests, schemas.orderFillRequestsSchema); + const exchangeContractAddresses = _.map( + orderFillRequests, + orderFillRequest => orderFillRequest.signedOrder.exchangeContractAddress, + ); + assert.hasAtMostOneUniqueValue(exchangeContractAddresses, + ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress); + assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); + const shouldValidate = _.isUndefined(orderTransactionOpts) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; + if (shouldValidate) { + const zrxTokenAddress = await this.getZRXTokenAddressAsync(); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper); + for (const orderFillRequest of orderFillRequests) { + await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync( + exchangeTradeEmulator, orderFillRequest.signedOrder, orderFillRequest.takerTokenFillAmount, + takerAddress, zrxTokenAddress, + ); + } + } + if (_.isEmpty(orderFillRequests)) { + throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem); + } + + const orderAddressesValuesAmountsAndSignatureArray = _.map(orderFillRequests, orderFillRequest => { + return [ + ...ExchangeWrapper._getOrderAddressesAndValues(orderFillRequest.signedOrder), + orderFillRequest.takerTokenFillAmount, + orderFillRequest.signedOrder.ecSignature.v, + orderFillRequest.signedOrder.ecSignature.r, + orderFillRequest.signedOrder.ecSignature.s, + ]; + }); + // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'( + const [orderAddressesArray, orderValuesArray, fillTakerTokenAmounts, vArray, rArray, sArray] = _.unzip<any>( + orderAddressesValuesAmountsAndSignatureArray, + ); + + const exchangeInstance = await this._getExchangeContractAsync(); + const gas = await exchangeInstance.batchFillOrders.estimateGasAsync( + orderAddressesArray, + orderValuesArray, + fillTakerTokenAmounts, + shouldThrowOnInsufficientBalanceOrAllowance, + vArray, + rArray, + sArray, + { + from: takerAddress, + }, + ); + const txHash = await exchangeInstance.batchFillOrders.sendTransactionAsync( + orderAddressesArray, + orderValuesArray, + fillTakerTokenAmounts, + shouldThrowOnInsufficientBalanceOrAllowance, + vArray, + rArray, + sArray, + { + from: takerAddress, + gas, + }, + ); + return txHash; + } + /** + * Attempts to fill a specific amount of an order. If the entire amount specified cannot be filled, + * the fill order is abandoned. + * @param signedOrder An object that conforms to the SignedOrder interface. The + * signedOrder you wish to fill. + * @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill. + * @param takerAddress The user Ethereum address who would like to fill this order. + * Must be available via the supplied Web3.Provider passed to 0x.js. + * @param orderTransactionOpts Optional arguments this method accepts. + * @return Transaction hash. + */ + @decorators.contractCallErrorHandler + public async fillOrKillOrderAsync(signedOrder: SignedOrder, fillTakerTokenAmount: BigNumber, + takerAddress: string, + orderTransactionOpts?: OrderTransactionOpts): Promise<string> { + assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); + + const exchangeInstance = await this._getExchangeContractAsync(); + + const shouldValidate = _.isUndefined(orderTransactionOpts) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; + if (shouldValidate) { + const zrxTokenAddress = await this.getZRXTokenAddressAsync(); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper); + await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync( + exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress); + } + + const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(signedOrder); + + const gas = await exchangeInstance.fillOrKillOrder.estimateGasAsync( + orderAddresses, + orderValues, + fillTakerTokenAmount, + signedOrder.ecSignature.v, + signedOrder.ecSignature.r, + signedOrder.ecSignature.s, + { + from: takerAddress, + }, + ); + const txHash = await exchangeInstance.fillOrKillOrder.sendTransactionAsync( + orderAddresses, + orderValues, + fillTakerTokenAmount, + signedOrder.ecSignature.v, + signedOrder.ecSignature.r, + signedOrder.ecSignature.s, + { + from: takerAddress, + gas, + }, + ); + return txHash; + } + /** + * Batch version of fillOrKill. Allows a taker to specify a batch of orders that will either be atomically + * filled (each to the specified fillAmount) or aborted. + * @param orderFillRequests An array of objects that conform to the OrderFillRequest interface. + * @param takerAddress The user Ethereum address who would like to fill there orders. + * Must be available via the supplied Web3.Provider passed to 0x.js. + * @param orderTransactionOpts Optional arguments this method accepts. + * @return Transaction hash. + */ + @decorators.contractCallErrorHandler + public async batchFillOrKillAsync(orderFillRequests: OrderFillRequest[], + takerAddress: string, + orderTransactionOpts?: OrderTransactionOpts): Promise<string> { + assert.doesConformToSchema('orderFillRequests', orderFillRequests, + schemas.orderFillRequestsSchema); + const exchangeContractAddresses = _.map( + orderFillRequests, + orderFillRequest => orderFillRequest.signedOrder.exchangeContractAddress, + ); + assert.hasAtMostOneUniqueValue(exchangeContractAddresses, + ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); + if (_.isEmpty(orderFillRequests)) { + throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem); + } + const exchangeInstance = await this._getExchangeContractAsync(); + + const shouldValidate = _.isUndefined(orderTransactionOpts) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; + if (shouldValidate) { + const zrxTokenAddress = await this.getZRXTokenAddressAsync(); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper); + for (const orderFillRequest of orderFillRequests) { + await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync( + exchangeTradeEmulator, orderFillRequest.signedOrder, orderFillRequest.takerTokenFillAmount, + takerAddress, zrxTokenAddress, + ); + } + } + + const orderAddressesValuesAndTakerTokenFillAmounts = _.map(orderFillRequests, request => { + return [ + ...ExchangeWrapper._getOrderAddressesAndValues(request.signedOrder), + request.takerTokenFillAmount, + request.signedOrder.ecSignature.v, + request.signedOrder.ecSignature.r, + request.signedOrder.ecSignature.s, + ]; + }); + + // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'( + const [orderAddresses, orderValues, fillTakerTokenAmounts, vParams, rParams, sParams] = + _.unzip<any>(orderAddressesValuesAndTakerTokenFillAmounts); + + const gas = await exchangeInstance.batchFillOrKillOrders.estimateGasAsync( + orderAddresses, + orderValues, + fillTakerTokenAmounts, + vParams, + rParams, + sParams, + { + from: takerAddress, + }, + ); + const txHash = await exchangeInstance.batchFillOrKillOrders.sendTransactionAsync( + orderAddresses, + orderValues, + fillTakerTokenAmounts, + vParams, + rParams, + sParams, + { + from: takerAddress, + gas, + }, + ); + return txHash; + } + /** + * Cancel a given fill amount of an order. Cancellations are cumulative. + * @param order An object that conforms to the Order or SignedOrder interface. + * The order you would like to cancel. + * @param cancelTakerTokenAmount The amount (specified in taker tokens) that you would like to cancel. + * @param transactionOpts Optional arguments this method accepts. + * @return Transaction hash. + */ + @decorators.contractCallErrorHandler + public async cancelOrderAsync(order: Order|SignedOrder, + cancelTakerTokenAmount: BigNumber, + orderTransactionOpts?: OrderTransactionOpts): Promise<string> { + assert.doesConformToSchema('order', order, schemas.orderSchema); + assert.isValidBaseUnitAmount('takerTokenCancelAmount', cancelTakerTokenAmount); + await assert.isSenderAddressAsync('order.maker', order.maker, this._web3Wrapper); + + const exchangeInstance = await this._getExchangeContractAsync(); + + const shouldValidate = _.isUndefined(orderTransactionOpts) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; + if (shouldValidate) { + const orderHash = utils.getOrderHashHex(order); + const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash); + await this._orderValidationUtils.validateCancelOrderThrowIfInvalidAsync( + order, cancelTakerTokenAmount, unavailableTakerTokenAmount); + } + + const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(order); + const gas = await exchangeInstance.cancelOrder.estimateGasAsync( + orderAddresses, + orderValues, + cancelTakerTokenAmount, + { + from: order.maker, + }, + ); + const txHash = await exchangeInstance.cancelOrder.sendTransactionAsync( + orderAddresses, + orderValues, + cancelTakerTokenAmount, + { + from: order.maker, + gas, + }, + ); + return txHash; + } + /** + * Batch version of cancelOrderAsync. Atomically cancels multiple orders in a single transaction. + * All orders must be from the same maker. + * @param orderCancellationRequests An array of objects that conform to the OrderCancellationRequest + * interface. + * @param transactionOpts Optional arguments this method accepts. + * @return Transaction hash. + */ + @decorators.contractCallErrorHandler + public async batchCancelOrdersAsync(orderCancellationRequests: OrderCancellationRequest[], + orderTransactionOpts?: OrderTransactionOpts): Promise<string> { + assert.doesConformToSchema('orderCancellationRequests', orderCancellationRequests, + schemas.orderCancellationRequestsSchema); + const exchangeContractAddresses = _.map( + orderCancellationRequests, + orderCancellationRequest => orderCancellationRequest.order.exchangeContractAddress, + ); + assert.hasAtMostOneUniqueValue(exchangeContractAddresses, + ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress); + const makers = _.map(orderCancellationRequests, cancellationRequest => cancellationRequest.order.maker); + assert.hasAtMostOneUniqueValue(makers, ExchangeContractErrs.MultipleMakersInSingleCancelBatchDisallowed); + const maker = makers[0]; + await assert.isSenderAddressAsync('maker', maker, this._web3Wrapper); + const shouldValidate = _.isUndefined(orderTransactionOpts) ? + SHOULD_VALIDATE_BY_DEFAULT : + orderTransactionOpts.shouldValidate; + if (shouldValidate) { + for (const orderCancellationRequest of orderCancellationRequests) { + const orderHash = utils.getOrderHashHex(orderCancellationRequest.order); + const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash); + await this._orderValidationUtils.validateCancelOrderThrowIfInvalidAsync( + orderCancellationRequest.order, orderCancellationRequest.takerTokenCancelAmount, + unavailableTakerTokenAmount, + ); + } + + } + if (_.isEmpty(orderCancellationRequests)) { + throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem); + } + const exchangeInstance = await this._getExchangeContractAsync(); + const orderAddressesValuesAndTakerTokenCancelAmounts = _.map(orderCancellationRequests, cancellationRequest => { + return [ + ...ExchangeWrapper._getOrderAddressesAndValues(cancellationRequest.order), + cancellationRequest.takerTokenCancelAmount, + ]; + }); + // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'( + const [orderAddresses, orderValues, cancelTakerTokenAmounts] = + _.unzip<any>(orderAddressesValuesAndTakerTokenCancelAmounts); + const gas = await exchangeInstance.batchCancelOrders.estimateGasAsync( + orderAddresses, + orderValues, + cancelTakerTokenAmounts, + { + from: maker, + }, + ); + const txHash = await exchangeInstance.batchCancelOrders.sendTransactionAsync( + orderAddresses, + orderValues, + cancelTakerTokenAmounts, + { + from: maker, + gas, + }, + ); + return txHash; + } + /** + * Subscribe to an event type emitted by the Exchange contract. + * @param eventName The exchange contract event you would like to subscribe to. + * @param indexFilterValues An object where the keys are indexed args returned by the event and + * the value is the value you are interested in. E.g `{maker: aUserAddressHex}` + * @param callback Callback that gets called when a log is added/removed + * @return Subscription token used later to unsubscribe + */ + public async subscribeAsync<ArgsType extends ExchangeContractEventArgs>( + eventName: ExchangeEvents, indexFilterValues: IndexedFilterValues, + callback: EventCallback<ArgsType>): Promise<string> { + assert.doesBelongToStringEnum('eventName', eventName, ExchangeEvents); + assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema); + assert.isFunction('callback', callback); + const exchangeContractAddress = await this.getContractAddressAsync(); + const subscriptionToken = this._subscribe<ArgsType>( + exchangeContractAddress, eventName, indexFilterValues, artifacts.ExchangeArtifact.abi, callback, + ); + return subscriptionToken; + } + /** + * Cancel a subscription + * @param subscriptionToken Subscription token returned by `subscribe()` + */ + public unsubscribe(subscriptionToken: string): void { + this._unsubscribe(subscriptionToken); + } + /** + * Gets historical logs without creating a subscription + * @param eventName The exchange contract event you would like to subscribe to. + * @param subscriptionOpts Subscriptions options that let you configure the subscription. + * @param indexFilterValues An object where the keys are indexed args returned by the event and + * the value is the value you are interested in. E.g `{_from: aUserAddressHex}` + * @return Array of logs that match the parameters + */ + public async getLogsAsync<ArgsType extends ExchangeContractEventArgs>( + eventName: ExchangeEvents, subscriptionOpts: SubscriptionOpts, indexFilterValues: IndexedFilterValues, + ): Promise<Array<LogWithDecodedArgs<ArgsType>>> { + assert.doesBelongToStringEnum('eventName', eventName, ExchangeEvents); + assert.doesConformToSchema('subscriptionOpts', subscriptionOpts, schemas.subscriptionOptsSchema); + assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema); + const exchangeContractAddress = await this.getContractAddressAsync(); + const logs = await this._getLogsAsync<ArgsType>( + exchangeContractAddress, eventName, subscriptionOpts, indexFilterValues, artifacts.ExchangeArtifact.abi, + ); + return logs; + } + /** + * Retrieves the Ethereum address of the Exchange contract deployed on the network + * that the user-passed web3 provider is connected to. + * @returns The Ethereum address of the Exchange contract being used. + */ + public async getContractAddressAsync(): Promise<string> { + const exchangeInstance = await this._getExchangeContractAsync(); + const exchangeAddress = exchangeInstance.address; + return exchangeAddress; + } + /** + * Checks if order is still fillable and throws an error otherwise. Useful for orderbook + * pruning where you want to remove stale orders without knowing who the taker will be. + * @param signedOrder An object that conforms to the SignedOrder interface. The + * signedOrder you wish to validate. + * @param opts An object that conforms to the ValidateOrderFillableOpts + * interface. Allows specifying a specific fillTakerTokenAmount + * to validate for. + */ + public async validateOrderFillableOrThrowAsync( + signedOrder: SignedOrder, opts?: ValidateOrderFillableOpts, + ): Promise<void> { + assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); + const zrxTokenAddress = await this.getZRXTokenAddressAsync(); + const expectedFillTakerTokenAmount = !_.isUndefined(opts) ? opts.expectedFillTakerTokenAmount : undefined; + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper); + await this._orderValidationUtils.validateOrderFillableOrThrowAsync( + exchangeTradeEmulator, signedOrder, zrxTokenAddress, expectedFillTakerTokenAmount, + ); + } + /** + * Checks if order fill will succeed and throws an error otherwise. + * @param signedOrder An object that conforms to the SignedOrder interface. The + * signedOrder you wish to fill. + * @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill. + * @param takerAddress The user Ethereum address who would like to fill this order. + * Must be available via the supplied Web3.Provider passed to 0x.js. + */ + public async validateFillOrderThrowIfInvalidAsync(signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber, + takerAddress: string): Promise<void> { + assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); + const zrxTokenAddress = await this.getZRXTokenAddressAsync(); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper); + await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync( + exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress); + } + /** + * Checks if cancelling a given order will succeed and throws an informative error if it won't. + * @param order An object that conforms to the Order or SignedOrder interface. + * The order you would like to cancel. + * @param cancelTakerTokenAmount The amount (specified in taker tokens) that you would like to cancel. + */ + public async validateCancelOrderThrowIfInvalidAsync( + order: Order, cancelTakerTokenAmount: BigNumber): Promise<void> { + assert.doesConformToSchema('order', order, schemas.orderSchema); + assert.isValidBaseUnitAmount('cancelTakerTokenAmount', cancelTakerTokenAmount); + const orderHash = utils.getOrderHashHex(order); + const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash); + await this._orderValidationUtils.validateCancelOrderThrowIfInvalidAsync( + order, cancelTakerTokenAmount, unavailableTakerTokenAmount); + } + /** + * Checks if calling fillOrKill on a given order will succeed and throws an informative error if it won't. + * @param signedOrder An object that conforms to the SignedOrder interface. The + * signedOrder you wish to fill. + * @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill. + * @param takerAddress The user Ethereum address who would like to fill this order. + * Must be available via the supplied Web3.Provider passed to 0x.js. + */ + public async validateFillOrKillOrderThrowIfInvalidAsync(signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber, + takerAddress: string): Promise<void> { + assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); + const zrxTokenAddress = await this.getZRXTokenAddressAsync(); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper); + await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync( + exchangeTradeEmulator, signedOrder, fillTakerTokenAmount, takerAddress, zrxTokenAddress); + } + /** + * Checks if rounding error will be > 0.1% when computing makerTokenAmount by doing: + * `(fillTakerTokenAmount * makerTokenAmount) / takerTokenAmount`. + * 0x Protocol does not accept any trades that result in large rounding errors. This means that tokens with few or + * no decimals can only be filled in quantities and ratios that avoid large rounding errors. + * @param fillTakerTokenAmount The amount of the order (in taker tokens baseUnits) that you wish to fill. + * @param takerTokenAmount The order size on the taker side + * @param makerTokenAmount The order size on the maker side + */ + public async isRoundingErrorAsync(fillTakerTokenAmount: BigNumber, + takerTokenAmount: BigNumber, + makerTokenAmount: BigNumber): Promise<boolean> { + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); + assert.isValidBaseUnitAmount('takerTokenAmount', takerTokenAmount); + assert.isValidBaseUnitAmount('makerTokenAmount', makerTokenAmount); + const exchangeInstance = await this._getExchangeContractAsync(); + const isRoundingError = await exchangeInstance.isRoundingError.callAsync( + fillTakerTokenAmount, takerTokenAmount, makerTokenAmount, + ); + return isRoundingError; + } + /** + * Checks if logs contain LogError, which is emmited by Exchange contract on transaction failure. + * @param logs Transaction logs as returned by `zeroEx.awaitTransactionMinedAsync` + */ + public throwLogErrorsAsErrors(logs: Array<LogWithDecodedArgs<DecodedLogArgs>|Web3.LogEntry>): void { + const errLog = _.find(logs, { + event: ExchangeEvents.LogError, + }) as LogWithDecodedArgs<LogErrorContractEventArgs>|undefined; + if (!_.isUndefined(errLog)) { + const logArgs = errLog.args; + const errCode = logArgs.errorId.toNumber(); + const errMessage = this._exchangeContractErrCodesToMsg[errCode]; + throw new Error(errMessage); + } + } + /** + * Returns the ZRX token address used by the exchange contract. + * @return Address of ZRX token + */ + public async getZRXTokenAddressAsync(): Promise<string> { + const exchangeInstance = await this._getExchangeContractAsync(); + const ZRXtokenAddress = await exchangeInstance.ZRX_TOKEN_CONTRACT.callAsync(); + return ZRXtokenAddress; + } + private async _invalidateContractInstancesAsync(): Promise<void> { + this.unsubscribeAll(); + delete this._exchangeContractIfExists; + } + private async _isValidSignatureUsingContractCallAsync(dataHex: string, ecSignature: ECSignature, + signerAddressHex: string): Promise<boolean> { + assert.isHexString('dataHex', dataHex); + assert.doesConformToSchema('ecSignature', ecSignature, schemas.ecSignatureSchema); + assert.isETHAddressHex('signerAddressHex', signerAddressHex); + + const exchangeInstance = await this._getExchangeContractAsync(); + + const isValidSignature = await exchangeInstance.isValidSignature.callAsync( + signerAddressHex, + dataHex, + ecSignature.v, + ecSignature.r, + ecSignature.s, + ); + return isValidSignature; + } + private async _getOrderHashHexUsingContractCallAsync(order: Order|SignedOrder): Promise<string> { + const exchangeInstance = await this._getExchangeContractAsync(); + const [orderAddresses, orderValues] = ExchangeWrapper._getOrderAddressesAndValues(order); + const orderHashHex = await exchangeInstance.getOrderHash.callAsync(orderAddresses, orderValues); + return orderHashHex; + } + private async _getExchangeContractAsync(): Promise<ExchangeContract> { + if (!_.isUndefined(this._exchangeContractIfExists)) { + return this._exchangeContractIfExists; + } + const contractInstance = await this._instantiateContractIfExistsAsync<ExchangeContract>( + artifacts.ExchangeArtifact, this._contractAddressIfExists, + ); + this._exchangeContractIfExists = contractInstance as ExchangeContract; + return this._exchangeContractIfExists; + } + private async _getTokenTransferProxyAddressAsync(): Promise<string> { + const exchangeInstance = await this._getExchangeContractAsync(); + const tokenTransferProxyAddress = await exchangeInstance.TOKEN_TRANSFER_PROXY_CONTRACT.callAsync(); + const tokenTransferProxyAddressLowerCase = tokenTransferProxyAddress.toLowerCase(); + return tokenTransferProxyAddressLowerCase; + } +} diff --git a/packages/0x.js/src/contract_wrappers/token_registry_wrapper.ts b/packages/0x.js/src/contract_wrappers/token_registry_wrapper.ts new file mode 100644 index 000000000..2cc5a9aa0 --- /dev/null +++ b/packages/0x.js/src/contract_wrappers/token_registry_wrapper.ts @@ -0,0 +1,122 @@ +import * as _ from 'lodash'; +import {Web3Wrapper} from '../web3_wrapper'; +import {assert} from '../utils/assert'; +import {Token, TokenRegistryContract, TokenMetadata} from '../types'; +import {constants} from '../utils/constants'; +import {ContractWrapper} from './contract_wrapper'; +import {artifacts} from '../artifacts'; + +/** + * This class includes all the functionality related to interacting with the 0x Token Registry smart contract. + */ +export class TokenRegistryWrapper extends ContractWrapper { + private _tokenRegistryContractIfExists?: TokenRegistryContract; + private _contractAddressIfExists?: string; + constructor(web3Wrapper: Web3Wrapper, contractAddressIfExists?: string) { + super(web3Wrapper); + this._contractAddressIfExists = contractAddressIfExists; + } + /** + * Retrieves all the tokens currently listed in the Token Registry smart contract + * @return An array of objects that conform to the Token interface. + */ + public async getTokensAsync(): Promise<Token[]> { + const tokenRegistryContract = await this._getTokenRegistryContractAsync(); + + const addresses = await this.getTokenAddressesAsync(); + const tokenPromises: Array<Promise<Token|undefined>> = _.map( + addresses, + (address: string) => (this.getTokenIfExistsAsync(address)), + ); + const tokens = await Promise.all(tokenPromises); + return tokens as Token[]; + } + /** + * Retrieves all the addresses of the tokens currently listed in the Token Registry smart contract + * @return An array of token addresses. + */ + public async getTokenAddressesAsync(): Promise<string[]> { + const tokenRegistryContract = await this._getTokenRegistryContractAsync(); + const addresses = await tokenRegistryContract.getTokenAddresses.callAsync(); + return addresses; + } + /** + * Retrieves a token by address currently listed in the Token Registry smart contract + * @return An object that conforms to the Token interface or undefined if token not found. + */ + public async getTokenIfExistsAsync(address: string): Promise<Token|undefined> { + assert.isETHAddressHex('address', address); + + const tokenRegistryContract = await this._getTokenRegistryContractAsync(); + const metadata = await tokenRegistryContract.getTokenMetaData.callAsync(address); + const token = this._createTokenFromMetadata(metadata); + return token; + } + public async getTokenAddressBySymbolIfExistsAsync(symbol: string): Promise<string|undefined> { + assert.isString('symbol', symbol); + const tokenRegistryContract = await this._getTokenRegistryContractAsync(); + const addressIfExists = await tokenRegistryContract.getTokenAddressBySymbol.callAsync(symbol); + if (addressIfExists === constants.NULL_ADDRESS) { + return undefined; + } + return addressIfExists; + } + public async getTokenAddressByNameIfExistsAsync(name: string): Promise<string|undefined> { + assert.isString('name', name); + const tokenRegistryContract = await this._getTokenRegistryContractAsync(); + const addressIfExists = await tokenRegistryContract.getTokenAddressByName.callAsync(name); + if (addressIfExists === constants.NULL_ADDRESS) { + return undefined; + } + return addressIfExists; + } + public async getTokenBySymbolIfExistsAsync(symbol: string): Promise<Token|undefined> { + assert.isString('symbol', symbol); + const tokenRegistryContract = await this._getTokenRegistryContractAsync(); + const metadata = await tokenRegistryContract.getTokenBySymbol.callAsync(symbol); + const token = this._createTokenFromMetadata(metadata); + return token; + } + public async getTokenByNameIfExistsAsync(name: string): Promise<Token|undefined> { + assert.isString('name', name); + const tokenRegistryContract = await this._getTokenRegistryContractAsync(); + const metadata = await tokenRegistryContract.getTokenByName.callAsync(name); + const token = this._createTokenFromMetadata(metadata); + return token; + } + /** + * Retrieves the Ethereum address of the TokenRegistry contract deployed on the network + * that the user-passed web3 provider is connected to. + * @returns The Ethereum address of the TokenRegistry contract being used. + */ + public async getContractAddressAsync(): Promise<string> { + const tokenRegistryInstance = await this._getTokenRegistryContractAsync(); + const tokenRegistryAddress = tokenRegistryInstance.address; + return tokenRegistryAddress; + } + private _createTokenFromMetadata(metadata: TokenMetadata): Token|undefined { + if (metadata[0] === constants.NULL_ADDRESS) { + return undefined; + } + const token = { + address: metadata[0], + name: metadata[1], + symbol: metadata[2], + decimals: metadata[3].toNumber(), + }; + return token; + } + private _invalidateContractInstance(): void { + delete this._tokenRegistryContractIfExists; + } + private async _getTokenRegistryContractAsync(): Promise<TokenRegistryContract> { + if (!_.isUndefined(this._tokenRegistryContractIfExists)) { + return this._tokenRegistryContractIfExists; + } + const contractInstance = await this._instantiateContractIfExistsAsync<TokenRegistryContract>( + artifacts.TokenRegistryArtifact, this._contractAddressIfExists, + ); + this._tokenRegistryContractIfExists = contractInstance as TokenRegistryContract; + return this._tokenRegistryContractIfExists; + } +} diff --git a/packages/0x.js/src/contract_wrappers/token_transfer_proxy_wrapper.ts b/packages/0x.js/src/contract_wrappers/token_transfer_proxy_wrapper.ts new file mode 100644 index 000000000..f81845af9 --- /dev/null +++ b/packages/0x.js/src/contract_wrappers/token_transfer_proxy_wrapper.ts @@ -0,0 +1,60 @@ +import * as _ from 'lodash'; +import {Web3Wrapper} from '../web3_wrapper'; +import {ContractWrapper} from './contract_wrapper'; +import {artifacts} from '../artifacts'; +import {TokenTransferProxyContract} from '../types'; + +/** + * This class includes the functionality related to interacting with the TokenTransferProxy contract. + */ +export class TokenTransferProxyWrapper extends ContractWrapper { + private _tokenTransferProxyContractIfExists?: TokenTransferProxyContract; + private _tokenTransferProxyContractAddressFetcher: () => Promise<string>; + constructor(web3Wrapper: Web3Wrapper, tokenTransferProxyContractAddressFetcher: () => Promise<string>) { + super(web3Wrapper); + this._tokenTransferProxyContractAddressFetcher = tokenTransferProxyContractAddressFetcher; + } + /** + * Check if the Exchange contract address is authorized by the TokenTransferProxy contract. + * @param exchangeContractAddress The hex encoded address of the Exchange contract to call. + * @return Whether the exchangeContractAddress is authorized. + */ + public async isAuthorizedAsync(exchangeContractAddress: string): Promise<boolean> { + const tokenTransferProxyContractInstance = await this._getTokenTransferProxyContractAsync(); + const isAuthorized = await tokenTransferProxyContractInstance.authorized.callAsync(exchangeContractAddress); + return isAuthorized; + } + /** + * Get the list of all Exchange contract addresses authorized by the TokenTransferProxy contract. + * @return The list of authorized addresses. + */ + public async getAuthorizedAddressesAsync(): Promise<string[]> { + const tokenTransferProxyContractInstance = await this._getTokenTransferProxyContractAsync(); + const authorizedAddresses = await tokenTransferProxyContractInstance.getAuthorizedAddresses.callAsync(); + return authorizedAddresses; + } + /** + * Retrieves the Ethereum address of the TokenTransferProxy contract deployed on the network + * that the user-passed web3 provider is connected to. + * @returns The Ethereum address of the TokenTransferProxy contract being used. + */ + public async getContractAddressAsync(): Promise<string> { + const proxyInstance = await this._getTokenTransferProxyContractAsync(); + const proxyAddress = proxyInstance.address; + return proxyAddress; + } + private _invalidateContractInstance(): void { + delete this._tokenTransferProxyContractIfExists; + } + private async _getTokenTransferProxyContractAsync(): Promise<TokenTransferProxyContract> { + if (!_.isUndefined(this._tokenTransferProxyContractIfExists)) { + return this._tokenTransferProxyContractIfExists; + } + const contractAddress = await this._tokenTransferProxyContractAddressFetcher(); + const contractInstance = await this._instantiateContractIfExistsAsync<TokenTransferProxyContract>( + artifacts.TokenTransferProxyArtifact, contractAddress, + ); + this._tokenTransferProxyContractIfExists = contractInstance as TokenTransferProxyContract; + return this._tokenTransferProxyContractIfExists; + } +} diff --git a/packages/0x.js/src/contract_wrappers/token_wrapper.ts b/packages/0x.js/src/contract_wrappers/token_wrapper.ts new file mode 100644 index 000000000..614ac19d4 --- /dev/null +++ b/packages/0x.js/src/contract_wrappers/token_wrapper.ts @@ -0,0 +1,313 @@ +import * as _ from 'lodash'; +import BigNumber from 'bignumber.js'; +import {schemas} from '0x-json-schemas'; +import {Web3Wrapper} from '../web3_wrapper'; +import {assert} from '../utils/assert'; +import {constants} from '../utils/constants'; +import {ContractWrapper} from './contract_wrapper'; +import {AbiDecoder} from '../utils/abi_decoder'; +import {artifacts} from '../artifacts'; +import { + TokenContract, + ZeroExError, + TokenEvents, + IndexedFilterValues, + SubscriptionOpts, + MethodOpts, + LogWithDecodedArgs, + EventCallback, + TokenContractEventArgs, +} from '../types'; + +const ALLOWANCE_TO_ZERO_GAS_AMOUNT = 47275; + +/** + * This class includes all the functionality related to interacting with ERC20 token contracts. + * All ERC20 method calls are supported, along with some convenience methods for getting/setting allowances + * to the 0x Proxy smart contract. + */ +export class TokenWrapper extends ContractWrapper { + public UNLIMITED_ALLOWANCE_IN_BASE_UNITS = constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; + private _tokenContractsByAddress: {[address: string]: TokenContract}; + private _tokenTransferProxyContractAddressFetcher: () => Promise<string>; + constructor(web3Wrapper: Web3Wrapper, abiDecoder: AbiDecoder, + tokenTransferProxyContractAddressFetcher: () => Promise<string>) { + super(web3Wrapper, abiDecoder); + this._tokenContractsByAddress = {}; + this._tokenTransferProxyContractAddressFetcher = tokenTransferProxyContractAddressFetcher; + } + /** + * Retrieves an owner's ERC20 token balance. + * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed. + * @param ownerAddress The hex encoded user Ethereum address whose balance you would like to check. + * @param methodOpts Optional arguments this method accepts. + * @return The owner's ERC20 token balance in base units. + */ + public async getBalanceAsync(tokenAddress: string, ownerAddress: string, + methodOpts?: MethodOpts): Promise<BigNumber> { + assert.isETHAddressHex('ownerAddress', ownerAddress); + assert.isETHAddressHex('tokenAddress', tokenAddress); + + const tokenContract = await this._getTokenContractAsync(tokenAddress); + const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock; + let balance = await tokenContract.balanceOf.callAsync(ownerAddress, defaultBlock); + // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber + balance = new BigNumber(balance); + return balance; + } + /** + * Sets the spender's allowance to a specified number of baseUnits on behalf of the owner address. + * Equivalent to the ERC20 spec method `approve`. + * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed. + * @param ownerAddress The hex encoded user Ethereum address who would like to set an allowance + * for spenderAddress. + * @param spenderAddress The hex encoded user Ethereum address who will be able to spend the set allowance. + * @param amountInBaseUnits The allowance amount you would like to set. + * @return Transaction hash. + */ + public async setAllowanceAsync(tokenAddress: string, ownerAddress: string, spenderAddress: string, + amountInBaseUnits: BigNumber): Promise<string> { + await assert.isSenderAddressAsync('ownerAddress', ownerAddress, this._web3Wrapper); + assert.isETHAddressHex('spenderAddress', spenderAddress); + assert.isETHAddressHex('tokenAddress', tokenAddress); + assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); + + const tokenContract = await this._getTokenContractAsync(tokenAddress); + // Hack: for some reason default estimated gas amount causes `base fee exceeds gas limit` exception + // on testrpc. Probably related to https://github.com/ethereumjs/testrpc/issues/294 + // TODO: Debug issue in testrpc and submit a PR, then remove this hack + const networkIdIfExists = await this._web3Wrapper.getNetworkIdIfExistsAsync(); + const gas = networkIdIfExists === constants.TESTRPC_NETWORK_ID ? ALLOWANCE_TO_ZERO_GAS_AMOUNT : undefined; + const txHash = await tokenContract.approve.sendTransactionAsync(spenderAddress, amountInBaseUnits, { + from: ownerAddress, + gas, + }); + return txHash; + } + /** + * Sets the spender's allowance to an unlimited number of baseUnits on behalf of the owner address. + * Equivalent to the ERC20 spec method `approve`. + * Setting an unlimited allowance will lower the gas cost for filling orders involving tokens that forego updating + * allowances set to the max amount (e.g ZRX, WETH) + * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed. + * @param ownerAddress The hex encoded user Ethereum address who would like to set an allowance + * for spenderAddress. + * @param spenderAddress The hex encoded user Ethereum address who will be able to spend the set allowance. + * @return Transaction hash. + */ + public async setUnlimitedAllowanceAsync(tokenAddress: string, ownerAddress: string, + spenderAddress: string): Promise<string> { + const txHash = await this.setAllowanceAsync( + tokenAddress, ownerAddress, spenderAddress, this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + ); + return txHash; + } + /** + * Retrieves the owners allowance in baseUnits set to the spender's address. + * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed. + * @param ownerAddress The hex encoded user Ethereum address whose allowance to spenderAddress + * you would like to retrieve. + * @param spenderAddress The hex encoded user Ethereum address who can spend the allowance you are fetching. + * @param methodOpts Optional arguments this method accepts. + */ + public async getAllowanceAsync(tokenAddress: string, ownerAddress: string, + spenderAddress: string, methodOpts?: MethodOpts): Promise<BigNumber> { + assert.isETHAddressHex('ownerAddress', ownerAddress); + assert.isETHAddressHex('tokenAddress', tokenAddress); + + const tokenContract = await this._getTokenContractAsync(tokenAddress); + const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock; + let allowanceInBaseUnits = await tokenContract.allowance.callAsync(ownerAddress, spenderAddress, defaultBlock); + // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber + allowanceInBaseUnits = new BigNumber(allowanceInBaseUnits); + return allowanceInBaseUnits; + } + /** + * Retrieves the owner's allowance in baseUnits set to the 0x proxy contract. + * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed. + * @param ownerAddress The hex encoded user Ethereum address whose proxy contract allowance we are retrieving. + * @param methodOpts Optional arguments this method accepts. + */ + public async getProxyAllowanceAsync(tokenAddress: string, ownerAddress: string, + methodOpts?: MethodOpts): Promise<BigNumber> { + assert.isETHAddressHex('ownerAddress', ownerAddress); + assert.isETHAddressHex('tokenAddress', tokenAddress); + + const proxyAddress = await this._getTokenTransferProxyAddressAsync(); + const allowanceInBaseUnits = await this.getAllowanceAsync(tokenAddress, ownerAddress, proxyAddress, methodOpts); + return allowanceInBaseUnits; + } + /** + * Sets the 0x proxy contract's allowance to a specified number of a tokens' baseUnits on behalf + * of an owner address. + * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed. + * @param ownerAddress The hex encoded user Ethereum address who is setting an allowance + * for the Proxy contract. + * @param amountInBaseUnits The allowance amount specified in baseUnits. + * @return Transaction hash. + */ + public async setProxyAllowanceAsync(tokenAddress: string, ownerAddress: string, + amountInBaseUnits: BigNumber): Promise<string> { + assert.isETHAddressHex('ownerAddress', ownerAddress); + assert.isETHAddressHex('tokenAddress', tokenAddress); + assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); + + const proxyAddress = await this._getTokenTransferProxyAddressAsync(); + const txHash = await this.setAllowanceAsync(tokenAddress, ownerAddress, proxyAddress, amountInBaseUnits); + return txHash; + } + /** + * Sets the 0x proxy contract's allowance to a unlimited number of a tokens' baseUnits on behalf + * of an owner address. + * Setting an unlimited allowance will lower the gas cost for filling orders involving tokens that forego updating + * allowances set to the max amount (e.g ZRX, WETH) + * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed. + * @param ownerAddress The hex encoded user Ethereum address who is setting an allowance + * for the Proxy contract. + * @return Transaction hash. + */ + public async setUnlimitedProxyAllowanceAsync(tokenAddress: string, ownerAddress: string): Promise<string> { + const txHash = await this.setProxyAllowanceAsync( + tokenAddress, ownerAddress, this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + ); + return txHash; + } + /** + * Transfers `amountInBaseUnits` ERC20 tokens from `fromAddress` to `toAddress`. + * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed. + * @param fromAddress The hex encoded user Ethereum address that will send the funds. + * @param toAddress The hex encoded user Ethereum address that will receive the funds. + * @param amountInBaseUnits The amount (specified in baseUnits) of the token to transfer. + * @return Transaction hash. + */ + public async transferAsync(tokenAddress: string, fromAddress: string, toAddress: string, + amountInBaseUnits: BigNumber): Promise<string> { + assert.isETHAddressHex('tokenAddress', tokenAddress); + await assert.isSenderAddressAsync('fromAddress', fromAddress, this._web3Wrapper); + assert.isETHAddressHex('toAddress', toAddress); + assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); + + const tokenContract = await this._getTokenContractAsync(tokenAddress); + + const fromAddressBalance = await this.getBalanceAsync(tokenAddress, fromAddress); + if (fromAddressBalance.lessThan(amountInBaseUnits)) { + throw new Error(ZeroExError.InsufficientBalanceForTransfer); + } + + const txHash = await tokenContract.transfer.sendTransactionAsync(toAddress, amountInBaseUnits, { + from: fromAddress, + }); + return txHash; + } + /** + * Transfers `amountInBaseUnits` ERC20 tokens from `fromAddress` to `toAddress`. + * Requires the fromAddress to have sufficient funds and to have approved an allowance of + * `amountInBaseUnits` to `senderAddress`. + * @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed. + * @param fromAddress The hex encoded user Ethereum address whose funds are being sent. + * @param toAddress The hex encoded user Ethereum address that will receive the funds. + * @param senderAddress The hex encoded user Ethereum address whose initiates the fund transfer. The + * `fromAddress` must have set an allowance to the `senderAddress` + * before this call. + * @param amountInBaseUnits The amount (specified in baseUnits) of the token to transfer. + * @return Transaction hash. + */ + public async transferFromAsync(tokenAddress: string, fromAddress: string, toAddress: string, + senderAddress: string, amountInBaseUnits: BigNumber): + Promise<string> { + assert.isETHAddressHex('tokenAddress', tokenAddress); + assert.isETHAddressHex('fromAddress', fromAddress); + assert.isETHAddressHex('toAddress', toAddress); + await assert.isSenderAddressAsync('senderAddress', senderAddress, this._web3Wrapper); + assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); + + const tokenContract = await this._getTokenContractAsync(tokenAddress); + + const fromAddressAllowance = await this.getAllowanceAsync(tokenAddress, fromAddress, senderAddress); + if (fromAddressAllowance.lessThan(amountInBaseUnits)) { + throw new Error(ZeroExError.InsufficientAllowanceForTransfer); + } + + const fromAddressBalance = await this.getBalanceAsync(tokenAddress, fromAddress); + if (fromAddressBalance.lessThan(amountInBaseUnits)) { + throw new Error(ZeroExError.InsufficientBalanceForTransfer); + } + + const txHash = await tokenContract.transferFrom.sendTransactionAsync( + fromAddress, toAddress, amountInBaseUnits, + { + from: senderAddress, + }, + ); + return txHash; + } + /** + * Subscribe to an event type emitted by the Token contract. + * @param tokenAddress The hex encoded address where the ERC20 token is deployed. + * @param eventName The token contract event you would like to subscribe to. + * @param indexFilterValues An object where the keys are indexed args returned by the event and + * the value is the value you are interested in. E.g `{maker: aUserAddressHex}` + * @param callback Callback that gets called when a log is added/removed + * @return Subscription token used later to unsubscribe + */ + public subscribe<ArgsType extends TokenContractEventArgs>( + tokenAddress: string, eventName: TokenEvents, indexFilterValues: IndexedFilterValues, + callback: EventCallback<ArgsType>): string { + assert.isETHAddressHex('tokenAddress', tokenAddress); + assert.doesBelongToStringEnum('eventName', eventName, TokenEvents); + assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema); + assert.isFunction('callback', callback); + const subscriptionToken = this._subscribe<ArgsType>( + tokenAddress, eventName, indexFilterValues, artifacts.TokenArtifact.abi, callback, + ); + return subscriptionToken; + } + /** + * Cancel a subscription + * @param subscriptionToken Subscription token returned by `subscribe()` + */ + public unsubscribe(subscriptionToken: string): void { + this._unsubscribe(subscriptionToken); + } + /** + * Gets historical logs without creating a subscription + * @param tokenAddress An address of the token that emmited the logs. + * @param eventName The token contract event you would like to subscribe to. + * @param subscriptionOpts Subscriptions options that let you configure the subscription. + * @param indexFilterValues An object where the keys are indexed args returned by the event and + * the value is the value you are interested in. E.g `{_from: aUserAddressHex}` + * @return Array of logs that match the parameters + */ + public async getLogsAsync<ArgsType extends TokenContractEventArgs>( + tokenAddress: string, eventName: TokenEvents, subscriptionOpts: SubscriptionOpts, + indexFilterValues: IndexedFilterValues): Promise<Array<LogWithDecodedArgs<ArgsType>>> { + assert.isETHAddressHex('tokenAddress', tokenAddress); + assert.doesBelongToStringEnum('eventName', eventName, TokenEvents); + assert.doesConformToSchema('subscriptionOpts', subscriptionOpts, schemas.subscriptionOptsSchema); + assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema); + const logs = await this._getLogsAsync<ArgsType>( + tokenAddress, eventName, subscriptionOpts, indexFilterValues, artifacts.TokenArtifact.abi, + ); + return logs; + } + private _invalidateContractInstancesAsync(): void { + this.unsubscribeAll(); + this._tokenContractsByAddress = {}; + } + private async _getTokenContractAsync(tokenAddress: string): Promise<TokenContract> { + let tokenContract = this._tokenContractsByAddress[tokenAddress]; + if (!_.isUndefined(tokenContract)) { + return tokenContract; + } + const contractInstance = await this._instantiateContractIfExistsAsync<TokenContract>( + artifacts.TokenArtifact, tokenAddress, + ); + tokenContract = contractInstance as TokenContract; + this._tokenContractsByAddress[tokenAddress] = tokenContract; + return tokenContract; + } + private async _getTokenTransferProxyAddressAsync(): Promise<string> { + const tokenTransferProxyContractAddress = await this._tokenTransferProxyContractAddressFetcher(); + return tokenTransferProxyContractAddress; + } +} |