From a655cd046c79d53de69e71bf9217155a00a2ebe8 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Wed, 27 Jun 2018 11:39:46 +0300 Subject: Refactor ERC20 token wrapper and it's tests --- .../src/contract_wrappers/erc20_token_wrapper.ts | 443 ++++++++++++++ .../src/contract_wrappers/token_wrapper.ts | 443 -------------- .../contract-wrappers/test/erc20_wrapper_test.ts | 660 +++++++++++++++++++++ .../contract-wrappers/test/token_wrapper_test.ts | 603 ------------------- 4 files changed, 1103 insertions(+), 1046 deletions(-) create mode 100644 packages/contract-wrappers/src/contract_wrappers/erc20_token_wrapper.ts delete mode 100644 packages/contract-wrappers/src/contract_wrappers/token_wrapper.ts create mode 100644 packages/contract-wrappers/test/erc20_wrapper_test.ts delete mode 100644 packages/contract-wrappers/test/token_wrapper_test.ts (limited to 'packages') diff --git a/packages/contract-wrappers/src/contract_wrappers/erc20_token_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/erc20_token_wrapper.ts new file mode 100644 index 000000000..315b0314a --- /dev/null +++ b/packages/contract-wrappers/src/contract_wrappers/erc20_token_wrapper.ts @@ -0,0 +1,443 @@ +import { schemas } from '@0xproject/json-schemas'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { ContractAbi, LogWithDecodedArgs } from 'ethereum-types'; +import * as _ from 'lodash'; + +import { artifacts } from '../artifacts'; +import { + BlockRange, + ContractWrappersError, + EventCallback, + IndexedFilterValues, + MethodOpts, + TransactionOpts, +} from '../types'; +import { assert } from '../utils/assert'; +import { constants } from '../utils/constants'; + +import { ContractWrapper } from './contract_wrapper'; +import { ERC20ProxyWrapper } from './erc20_proxy_wrapper'; +import { ERC20TokenContract, ERC20TokenEventArgs, ERC20TokenEvents } from './generated/erc20_token'; + +/** + * 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 ERC20 Proxy smart contract. + */ +export class ERC20TokenWrapper extends ContractWrapper { + public abi: ContractAbi = artifacts.ERC20Token.compilerOutput.abi; + public UNLIMITED_ALLOWANCE_IN_BASE_UNITS = constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; + private _tokenContractsByAddress: { [address: string]: ERC20TokenContract }; + private _erc20ProxyWrapper: ERC20ProxyWrapper; + constructor(web3Wrapper: Web3Wrapper, networkId: number, erc20ProxyWrapper: ERC20ProxyWrapper) { + super(web3Wrapper, networkId); + this._tokenContractsByAddress = {}; + this._erc20ProxyWrapper = erc20ProxyWrapper; + } + /** + * 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 { + assert.isETHAddressHex('ownerAddress', ownerAddress); + assert.isETHAddressHex('tokenAddress', tokenAddress); + const normalizedTokenAddress = tokenAddress.toLowerCase(); + const normalizedOwnerAddress = ownerAddress.toLowerCase(); + + const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress); + const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock; + const txData = {}; + let balance = await tokenContract.balanceOf.callAsync(normalizedOwnerAddress, txData, 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. + * @param txOpts Transaction parameters. + * @return Transaction hash. + */ + public async setAllowanceAsync( + tokenAddress: string, + ownerAddress: string, + spenderAddress: string, + amountInBaseUnits: BigNumber, + txOpts: TransactionOpts = {}, + ): Promise { + assert.isETHAddressHex('spenderAddress', spenderAddress); + assert.isETHAddressHex('tokenAddress', tokenAddress); + await assert.isSenderAddressAsync('ownerAddress', ownerAddress, this._web3Wrapper); + const normalizedTokenAddress = tokenAddress.toLowerCase(); + const normalizedSpenderAddress = spenderAddress.toLowerCase(); + const normalizedOwnerAddress = ownerAddress.toLowerCase(); + assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); + + const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress); + const txHash = await tokenContract.approve.sendTransactionAsync(normalizedSpenderAddress, amountInBaseUnits, { + from: normalizedOwnerAddress, + gas: txOpts.gasLimit, + gasPrice: txOpts.gasPrice, + }); + 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. + * @param txOpts Transaction parameters. + * @return Transaction hash. + */ + public async setUnlimitedAllowanceAsync( + tokenAddress: string, + ownerAddress: string, + spenderAddress: string, + txOpts: TransactionOpts = {}, + ): Promise { + assert.isETHAddressHex('ownerAddress', ownerAddress); + assert.isETHAddressHex('tokenAddress', tokenAddress); + assert.isETHAddressHex('spenderAddress', spenderAddress); + const normalizedTokenAddress = tokenAddress.toLowerCase(); + const normalizedOwnerAddress = ownerAddress.toLowerCase(); + const normalizedSpenderAddress = spenderAddress.toLowerCase(); + const txHash = await this.setAllowanceAsync( + normalizedTokenAddress, + normalizedOwnerAddress, + normalizedSpenderAddress, + this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + txOpts, + ); + 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 { + assert.isETHAddressHex('ownerAddress', ownerAddress); + assert.isETHAddressHex('tokenAddress', tokenAddress); + assert.isETHAddressHex('spenderAddress', spenderAddress); + const normalizedTokenAddress = tokenAddress.toLowerCase(); + const normalizedOwnerAddress = ownerAddress.toLowerCase(); + const normalizedSpenderAddress = spenderAddress.toLowerCase(); + + const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress); + const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock; + const txData = {}; + let allowanceInBaseUnits = await tokenContract.allowance.callAsync( + normalizedOwnerAddress, + normalizedSpenderAddress, + txData, + 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 { + assert.isETHAddressHex('ownerAddress', ownerAddress); + assert.isETHAddressHex('tokenAddress', tokenAddress); + const normalizedTokenAddress = tokenAddress.toLowerCase(); + const normalizedOwnerAddress = ownerAddress.toLowerCase(); + + const proxyAddress = this._erc20ProxyWrapper.getContractAddress(); + const allowanceInBaseUnits = await this.getAllowanceAsync( + normalizedTokenAddress, + normalizedOwnerAddress, + 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. + * @param txOpts Transaction parameters. + * @return Transaction hash. + */ + public async setProxyAllowanceAsync( + tokenAddress: string, + ownerAddress: string, + amountInBaseUnits: BigNumber, + txOpts: TransactionOpts = {}, + ): Promise { + assert.isETHAddressHex('ownerAddress', ownerAddress); + assert.isETHAddressHex('tokenAddress', tokenAddress); + const normalizedTokenAddress = tokenAddress.toLowerCase(); + const normalizedOwnerAddress = ownerAddress.toLowerCase(); + assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); + + const proxyAddress = this._erc20ProxyWrapper.getContractAddress(); + const txHash = await this.setAllowanceAsync( + normalizedTokenAddress, + normalizedOwnerAddress, + proxyAddress, + amountInBaseUnits, + txOpts, + ); + 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. + * @param txOpts Transaction parameters. + * @return Transaction hash. + */ + public async setUnlimitedProxyAllowanceAsync( + tokenAddress: string, + ownerAddress: string, + txOpts: TransactionOpts = {}, + ): Promise { + assert.isETHAddressHex('ownerAddress', ownerAddress); + assert.isETHAddressHex('tokenAddress', tokenAddress); + const normalizedTokenAddress = tokenAddress.toLowerCase(); + const normalizedOwnerAddress = ownerAddress.toLowerCase(); + const txHash = await this.setProxyAllowanceAsync( + normalizedTokenAddress, + normalizedOwnerAddress, + this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + txOpts, + ); + 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. + * @param txOpts Transaction parameters. + * @return Transaction hash. + */ + public async transferAsync( + tokenAddress: string, + fromAddress: string, + toAddress: string, + amountInBaseUnits: BigNumber, + txOpts: TransactionOpts = {}, + ): Promise { + assert.isETHAddressHex('tokenAddress', tokenAddress); + assert.isETHAddressHex('toAddress', toAddress); + await assert.isSenderAddressAsync('fromAddress', fromAddress, this._web3Wrapper); + const normalizedTokenAddress = tokenAddress.toLowerCase(); + const normalizedFromAddress = fromAddress.toLowerCase(); + const normalizedToAddress = toAddress.toLowerCase(); + assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); + + const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress); + + const fromAddressBalance = await this.getBalanceAsync(normalizedTokenAddress, normalizedFromAddress); + if (fromAddressBalance.lessThan(amountInBaseUnits)) { + throw new Error(ContractWrappersError.InsufficientBalanceForTransfer); + } + + const txHash = await tokenContract.transfer.sendTransactionAsync(normalizedToAddress, amountInBaseUnits, { + from: normalizedFromAddress, + gas: txOpts.gasLimit, + gasPrice: txOpts.gasPrice, + }); + 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. + * @param txOpts Transaction parameters. + * @return Transaction hash. + */ + public async transferFromAsync( + tokenAddress: string, + fromAddress: string, + toAddress: string, + senderAddress: string, + amountInBaseUnits: BigNumber, + txOpts: TransactionOpts = {}, + ): Promise { + assert.isETHAddressHex('toAddress', toAddress); + assert.isETHAddressHex('fromAddress', fromAddress); + assert.isETHAddressHex('tokenAddress', tokenAddress); + await assert.isSenderAddressAsync('senderAddress', senderAddress, this._web3Wrapper); + const normalizedToAddress = toAddress.toLowerCase(); + const normalizedFromAddress = fromAddress.toLowerCase(); + const normalizedTokenAddress = tokenAddress.toLowerCase(); + const normalizedSenderAddress = senderAddress.toLowerCase(); + assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); + + const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress); + + const fromAddressAllowance = await this.getAllowanceAsync( + normalizedTokenAddress, + normalizedFromAddress, + normalizedSenderAddress, + ); + if (fromAddressAllowance.lessThan(amountInBaseUnits)) { + throw new Error(ContractWrappersError.InsufficientAllowanceForTransfer); + } + + const fromAddressBalance = await this.getBalanceAsync(normalizedTokenAddress, normalizedFromAddress); + if (fromAddressBalance.lessThan(amountInBaseUnits)) { + throw new Error(ContractWrappersError.InsufficientBalanceForTransfer); + } + + const txHash = await tokenContract.transferFrom.sendTransactionAsync( + normalizedFromAddress, + normalizedToAddress, + amountInBaseUnits, + { + from: normalizedSenderAddress, + gas: txOpts.gasLimit, + gasPrice: txOpts.gasPrice, + }, + ); + 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( + tokenAddress: string, + eventName: ERC20TokenEvents, + indexFilterValues: IndexedFilterValues, + callback: EventCallback, + ): string { + assert.isETHAddressHex('tokenAddress', tokenAddress); + const normalizedTokenAddress = tokenAddress.toLowerCase(); + assert.doesBelongToStringEnum('eventName', eventName, ERC20TokenEvents); + assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema); + assert.isFunction('callback', callback); + const subscriptionToken = this._subscribe( + normalizedTokenAddress, + eventName, + indexFilterValues, + artifacts.ERC20Token.compilerOutput.abi, + callback, + ); + return subscriptionToken; + } + /** + * Cancel a subscription + * @param subscriptionToken Subscription token returned by `subscribe()` + */ + public unsubscribe(subscriptionToken: string): void { + this._unsubscribe(subscriptionToken); + } + /** + * Cancels all existing subscriptions + */ + public unsubscribeAll(): void { + super._unsubscribeAll(); + } + /** + * Gets historical logs without creating a subscription + * @param tokenAddress An address of the token that emitted the logs. + * @param eventName The token contract event you would like to subscribe to. + * @param blockRange Block range to get logs from. + * @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( + tokenAddress: string, + eventName: ERC20TokenEvents, + blockRange: BlockRange, + indexFilterValues: IndexedFilterValues, + ): Promise>> { + assert.isETHAddressHex('tokenAddress', tokenAddress); + const normalizedTokenAddress = tokenAddress.toLowerCase(); + assert.doesBelongToStringEnum('eventName', eventName, ERC20TokenEvents); + assert.doesConformToSchema('blockRange', blockRange, schemas.blockRangeSchema); + assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema); + const logs = await this._getLogsAsync( + normalizedTokenAddress, + eventName, + blockRange, + indexFilterValues, + artifacts.ERC20Token.compilerOutput.abi, + ); + return logs; + } + // tslint:disable-next-line:no-unused-variable + private _invalidateContractInstances(): void { + this.unsubscribeAll(); + this._tokenContractsByAddress = {}; + } + private async _getTokenContractAsync(tokenAddress: string): Promise { + const normalizedTokenAddress = tokenAddress.toLowerCase(); + let tokenContract = this._tokenContractsByAddress[normalizedTokenAddress]; + if (!_.isUndefined(tokenContract)) { + return tokenContract; + } + const [abi, address] = await this._getContractAbiAndAddressFromArtifactsAsync( + artifacts.ERC20Token, + normalizedTokenAddress, + ); + const contractInstance = new ERC20TokenContract( + abi, + address, + this._web3Wrapper.getProvider(), + this._web3Wrapper.getContractDefaults(), + ); + tokenContract = contractInstance; + this._tokenContractsByAddress[normalizedTokenAddress] = tokenContract; + return tokenContract; + } +} diff --git a/packages/contract-wrappers/src/contract_wrappers/token_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/token_wrapper.ts deleted file mode 100644 index d9364715f..000000000 --- a/packages/contract-wrappers/src/contract_wrappers/token_wrapper.ts +++ /dev/null @@ -1,443 +0,0 @@ -import { schemas } from '@0xproject/json-schemas'; -import { ContractAbi, LogWithDecodedArgs } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; -import * as _ from 'lodash'; - -import { artifacts } from '../artifacts'; -import { - BlockRange, - ContractWrappersError, - EventCallback, - IndexedFilterValues, - MethodOpts, - TransactionOpts, -} from '../types'; -import { assert } from '../utils/assert'; -import { constants } from '../utils/constants'; - -import { ContractWrapper } from './contract_wrapper'; -import { TokenContract, TokenContractEventArgs, TokenEvents } from './generated/token'; -import { TokenTransferProxyWrapper } from './token_transfer_proxy_wrapper'; - -/** - * 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 abi: ContractAbi = artifacts.Token.abi; - public UNLIMITED_ALLOWANCE_IN_BASE_UNITS = constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; - private _tokenContractsByAddress: { [address: string]: TokenContract }; - private _tokenTransferProxyWrapper: TokenTransferProxyWrapper; - constructor(web3Wrapper: Web3Wrapper, networkId: number, tokenTransferProxyWrapper: TokenTransferProxyWrapper) { - super(web3Wrapper, networkId); - this._tokenContractsByAddress = {}; - this._tokenTransferProxyWrapper = tokenTransferProxyWrapper; - } - /** - * 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 { - assert.isETHAddressHex('ownerAddress', ownerAddress); - assert.isETHAddressHex('tokenAddress', tokenAddress); - const normalizedTokenAddress = tokenAddress.toLowerCase(); - const normalizedOwnerAddress = ownerAddress.toLowerCase(); - - const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress); - const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock; - const txData = {}; - let balance = await tokenContract.balanceOf.callAsync(normalizedOwnerAddress, txData, 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. - * @param txOpts Transaction parameters. - * @return Transaction hash. - */ - public async setAllowanceAsync( - tokenAddress: string, - ownerAddress: string, - spenderAddress: string, - amountInBaseUnits: BigNumber, - txOpts: TransactionOpts = {}, - ): Promise { - assert.isETHAddressHex('spenderAddress', spenderAddress); - assert.isETHAddressHex('tokenAddress', tokenAddress); - await assert.isSenderAddressAsync('ownerAddress', ownerAddress, this._web3Wrapper); - const normalizedTokenAddress = tokenAddress.toLowerCase(); - const normalizedSpenderAddress = spenderAddress.toLowerCase(); - const normalizedOwnerAddress = ownerAddress.toLowerCase(); - assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); - - const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress); - const txHash = await tokenContract.approve.sendTransactionAsync(normalizedSpenderAddress, amountInBaseUnits, { - from: normalizedOwnerAddress, - gas: txOpts.gasLimit, - gasPrice: txOpts.gasPrice, - }); - 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. - * @param txOpts Transaction parameters. - * @return Transaction hash. - */ - public async setUnlimitedAllowanceAsync( - tokenAddress: string, - ownerAddress: string, - spenderAddress: string, - txOpts: TransactionOpts = {}, - ): Promise { - assert.isETHAddressHex('ownerAddress', ownerAddress); - assert.isETHAddressHex('tokenAddress', tokenAddress); - assert.isETHAddressHex('spenderAddress', spenderAddress); - const normalizedTokenAddress = tokenAddress.toLowerCase(); - const normalizedOwnerAddress = ownerAddress.toLowerCase(); - const normalizedSpenderAddress = spenderAddress.toLowerCase(); - const txHash = await this.setAllowanceAsync( - normalizedTokenAddress, - normalizedOwnerAddress, - normalizedSpenderAddress, - this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, - txOpts, - ); - 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 { - assert.isETHAddressHex('ownerAddress', ownerAddress); - assert.isETHAddressHex('tokenAddress', tokenAddress); - assert.isETHAddressHex('spenderAddress', spenderAddress); - const normalizedTokenAddress = tokenAddress.toLowerCase(); - const normalizedOwnerAddress = ownerAddress.toLowerCase(); - const normalizedSpenderAddress = spenderAddress.toLowerCase(); - - const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress); - const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock; - const txData = {}; - let allowanceInBaseUnits = await tokenContract.allowance.callAsync( - normalizedOwnerAddress, - normalizedSpenderAddress, - txData, - 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 { - assert.isETHAddressHex('ownerAddress', ownerAddress); - assert.isETHAddressHex('tokenAddress', tokenAddress); - const normalizedTokenAddress = tokenAddress.toLowerCase(); - const normalizedOwnerAddress = ownerAddress.toLowerCase(); - - const proxyAddress = this._tokenTransferProxyWrapper.getContractAddress(); - const allowanceInBaseUnits = await this.getAllowanceAsync( - normalizedTokenAddress, - normalizedOwnerAddress, - 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. - * @param txOpts Transaction parameters. - * @return Transaction hash. - */ - public async setProxyAllowanceAsync( - tokenAddress: string, - ownerAddress: string, - amountInBaseUnits: BigNumber, - txOpts: TransactionOpts = {}, - ): Promise { - assert.isETHAddressHex('ownerAddress', ownerAddress); - assert.isETHAddressHex('tokenAddress', tokenAddress); - const normalizedTokenAddress = tokenAddress.toLowerCase(); - const normalizedOwnerAddress = ownerAddress.toLowerCase(); - assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); - - const proxyAddress = this._tokenTransferProxyWrapper.getContractAddress(); - const txHash = await this.setAllowanceAsync( - normalizedTokenAddress, - normalizedOwnerAddress, - proxyAddress, - amountInBaseUnits, - txOpts, - ); - 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. - * @param txOpts Transaction parameters. - * @return Transaction hash. - */ - public async setUnlimitedProxyAllowanceAsync( - tokenAddress: string, - ownerAddress: string, - txOpts: TransactionOpts = {}, - ): Promise { - assert.isETHAddressHex('ownerAddress', ownerAddress); - assert.isETHAddressHex('tokenAddress', tokenAddress); - const normalizedTokenAddress = tokenAddress.toLowerCase(); - const normalizedOwnerAddress = ownerAddress.toLowerCase(); - const txHash = await this.setProxyAllowanceAsync( - normalizedTokenAddress, - normalizedOwnerAddress, - this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, - txOpts, - ); - 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. - * @param txOpts Transaction parameters. - * @return Transaction hash. - */ - public async transferAsync( - tokenAddress: string, - fromAddress: string, - toAddress: string, - amountInBaseUnits: BigNumber, - txOpts: TransactionOpts = {}, - ): Promise { - assert.isETHAddressHex('tokenAddress', tokenAddress); - assert.isETHAddressHex('toAddress', toAddress); - await assert.isSenderAddressAsync('fromAddress', fromAddress, this._web3Wrapper); - const normalizedTokenAddress = tokenAddress.toLowerCase(); - const normalizedFromAddress = fromAddress.toLowerCase(); - const normalizedToAddress = toAddress.toLowerCase(); - assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); - - const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress); - - const fromAddressBalance = await this.getBalanceAsync(normalizedTokenAddress, normalizedFromAddress); - if (fromAddressBalance.lessThan(amountInBaseUnits)) { - throw new Error(ContractWrappersError.InsufficientBalanceForTransfer); - } - - const txHash = await tokenContract.transfer.sendTransactionAsync(normalizedToAddress, amountInBaseUnits, { - from: normalizedFromAddress, - gas: txOpts.gasLimit, - gasPrice: txOpts.gasPrice, - }); - 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. - * @param txOpts Transaction parameters. - * @return Transaction hash. - */ - public async transferFromAsync( - tokenAddress: string, - fromAddress: string, - toAddress: string, - senderAddress: string, - amountInBaseUnits: BigNumber, - txOpts: TransactionOpts = {}, - ): Promise { - assert.isETHAddressHex('toAddress', toAddress); - assert.isETHAddressHex('fromAddress', fromAddress); - assert.isETHAddressHex('tokenAddress', tokenAddress); - await assert.isSenderAddressAsync('senderAddress', senderAddress, this._web3Wrapper); - const normalizedToAddress = toAddress.toLowerCase(); - const normalizedFromAddress = fromAddress.toLowerCase(); - const normalizedTokenAddress = tokenAddress.toLowerCase(); - const normalizedSenderAddress = senderAddress.toLowerCase(); - assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits); - - const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress); - - const fromAddressAllowance = await this.getAllowanceAsync( - normalizedTokenAddress, - normalizedFromAddress, - normalizedSenderAddress, - ); - if (fromAddressAllowance.lessThan(amountInBaseUnits)) { - throw new Error(ContractWrappersError.InsufficientAllowanceForTransfer); - } - - const fromAddressBalance = await this.getBalanceAsync(normalizedTokenAddress, normalizedFromAddress); - if (fromAddressBalance.lessThan(amountInBaseUnits)) { - throw new Error(ContractWrappersError.InsufficientBalanceForTransfer); - } - - const txHash = await tokenContract.transferFrom.sendTransactionAsync( - normalizedFromAddress, - normalizedToAddress, - amountInBaseUnits, - { - from: normalizedSenderAddress, - gas: txOpts.gasLimit, - gasPrice: txOpts.gasPrice, - }, - ); - 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( - tokenAddress: string, - eventName: TokenEvents, - indexFilterValues: IndexedFilterValues, - callback: EventCallback, - ): string { - assert.isETHAddressHex('tokenAddress', tokenAddress); - const normalizedTokenAddress = tokenAddress.toLowerCase(); - assert.doesBelongToStringEnum('eventName', eventName, TokenEvents); - assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema); - assert.isFunction('callback', callback); - const subscriptionToken = this._subscribe( - normalizedTokenAddress, - eventName, - indexFilterValues, - artifacts.Token.abi, - callback, - ); - return subscriptionToken; - } - /** - * Cancel a subscription - * @param subscriptionToken Subscription token returned by `subscribe()` - */ - public unsubscribe(subscriptionToken: string): void { - this._unsubscribe(subscriptionToken); - } - /** - * Cancels all existing subscriptions - */ - public unsubscribeAll(): void { - super._unsubscribeAll(); - } - /** - * Gets historical logs without creating a subscription - * @param tokenAddress An address of the token that emitted the logs. - * @param eventName The token contract event you would like to subscribe to. - * @param blockRange Block range to get logs from. - * @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( - tokenAddress: string, - eventName: TokenEvents, - blockRange: BlockRange, - indexFilterValues: IndexedFilterValues, - ): Promise>> { - assert.isETHAddressHex('tokenAddress', tokenAddress); - const normalizedTokenAddress = tokenAddress.toLowerCase(); - assert.doesBelongToStringEnum('eventName', eventName, TokenEvents); - assert.doesConformToSchema('blockRange', blockRange, schemas.blockRangeSchema); - assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema); - const logs = await this._getLogsAsync( - normalizedTokenAddress, - eventName, - blockRange, - indexFilterValues, - artifacts.Token.abi, - ); - return logs; - } - // tslint:disable-next-line:no-unused-variable - private _invalidateContractInstances(): void { - this.unsubscribeAll(); - this._tokenContractsByAddress = {}; - } - private async _getTokenContractAsync(tokenAddress: string): Promise { - const normalizedTokenAddress = tokenAddress.toLowerCase(); - let tokenContract = this._tokenContractsByAddress[normalizedTokenAddress]; - if (!_.isUndefined(tokenContract)) { - return tokenContract; - } - const [abi, address] = await this._getContractAbiAndAddressFromArtifactsAsync( - artifacts.Token, - normalizedTokenAddress, - ); - const contractInstance = new TokenContract( - abi, - address, - this._web3Wrapper.getProvider(), - this._web3Wrapper.getContractDefaults(), - ); - tokenContract = contractInstance; - this._tokenContractsByAddress[normalizedTokenAddress] = tokenContract; - return tokenContract; - } -} diff --git a/packages/contract-wrappers/test/erc20_wrapper_test.ts b/packages/contract-wrappers/test/erc20_wrapper_test.ts new file mode 100644 index 000000000..5475fbcb4 --- /dev/null +++ b/packages/contract-wrappers/test/erc20_wrapper_test.ts @@ -0,0 +1,660 @@ +import { BlockchainLifecycle, callbackErrorReporter } from '@0xproject/dev-utils'; +import { EmptyWalletSubprovider } from '@0xproject/subproviders'; +import { DoneCallback } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import { Provider } from 'ethereum-types'; +import 'make-promises-safe'; +import 'mocha'; +import Web3ProviderEngine = require('web3-provider-engine'); + +import { + BlockParamLiteral, + BlockRange, + ContractWrappers, + ContractWrappersError, + DecodedLogEvent, + ERC20TokenApprovalEventArgs, + ERC20TokenEvents, + ERC20TokenTransferEventArgs, +} from '../src'; + +import { chaiSetup } from './utils/chai_setup'; +import { constants } from './utils/constants'; +import { tokenUtils } from './utils/token_utils'; +import { provider, web3Wrapper } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('ERC20Wrapper', () => { + let contractWrappers: ContractWrappers; + let userAddresses: string[]; + let tokens: string[]; + let coinbase: string; + let addressWithoutFunds: string; + const config = { + networkId: constants.TESTRPC_NETWORK_ID, + }; + before(async () => { + contractWrappers = new ContractWrappers(provider, config); + userAddresses = await web3Wrapper.getAvailableAddressesAsync(); + tokens = tokenUtils.getDummyERC20TokenAddresses(); + coinbase = userAddresses[0]; + addressWithoutFunds = userAddresses[1]; + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('#transferAsync', () => { + let tokenAddress: string; + let transferAmount: BigNumber; + before(() => { + tokenAddress = tokens[0]; + transferAmount = new BigNumber(42); + }); + it('should successfully transfer tokens', async () => { + const fromAddress = coinbase; + const toAddress = addressWithoutFunds; + const preBalance = await contractWrappers.erc20Token.getBalanceAsync(tokenAddress, toAddress); + expect(preBalance).to.be.bignumber.equal(0); + await contractWrappers.erc20Token.transferAsync(tokenAddress, fromAddress, toAddress, transferAmount); + const postBalance = await contractWrappers.erc20Token.getBalanceAsync(tokenAddress, toAddress); + return expect(postBalance).to.be.bignumber.equal(transferAmount); + }); + it('should fail to transfer tokens if fromAddress has an insufficient balance', async () => { + const fromAddress = addressWithoutFunds; + const toAddress = coinbase; + return expect( + contractWrappers.erc20Token.transferAsync(tokenAddress, fromAddress, toAddress, transferAmount), + ).to.be.rejectedWith(ContractWrappersError.InsufficientBalanceForTransfer); + }); + it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => { + const nonExistentTokenAddress = '0x9dd402f14d67e001d8efbe6583e51bf9706aa065'; + const fromAddress = coinbase; + const toAddress = coinbase; + return expect( + contractWrappers.erc20Token.transferAsync( + nonExistentTokenAddress, + fromAddress, + toAddress, + transferAmount, + ), + ).to.be.rejectedWith(ContractWrappersError.ERC20TokenContractDoesNotExist); + }); + }); + describe('#transferFromAsync', () => { + let tokenAddress: string; + let toAddress: string; + let senderAddress: string; + before(async () => { + tokenAddress = tokens[0]; + toAddress = addressWithoutFunds; + senderAddress = userAddresses[2]; + }); + it('should fail to transfer tokens if fromAddress has insufficient allowance set', async () => { + const fromAddress = coinbase; + const transferAmount = new BigNumber(42); + + const fromAddressBalance = await contractWrappers.erc20Token.getBalanceAsync(tokenAddress, fromAddress); + expect(fromAddressBalance).to.be.bignumber.greaterThan(transferAmount); + + const fromAddressAllowance = await contractWrappers.erc20Token.getAllowanceAsync( + tokenAddress, + fromAddress, + toAddress, + ); + expect(fromAddressAllowance).to.be.bignumber.equal(0); + + return expect( + contractWrappers.erc20Token.transferFromAsync( + tokenAddress, + fromAddress, + toAddress, + senderAddress, + transferAmount, + ), + ).to.be.rejectedWith(ContractWrappersError.InsufficientAllowanceForTransfer); + }); + it('[regression] should fail to transfer tokens if set allowance for toAddress instead of senderAddress', async () => { + const fromAddress = coinbase; + const transferAmount = new BigNumber(42); + + await contractWrappers.erc20Token.setAllowanceAsync(tokenAddress, fromAddress, toAddress, transferAmount); + + return expect( + contractWrappers.erc20Token.transferFromAsync( + tokenAddress, + fromAddress, + toAddress, + senderAddress, + transferAmount, + ), + ).to.be.rejectedWith(ContractWrappersError.InsufficientAllowanceForTransfer); + }); + it('should fail to transfer tokens if fromAddress has insufficient balance', async () => { + const fromAddress = addressWithoutFunds; + const transferAmount = new BigNumber(42); + + const fromAddressBalance = await contractWrappers.erc20Token.getBalanceAsync(tokenAddress, fromAddress); + expect(fromAddressBalance).to.be.bignumber.equal(0); + + await contractWrappers.erc20Token.setAllowanceAsync( + tokenAddress, + fromAddress, + senderAddress, + transferAmount, + ); + const fromAddressAllowance = await contractWrappers.erc20Token.getAllowanceAsync( + tokenAddress, + fromAddress, + senderAddress, + ); + expect(fromAddressAllowance).to.be.bignumber.equal(transferAmount); + + return expect( + contractWrappers.erc20Token.transferFromAsync( + tokenAddress, + fromAddress, + toAddress, + senderAddress, + transferAmount, + ), + ).to.be.rejectedWith(ContractWrappersError.InsufficientBalanceForTransfer); + }); + it('should successfully transfer tokens', async () => { + const fromAddress = coinbase; + + const preBalance = await contractWrappers.erc20Token.getBalanceAsync(tokenAddress, toAddress); + expect(preBalance).to.be.bignumber.equal(0); + + const transferAmount = new BigNumber(42); + await contractWrappers.erc20Token.setAllowanceAsync( + tokenAddress, + fromAddress, + senderAddress, + transferAmount, + ); + + await contractWrappers.erc20Token.transferFromAsync( + tokenAddress, + fromAddress, + toAddress, + senderAddress, + transferAmount, + ); + const postBalance = await contractWrappers.erc20Token.getBalanceAsync(tokenAddress, toAddress); + return expect(postBalance).to.be.bignumber.equal(transferAmount); + }); + it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => { + const fromAddress = coinbase; + const nonExistentTokenAddress = '0x9dd402f14d67e001d8efbe6583e51bf9706aa065'; + return expect( + contractWrappers.erc20Token.transferFromAsync( + nonExistentTokenAddress, + fromAddress, + toAddress, + senderAddress, + new BigNumber(42), + ), + ).to.be.rejectedWith(ContractWrappersError.ERC20TokenContractDoesNotExist); + }); + }); + describe('#getBalanceAsync', () => { + describe('With provider with accounts', () => { + it('should return the balance for an existing ERC20 token', async () => { + const tokenAddress = tokens[0]; + const ownerAddress = coinbase; + const balance = await contractWrappers.erc20Token.getBalanceAsync(tokenAddress, ownerAddress); + const expectedBalance = new BigNumber('1000000000000000000000000000'); + return expect(balance).to.be.bignumber.equal(expectedBalance); + }); + it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => { + const nonExistentTokenAddress = '0x9dd402f14d67e001d8efbe6583e51bf9706aa065'; + const ownerAddress = coinbase; + return expect( + contractWrappers.erc20Token.getBalanceAsync(nonExistentTokenAddress, ownerAddress), + ).to.be.rejectedWith(ContractWrappersError.ERC20TokenContractDoesNotExist); + }); + it('should return a balance of 0 for a non-existent owner address', async () => { + const tokenAddress = tokens[0]; + const nonExistentOwner = '0x198c6ad858f213fb31b6fe809e25040e6b964593'; + const balance = await contractWrappers.erc20Token.getBalanceAsync(tokenAddress, nonExistentOwner); + const expectedBalance = new BigNumber(0); + return expect(balance).to.be.bignumber.equal(expectedBalance); + }); + }); + describe('With provider without accounts', () => { + let zeroExContractWithoutAccounts: ContractWrappers; + before(async () => { + const emptyWalletProvider = addEmptyWalletSubprovider(provider); + zeroExContractWithoutAccounts = new ContractWrappers(emptyWalletProvider, config); + }); + it('should return balance even when called with provider instance without addresses', async () => { + const tokenAddress = tokens[0]; + const ownerAddress = coinbase; + const balance = await zeroExContractWithoutAccounts.erc20Token.getBalanceAsync( + tokenAddress, + ownerAddress, + ); + const expectedBalance = new BigNumber('1000000000000000000000000000'); + return expect(balance).to.be.bignumber.equal(expectedBalance); + }); + }); + }); + describe('#setAllowanceAsync', () => { + it("should set the spender's allowance", async () => { + const tokenAddress = tokens[0]; + const ownerAddress = coinbase; + const spenderAddress = addressWithoutFunds; + + const allowanceBeforeSet = await contractWrappers.erc20Token.getAllowanceAsync( + tokenAddress, + ownerAddress, + spenderAddress, + ); + const expectedAllowanceBeforeAllowanceSet = new BigNumber(0); + expect(allowanceBeforeSet).to.be.bignumber.equal(expectedAllowanceBeforeAllowanceSet); + + const amountInBaseUnits = new BigNumber(50); + await contractWrappers.erc20Token.setAllowanceAsync( + tokenAddress, + ownerAddress, + spenderAddress, + amountInBaseUnits, + ); + + const allowanceAfterSet = await contractWrappers.erc20Token.getAllowanceAsync( + tokenAddress, + ownerAddress, + spenderAddress, + ); + const expectedAllowanceAfterAllowanceSet = amountInBaseUnits; + return expect(allowanceAfterSet).to.be.bignumber.equal(expectedAllowanceAfterAllowanceSet); + }); + }); + describe('#setUnlimitedAllowanceAsync', () => { + it("should set the unlimited spender's allowance", async () => { + const tokenAddress = tokens[0]; + const ownerAddress = coinbase; + const spenderAddress = addressWithoutFunds; + + await contractWrappers.erc20Token.setUnlimitedAllowanceAsync(tokenAddress, ownerAddress, spenderAddress); + const allowance = await contractWrappers.erc20Token.getAllowanceAsync( + tokenAddress, + ownerAddress, + spenderAddress, + ); + return expect(allowance).to.be.bignumber.equal( + contractWrappers.erc20Token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + ); + }); + it('should reduce the gas cost for transfers including tokens with unlimited allowance support', async () => { + const transferAmount = new BigNumber(5); + const zrxAddress = tokenUtils.getProtocolTokenAddress(); + const [, userWithNormalAllowance, userWithUnlimitedAllowance] = userAddresses; + await contractWrappers.erc20Token.setAllowanceAsync( + zrxAddress, + coinbase, + userWithNormalAllowance, + transferAmount, + ); + await contractWrappers.erc20Token.setUnlimitedAllowanceAsync( + zrxAddress, + coinbase, + userWithUnlimitedAllowance, + ); + + const initBalanceWithNormalAllowance = await web3Wrapper.getBalanceInWeiAsync(userWithNormalAllowance); + const initBalanceWithUnlimitedAllowance = await web3Wrapper.getBalanceInWeiAsync( + userWithUnlimitedAllowance, + ); + + await contractWrappers.erc20Token.transferFromAsync( + zrxAddress, + coinbase, + userWithNormalAllowance, + userWithNormalAllowance, + transferAmount, + ); + await contractWrappers.erc20Token.transferFromAsync( + zrxAddress, + coinbase, + userWithUnlimitedAllowance, + userWithUnlimitedAllowance, + transferAmount, + ); + + const finalBalanceWithNormalAllowance = await web3Wrapper.getBalanceInWeiAsync(userWithNormalAllowance); + const finalBalanceWithUnlimitedAllowance = await web3Wrapper.getBalanceInWeiAsync( + userWithUnlimitedAllowance, + ); + + const normalGasCost = initBalanceWithNormalAllowance.minus(finalBalanceWithNormalAllowance); + const unlimitedGasCost = initBalanceWithUnlimitedAllowance.minus(finalBalanceWithUnlimitedAllowance); + + // In theory the gas cost with unlimited allowance should be smaller, but with testrpc it's actually bigger. + // This needs to be investigated in ethereumjs-vm. This test is essentially a repro. + // TODO: Make this test pass with inverted assertion. + expect(unlimitedGasCost.toNumber()).to.be.gt(normalGasCost.toNumber()); + }); + }); + describe('#getAllowanceAsync', () => { + describe('With provider with accounts', () => { + it('should get the proxy allowance', async () => { + const tokenAddress = tokens[0]; + const ownerAddress = coinbase; + const spenderAddress = addressWithoutFunds; + + const amountInBaseUnits = new BigNumber(50); + await contractWrappers.erc20Token.setAllowanceAsync( + tokenAddress, + ownerAddress, + spenderAddress, + amountInBaseUnits, + ); + + const allowance = await contractWrappers.erc20Token.getAllowanceAsync( + tokenAddress, + ownerAddress, + spenderAddress, + ); + const expectedAllowance = amountInBaseUnits; + return expect(allowance).to.be.bignumber.equal(expectedAllowance); + }); + it('should return 0 if no allowance set yet', async () => { + const tokenAddress = tokens[0]; + const ownerAddress = coinbase; + const spenderAddress = addressWithoutFunds; + const allowance = await contractWrappers.erc20Token.getAllowanceAsync( + tokenAddress, + ownerAddress, + spenderAddress, + ); + const expectedAllowance = new BigNumber(0); + return expect(allowance).to.be.bignumber.equal(expectedAllowance); + }); + }); + describe('With provider without accounts', () => { + let zeroExContractWithoutAccounts: ContractWrappers; + before(async () => { + const emptyWalletProvider = addEmptyWalletSubprovider(provider); + zeroExContractWithoutAccounts = new ContractWrappers(emptyWalletProvider, config); + }); + it('should get the proxy allowance', async () => { + const tokenAddress = tokens[0]; + const ownerAddress = coinbase; + const spenderAddress = addressWithoutFunds; + + const amountInBaseUnits = new BigNumber(50); + await contractWrappers.erc20Token.setAllowanceAsync( + tokenAddress, + ownerAddress, + spenderAddress, + amountInBaseUnits, + ); + + const allowance = await zeroExContractWithoutAccounts.erc20Token.getAllowanceAsync( + tokenAddress, + ownerAddress, + spenderAddress, + ); + const expectedAllowance = amountInBaseUnits; + return expect(allowance).to.be.bignumber.equal(expectedAllowance); + }); + }); + }); + describe('#getProxyAllowanceAsync', () => { + it('should get the proxy allowance', async () => { + const tokenAddress = tokens[0]; + const ownerAddress = coinbase; + + const amountInBaseUnits = new BigNumber(50); + await contractWrappers.erc20Token.setProxyAllowanceAsync(tokenAddress, ownerAddress, amountInBaseUnits); + + const allowance = await contractWrappers.erc20Token.getProxyAllowanceAsync(tokenAddress, ownerAddress); + const expectedAllowance = amountInBaseUnits; + return expect(allowance).to.be.bignumber.equal(expectedAllowance); + }); + }); + describe('#setProxyAllowanceAsync', () => { + it('should set the proxy allowance', async () => { + const tokenAddress = tokens[0]; + const ownerAddress = coinbase; + + const allowanceBeforeSet = await contractWrappers.erc20Token.getProxyAllowanceAsync( + tokenAddress, + ownerAddress, + ); + const expectedAllowanceBeforeAllowanceSet = new BigNumber(0); + expect(allowanceBeforeSet).to.be.bignumber.equal(expectedAllowanceBeforeAllowanceSet); + + const amountInBaseUnits = new BigNumber(50); + await contractWrappers.erc20Token.setProxyAllowanceAsync(tokenAddress, ownerAddress, amountInBaseUnits); + + const allowanceAfterSet = await contractWrappers.erc20Token.getProxyAllowanceAsync( + tokenAddress, + ownerAddress, + ); + const expectedAllowanceAfterAllowanceSet = amountInBaseUnits; + return expect(allowanceAfterSet).to.be.bignumber.equal(expectedAllowanceAfterAllowanceSet); + }); + }); + describe('#setUnlimitedProxyAllowanceAsync', () => { + it('should set the unlimited proxy allowance', async () => { + const tokenAddress = tokens[0]; + const ownerAddress = coinbase; + + await contractWrappers.erc20Token.setUnlimitedProxyAllowanceAsync(tokenAddress, ownerAddress); + const allowance = await contractWrappers.erc20Token.getProxyAllowanceAsync(tokenAddress, ownerAddress); + return expect(allowance).to.be.bignumber.equal( + contractWrappers.erc20Token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, + ); + }); + }); + describe('#subscribe', () => { + const indexFilterValues = {}; + let tokenAddress: string; + const transferAmount = new BigNumber(42); + const allowanceAmount = new BigNumber(42); + before(() => { + tokenAddress = tokens[0]; + }); + afterEach(() => { + contractWrappers.erc20Token.unsubscribeAll(); + }); + // Hack: Mocha does not allow a test to be both async and have a `done` callback + // Since we need to await the receipt of the event in the `subscribe` callback, + // we do need both. A hack is to make the top-level a sync fn w/ a done callback and then + // wrap the rest of the test in an async block + // Source: https://github.com/mochajs/mocha/issues/2407 + it('Should receive the Transfer event when tokens are transfered', (done: DoneCallback) => { + (async () => { + const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent) => { + expect(logEvent.isRemoved).to.be.false(); + expect(logEvent.log.logIndex).to.be.equal(0); + expect(logEvent.log.transactionIndex).to.be.equal(0); + expect(logEvent.log.blockNumber).to.be.a('number'); + const args = logEvent.log.args; + expect(args._from).to.be.equal(coinbase); + expect(args._to).to.be.equal(addressWithoutFunds); + expect(args._value).to.be.bignumber.equal(transferAmount); + }, + ); + contractWrappers.erc20Token.subscribe( + tokenAddress, + ERC20TokenEvents.Transfer, + indexFilterValues, + callback, + ); + await contractWrappers.erc20Token.transferAsync( + tokenAddress, + coinbase, + addressWithoutFunds, + transferAmount, + ); + })().catch(done); + }); + it('Should receive the Approval event when allowance is being set', (done: DoneCallback) => { + (async () => { + const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent) => { + expect(logEvent).to.not.be.undefined(); + expect(logEvent.isRemoved).to.be.false(); + const args = logEvent.log.args; + expect(args._owner).to.be.equal(coinbase); + expect(args._spender).to.be.equal(addressWithoutFunds); + expect(args._value).to.be.bignumber.equal(allowanceAmount); + }, + ); + contractWrappers.erc20Token.subscribe( + tokenAddress, + ERC20TokenEvents.Approval, + indexFilterValues, + callback, + ); + await contractWrappers.erc20Token.setAllowanceAsync( + tokenAddress, + coinbase, + addressWithoutFunds, + allowanceAmount, + ); + })().catch(done); + }); + it('Outstanding subscriptions are cancelled when contractWrappers.setProvider called', (done: DoneCallback) => { + (async () => { + const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent) => { + done(new Error('Expected this subscription to have been cancelled')); + }, + ); + contractWrappers.erc20Token.subscribe( + tokenAddress, + ERC20TokenEvents.Transfer, + indexFilterValues, + callbackNeverToBeCalled, + ); + const callbackToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)(); + contractWrappers.setProvider(provider, constants.TESTRPC_NETWORK_ID); + contractWrappers.erc20Token.subscribe( + tokenAddress, + ERC20TokenEvents.Transfer, + indexFilterValues, + callbackToBeCalled, + ); + await contractWrappers.erc20Token.transferAsync( + tokenAddress, + coinbase, + addressWithoutFunds, + transferAmount, + ); + })().catch(done); + }); + it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => { + (async () => { + const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent) => { + done(new Error('Expected this subscription to have been cancelled')); + }, + ); + const subscriptionToken = contractWrappers.erc20Token.subscribe( + tokenAddress, + ERC20TokenEvents.Transfer, + indexFilterValues, + callbackNeverToBeCalled, + ); + contractWrappers.erc20Token.unsubscribe(subscriptionToken); + await contractWrappers.erc20Token.transferAsync( + tokenAddress, + coinbase, + addressWithoutFunds, + transferAmount, + ); + done(); + })().catch(done); + }); + }); + describe('#getLogsAsync', () => { + let tokenAddress: string; + let tokenTransferProxyAddress: string; + const blockRange: BlockRange = { + fromBlock: 0, + toBlock: BlockParamLiteral.Latest, + }; + let txHash: string; + before(() => { + tokenAddress = tokens[0]; + tokenTransferProxyAddress = contractWrappers.erc20Proxy.getContractAddress(); + }); + it('should get logs with decoded args emitted by Approval', async () => { + txHash = await contractWrappers.erc20Token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const eventName = ERC20TokenEvents.Approval; + const indexFilterValues = {}; + const logs = await contractWrappers.erc20Token.getLogsAsync( + tokenAddress, + eventName, + blockRange, + indexFilterValues, + ); + expect(logs).to.have.length(1); + const args = logs[0].args; + expect(logs[0].event).to.be.equal(eventName); + expect(args._owner).to.be.equal(coinbase); + expect(args._spender).to.be.equal(tokenTransferProxyAddress); + expect(args._value).to.be.bignumber.equal(contractWrappers.erc20Token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS); + }); + it('should only get the logs with the correct event name', async () => { + txHash = await contractWrappers.erc20Token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const differentEventName = ERC20TokenEvents.Transfer; + const indexFilterValues = {}; + const logs = await contractWrappers.erc20Token.getLogsAsync( + tokenAddress, + differentEventName, + blockRange, + indexFilterValues, + ); + expect(logs).to.have.length(0); + }); + it('should only get the logs with the correct indexed fields', async () => { + txHash = await contractWrappers.erc20Token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + txHash = await contractWrappers.erc20Token.setUnlimitedProxyAllowanceAsync( + tokenAddress, + addressWithoutFunds, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const eventName = ERC20TokenEvents.Approval; + const indexFilterValues = { + _owner: coinbase, + }; + const logs = await contractWrappers.erc20Token.getLogsAsync( + tokenAddress, + eventName, + blockRange, + indexFilterValues, + ); + expect(logs).to.have.length(1); + const args = logs[0].args; + expect(args._owner).to.be.equal(coinbase); + }); + }); +}); +// tslint:disable:max-file-line-count + +function addEmptyWalletSubprovider(p: Provider): Provider { + const providerEngine = new Web3ProviderEngine(); + providerEngine.addProvider(new EmptyWalletSubprovider()); + const currentSubproviders = (p as any)._providers; + for (const subprovider of currentSubproviders) { + providerEngine.addProvider(subprovider); + } + providerEngine.start(); + return providerEngine; +} diff --git a/packages/contract-wrappers/test/token_wrapper_test.ts b/packages/contract-wrappers/test/token_wrapper_test.ts deleted file mode 100644 index c9722c7b4..000000000 --- a/packages/contract-wrappers/test/token_wrapper_test.ts +++ /dev/null @@ -1,603 +0,0 @@ -import { BlockchainLifecycle, callbackErrorReporter } from '@0xproject/dev-utils'; -import { EmptyWalletSubprovider } from '@0xproject/subproviders'; -import { DoneCallback, Provider } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; -import * as chai from 'chai'; -import 'mocha'; -import Web3ProviderEngine = require('web3-provider-engine'); - -import { - ApprovalContractEventArgs, - BlockParamLiteral, - BlockRange, - ContractWrappers, - ContractWrappersError, - DecodedLogEvent, - Token, - TokenEvents, - TransferContractEventArgs, -} from '../src'; - -import { chaiSetup } from './utils/chai_setup'; -import { constants } from './utils/constants'; -import { TokenUtils } from './utils/token_utils'; -import { provider, web3Wrapper } from './utils/web3_wrapper'; - -chaiSetup.configure(); -const expect = chai.expect; -const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); - -describe('TokenWrapper', () => { - let contractWrappers: ContractWrappers; - let userAddresses: string[]; - let tokens: Token[]; - let tokenUtils: TokenUtils; - let coinbase: string; - let addressWithoutFunds: string; - const config = { - networkId: constants.TESTRPC_NETWORK_ID, - }; - before(async () => { - contractWrappers = new ContractWrappers(provider, config); - userAddresses = await web3Wrapper.getAvailableAddressesAsync(); - tokens = await contractWrappers.tokenRegistry.getTokensAsync(); - tokenUtils = new TokenUtils(tokens); - coinbase = userAddresses[0]; - addressWithoutFunds = userAddresses[1]; - }); - beforeEach(async () => { - await blockchainLifecycle.startAsync(); - }); - afterEach(async () => { - await blockchainLifecycle.revertAsync(); - }); - describe('#transferAsync', () => { - let token: Token; - let transferAmount: BigNumber; - before(() => { - token = tokens[0]; - transferAmount = new BigNumber(42); - }); - it('should successfully transfer tokens', async () => { - const fromAddress = coinbase; - const toAddress = addressWithoutFunds; - const preBalance = await contractWrappers.token.getBalanceAsync(token.address, toAddress); - expect(preBalance).to.be.bignumber.equal(0); - await contractWrappers.token.transferAsync(token.address, fromAddress, toAddress, transferAmount); - const postBalance = await contractWrappers.token.getBalanceAsync(token.address, toAddress); - return expect(postBalance).to.be.bignumber.equal(transferAmount); - }); - it('should fail to transfer tokens if fromAddress has an insufficient balance', async () => { - const fromAddress = addressWithoutFunds; - const toAddress = coinbase; - return expect( - contractWrappers.token.transferAsync(token.address, fromAddress, toAddress, transferAmount), - ).to.be.rejectedWith(ContractWrappersError.InsufficientBalanceForTransfer); - }); - it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => { - const nonExistentTokenAddress = '0x9dd402f14d67e001d8efbe6583e51bf9706aa065'; - const fromAddress = coinbase; - const toAddress = coinbase; - return expect( - contractWrappers.token.transferAsync(nonExistentTokenAddress, fromAddress, toAddress, transferAmount), - ).to.be.rejectedWith(ContractWrappersError.TokenContractDoesNotExist); - }); - }); - describe('#transferFromAsync', () => { - let token: Token; - let toAddress: string; - let senderAddress: string; - before(async () => { - token = tokens[0]; - toAddress = addressWithoutFunds; - senderAddress = userAddresses[2]; - }); - it('should fail to transfer tokens if fromAddress has insufficient allowance set', async () => { - const fromAddress = coinbase; - const transferAmount = new BigNumber(42); - - const fromAddressBalance = await contractWrappers.token.getBalanceAsync(token.address, fromAddress); - expect(fromAddressBalance).to.be.bignumber.greaterThan(transferAmount); - - const fromAddressAllowance = await contractWrappers.token.getAllowanceAsync( - token.address, - fromAddress, - toAddress, - ); - expect(fromAddressAllowance).to.be.bignumber.equal(0); - - return expect( - contractWrappers.token.transferFromAsync( - token.address, - fromAddress, - toAddress, - senderAddress, - transferAmount, - ), - ).to.be.rejectedWith(ContractWrappersError.InsufficientAllowanceForTransfer); - }); - it('[regression] should fail to transfer tokens if set allowance for toAddress instead of senderAddress', async () => { - const fromAddress = coinbase; - const transferAmount = new BigNumber(42); - - await contractWrappers.token.setAllowanceAsync(token.address, fromAddress, toAddress, transferAmount); - - return expect( - contractWrappers.token.transferFromAsync( - token.address, - fromAddress, - toAddress, - senderAddress, - transferAmount, - ), - ).to.be.rejectedWith(ContractWrappersError.InsufficientAllowanceForTransfer); - }); - it('should fail to transfer tokens if fromAddress has insufficient balance', async () => { - const fromAddress = addressWithoutFunds; - const transferAmount = new BigNumber(42); - - const fromAddressBalance = await contractWrappers.token.getBalanceAsync(token.address, fromAddress); - expect(fromAddressBalance).to.be.bignumber.equal(0); - - await contractWrappers.token.setAllowanceAsync(token.address, fromAddress, senderAddress, transferAmount); - const fromAddressAllowance = await contractWrappers.token.getAllowanceAsync( - token.address, - fromAddress, - senderAddress, - ); - expect(fromAddressAllowance).to.be.bignumber.equal(transferAmount); - - return expect( - contractWrappers.token.transferFromAsync( - token.address, - fromAddress, - toAddress, - senderAddress, - transferAmount, - ), - ).to.be.rejectedWith(ContractWrappersError.InsufficientBalanceForTransfer); - }); - it('should successfully transfer tokens', async () => { - const fromAddress = coinbase; - - const preBalance = await contractWrappers.token.getBalanceAsync(token.address, toAddress); - expect(preBalance).to.be.bignumber.equal(0); - - const transferAmount = new BigNumber(42); - await contractWrappers.token.setAllowanceAsync(token.address, fromAddress, senderAddress, transferAmount); - - await contractWrappers.token.transferFromAsync( - token.address, - fromAddress, - toAddress, - senderAddress, - transferAmount, - ); - const postBalance = await contractWrappers.token.getBalanceAsync(token.address, toAddress); - return expect(postBalance).to.be.bignumber.equal(transferAmount); - }); - it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => { - const fromAddress = coinbase; - const nonExistentTokenAddress = '0x9dd402f14d67e001d8efbe6583e51bf9706aa065'; - return expect( - contractWrappers.token.transferFromAsync( - nonExistentTokenAddress, - fromAddress, - toAddress, - senderAddress, - new BigNumber(42), - ), - ).to.be.rejectedWith(ContractWrappersError.TokenContractDoesNotExist); - }); - }); - describe('#getBalanceAsync', () => { - describe('With provider with accounts', () => { - it('should return the balance for an existing ERC20 token', async () => { - const token = tokens[0]; - const ownerAddress = coinbase; - const balance = await contractWrappers.token.getBalanceAsync(token.address, ownerAddress); - const expectedBalance = new BigNumber('1000000000000000000000000000'); - return expect(balance).to.be.bignumber.equal(expectedBalance); - }); - it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => { - const nonExistentTokenAddress = '0x9dd402f14d67e001d8efbe6583e51bf9706aa065'; - const ownerAddress = coinbase; - return expect( - contractWrappers.token.getBalanceAsync(nonExistentTokenAddress, ownerAddress), - ).to.be.rejectedWith(ContractWrappersError.TokenContractDoesNotExist); - }); - it('should return a balance of 0 for a non-existent owner address', async () => { - const token = tokens[0]; - const nonExistentOwner = '0x198c6ad858f213fb31b6fe809e25040e6b964593'; - const balance = await contractWrappers.token.getBalanceAsync(token.address, nonExistentOwner); - const expectedBalance = new BigNumber(0); - return expect(balance).to.be.bignumber.equal(expectedBalance); - }); - }); - describe('With provider without accounts', () => { - let zeroExContractWithoutAccounts: ContractWrappers; - before(async () => { - const emptyWalletProvider = addEmptyWalletSubprovider(provider); - zeroExContractWithoutAccounts = new ContractWrappers(emptyWalletProvider, config); - }); - it('should return balance even when called with provider instance without addresses', async () => { - const token = tokens[0]; - const ownerAddress = coinbase; - const balance = await zeroExContractWithoutAccounts.token.getBalanceAsync(token.address, ownerAddress); - const expectedBalance = new BigNumber('1000000000000000000000000000'); - return expect(balance).to.be.bignumber.equal(expectedBalance); - }); - }); - }); - describe('#setAllowanceAsync', () => { - it("should set the spender's allowance", async () => { - const token = tokens[0]; - const ownerAddress = coinbase; - const spenderAddress = addressWithoutFunds; - - const allowanceBeforeSet = await contractWrappers.token.getAllowanceAsync( - token.address, - ownerAddress, - spenderAddress, - ); - const expectedAllowanceBeforeAllowanceSet = new BigNumber(0); - expect(allowanceBeforeSet).to.be.bignumber.equal(expectedAllowanceBeforeAllowanceSet); - - const amountInBaseUnits = new BigNumber(50); - await contractWrappers.token.setAllowanceAsync( - token.address, - ownerAddress, - spenderAddress, - amountInBaseUnits, - ); - - const allowanceAfterSet = await contractWrappers.token.getAllowanceAsync( - token.address, - ownerAddress, - spenderAddress, - ); - const expectedAllowanceAfterAllowanceSet = amountInBaseUnits; - return expect(allowanceAfterSet).to.be.bignumber.equal(expectedAllowanceAfterAllowanceSet); - }); - }); - describe('#setUnlimitedAllowanceAsync', () => { - it("should set the unlimited spender's allowance", async () => { - const token = tokens[0]; - const ownerAddress = coinbase; - const spenderAddress = addressWithoutFunds; - - await contractWrappers.token.setUnlimitedAllowanceAsync(token.address, ownerAddress, spenderAddress); - const allowance = await contractWrappers.token.getAllowanceAsync( - token.address, - ownerAddress, - spenderAddress, - ); - return expect(allowance).to.be.bignumber.equal(contractWrappers.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS); - }); - it('should reduce the gas cost for transfers including tokens with unlimited allowance support', async () => { - const transferAmount = new BigNumber(5); - const zrx = tokenUtils.getProtocolTokenOrThrow(); - const [, userWithNormalAllowance, userWithUnlimitedAllowance] = userAddresses; - await contractWrappers.token.setAllowanceAsync( - zrx.address, - coinbase, - userWithNormalAllowance, - transferAmount, - ); - await contractWrappers.token.setUnlimitedAllowanceAsync(zrx.address, coinbase, userWithUnlimitedAllowance); - - const initBalanceWithNormalAllowance = await web3Wrapper.getBalanceInWeiAsync(userWithNormalAllowance); - const initBalanceWithUnlimitedAllowance = await web3Wrapper.getBalanceInWeiAsync( - userWithUnlimitedAllowance, - ); - - await contractWrappers.token.transferFromAsync( - zrx.address, - coinbase, - userWithNormalAllowance, - userWithNormalAllowance, - transferAmount, - ); - await contractWrappers.token.transferFromAsync( - zrx.address, - coinbase, - userWithUnlimitedAllowance, - userWithUnlimitedAllowance, - transferAmount, - ); - - const finalBalanceWithNormalAllowance = await web3Wrapper.getBalanceInWeiAsync(userWithNormalAllowance); - const finalBalanceWithUnlimitedAllowance = await web3Wrapper.getBalanceInWeiAsync( - userWithUnlimitedAllowance, - ); - - const normalGasCost = initBalanceWithNormalAllowance.minus(finalBalanceWithNormalAllowance); - const unlimitedGasCost = initBalanceWithUnlimitedAllowance.minus(finalBalanceWithUnlimitedAllowance); - - // In theory the gas cost with unlimited allowance should be smaller, but with testrpc it's actually bigger. - // This needs to be investigated in ethereumjs-vm. This test is essentially a repro. - // TODO: Make this test pass with inverted assertion. - expect(unlimitedGasCost.toNumber()).to.be.gt(normalGasCost.toNumber()); - }); - }); - describe('#getAllowanceAsync', () => { - describe('With provider with accounts', () => { - it('should get the proxy allowance', async () => { - const token = tokens[0]; - const ownerAddress = coinbase; - const spenderAddress = addressWithoutFunds; - - const amountInBaseUnits = new BigNumber(50); - await contractWrappers.token.setAllowanceAsync( - token.address, - ownerAddress, - spenderAddress, - amountInBaseUnits, - ); - - const allowance = await contractWrappers.token.getAllowanceAsync( - token.address, - ownerAddress, - spenderAddress, - ); - const expectedAllowance = amountInBaseUnits; - return expect(allowance).to.be.bignumber.equal(expectedAllowance); - }); - it('should return 0 if no allowance set yet', async () => { - const token = tokens[0]; - const ownerAddress = coinbase; - const spenderAddress = addressWithoutFunds; - const allowance = await contractWrappers.token.getAllowanceAsync( - token.address, - ownerAddress, - spenderAddress, - ); - const expectedAllowance = new BigNumber(0); - return expect(allowance).to.be.bignumber.equal(expectedAllowance); - }); - }); - describe('With provider without accounts', () => { - let zeroExContractWithoutAccounts: ContractWrappers; - before(async () => { - const emptyWalletProvider = addEmptyWalletSubprovider(provider); - zeroExContractWithoutAccounts = new ContractWrappers(emptyWalletProvider, config); - }); - it('should get the proxy allowance', async () => { - const token = tokens[0]; - const ownerAddress = coinbase; - const spenderAddress = addressWithoutFunds; - - const amountInBaseUnits = new BigNumber(50); - await contractWrappers.token.setAllowanceAsync( - token.address, - ownerAddress, - spenderAddress, - amountInBaseUnits, - ); - - const allowance = await zeroExContractWithoutAccounts.token.getAllowanceAsync( - token.address, - ownerAddress, - spenderAddress, - ); - const expectedAllowance = amountInBaseUnits; - return expect(allowance).to.be.bignumber.equal(expectedAllowance); - }); - }); - }); - describe('#getProxyAllowanceAsync', () => { - it('should get the proxy allowance', async () => { - const token = tokens[0]; - const ownerAddress = coinbase; - - const amountInBaseUnits = new BigNumber(50); - await contractWrappers.token.setProxyAllowanceAsync(token.address, ownerAddress, amountInBaseUnits); - - const allowance = await contractWrappers.token.getProxyAllowanceAsync(token.address, ownerAddress); - const expectedAllowance = amountInBaseUnits; - return expect(allowance).to.be.bignumber.equal(expectedAllowance); - }); - }); - describe('#setProxyAllowanceAsync', () => { - it('should set the proxy allowance', async () => { - const token = tokens[0]; - const ownerAddress = coinbase; - - const allowanceBeforeSet = await contractWrappers.token.getProxyAllowanceAsync(token.address, ownerAddress); - const expectedAllowanceBeforeAllowanceSet = new BigNumber(0); - expect(allowanceBeforeSet).to.be.bignumber.equal(expectedAllowanceBeforeAllowanceSet); - - const amountInBaseUnits = new BigNumber(50); - await contractWrappers.token.setProxyAllowanceAsync(token.address, ownerAddress, amountInBaseUnits); - - const allowanceAfterSet = await contractWrappers.token.getProxyAllowanceAsync(token.address, ownerAddress); - const expectedAllowanceAfterAllowanceSet = amountInBaseUnits; - return expect(allowanceAfterSet).to.be.bignumber.equal(expectedAllowanceAfterAllowanceSet); - }); - }); - describe('#setUnlimitedProxyAllowanceAsync', () => { - it('should set the unlimited proxy allowance', async () => { - const token = tokens[0]; - const ownerAddress = coinbase; - - await contractWrappers.token.setUnlimitedProxyAllowanceAsync(token.address, ownerAddress); - const allowance = await contractWrappers.token.getProxyAllowanceAsync(token.address, ownerAddress); - return expect(allowance).to.be.bignumber.equal(contractWrappers.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS); - }); - }); - describe('#subscribe', () => { - const indexFilterValues = {}; - let tokenAddress: string; - const transferAmount = new BigNumber(42); - const allowanceAmount = new BigNumber(42); - before(() => { - const token = tokens[0]; - tokenAddress = token.address; - }); - afterEach(() => { - contractWrappers.token.unsubscribeAll(); - }); - // Hack: Mocha does not allow a test to be both async and have a `done` callback - // Since we need to await the receipt of the event in the `subscribe` callback, - // we do need both. A hack is to make the top-level a sync fn w/ a done callback and then - // wrap the rest of the test in an async block - // Source: https://github.com/mochajs/mocha/issues/2407 - it('Should receive the Transfer event when tokens are transfered', (done: DoneCallback) => { - (async () => { - const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( - (logEvent: DecodedLogEvent) => { - expect(logEvent.isRemoved).to.be.false(); - expect(logEvent.log.logIndex).to.be.equal(0); - expect(logEvent.log.transactionIndex).to.be.equal(0); - expect(logEvent.log.blockNumber).to.be.a('number'); - const args = logEvent.log.args; - expect(args._from).to.be.equal(coinbase); - expect(args._to).to.be.equal(addressWithoutFunds); - expect(args._value).to.be.bignumber.equal(transferAmount); - }, - ); - contractWrappers.token.subscribe(tokenAddress, TokenEvents.Transfer, indexFilterValues, callback); - await contractWrappers.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount); - })().catch(done); - }); - it('Should receive the Approval event when allowance is being set', (done: DoneCallback) => { - (async () => { - const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( - (logEvent: DecodedLogEvent) => { - expect(logEvent).to.not.be.undefined(); - expect(logEvent.isRemoved).to.be.false(); - const args = logEvent.log.args; - expect(args._owner).to.be.equal(coinbase); - expect(args._spender).to.be.equal(addressWithoutFunds); - expect(args._value).to.be.bignumber.equal(allowanceAmount); - }, - ); - contractWrappers.token.subscribe(tokenAddress, TokenEvents.Approval, indexFilterValues, callback); - await contractWrappers.token.setAllowanceAsync( - tokenAddress, - coinbase, - addressWithoutFunds, - allowanceAmount, - ); - })().catch(done); - }); - it('Outstanding subscriptions are cancelled when contractWrappers.setProvider called', (done: DoneCallback) => { - (async () => { - const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( - (logEvent: DecodedLogEvent) => { - done(new Error('Expected this subscription to have been cancelled')); - }, - ); - contractWrappers.token.subscribe( - tokenAddress, - TokenEvents.Transfer, - indexFilterValues, - callbackNeverToBeCalled, - ); - const callbackToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)(); - contractWrappers.setProvider(provider, constants.TESTRPC_NETWORK_ID); - contractWrappers.token.subscribe( - tokenAddress, - TokenEvents.Transfer, - indexFilterValues, - callbackToBeCalled, - ); - await contractWrappers.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount); - })().catch(done); - }); - it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => { - (async () => { - const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( - (logEvent: DecodedLogEvent) => { - done(new Error('Expected this subscription to have been cancelled')); - }, - ); - const subscriptionToken = contractWrappers.token.subscribe( - tokenAddress, - TokenEvents.Transfer, - indexFilterValues, - callbackNeverToBeCalled, - ); - contractWrappers.token.unsubscribe(subscriptionToken); - await contractWrappers.token.transferAsync(tokenAddress, coinbase, addressWithoutFunds, transferAmount); - done(); - })().catch(done); - }); - }); - describe('#getLogsAsync', () => { - let tokenAddress: string; - let tokenTransferProxyAddress: string; - const blockRange: BlockRange = { - fromBlock: 0, - toBlock: BlockParamLiteral.Latest, - }; - let txHash: string; - before(() => { - const token = tokens[0]; - tokenAddress = token.address; - tokenTransferProxyAddress = contractWrappers.proxy.getContractAddress(); - }); - it('should get logs with decoded args emitted by Approval', async () => { - txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase); - await web3Wrapper.awaitTransactionSuccessAsync(txHash); - const eventName = TokenEvents.Approval; - const indexFilterValues = {}; - const logs = await contractWrappers.token.getLogsAsync( - tokenAddress, - eventName, - blockRange, - indexFilterValues, - ); - expect(logs).to.have.length(1); - const args = logs[0].args; - expect(logs[0].event).to.be.equal(eventName); - expect(args._owner).to.be.equal(coinbase); - expect(args._spender).to.be.equal(tokenTransferProxyAddress); - expect(args._value).to.be.bignumber.equal(contractWrappers.token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS); - }); - it('should only get the logs with the correct event name', async () => { - txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase); - await web3Wrapper.awaitTransactionSuccessAsync(txHash); - const differentEventName = TokenEvents.Transfer; - const indexFilterValues = {}; - const logs = await contractWrappers.token.getLogsAsync( - tokenAddress, - differentEventName, - blockRange, - indexFilterValues, - ); - expect(logs).to.have.length(0); - }); - it('should only get the logs with the correct indexed fields', async () => { - txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(tokenAddress, coinbase); - await web3Wrapper.awaitTransactionSuccessAsync(txHash); - txHash = await contractWrappers.token.setUnlimitedProxyAllowanceAsync(tokenAddress, addressWithoutFunds); - await web3Wrapper.awaitTransactionSuccessAsync(txHash); - const eventName = TokenEvents.Approval; - const indexFilterValues = { - _owner: coinbase, - }; - const logs = await contractWrappers.token.getLogsAsync( - tokenAddress, - eventName, - blockRange, - indexFilterValues, - ); - expect(logs).to.have.length(1); - const args = logs[0].args; - expect(args._owner).to.be.equal(coinbase); - }); - }); -}); -// tslint:disable:max-file-line-count - -function addEmptyWalletSubprovider(p: Provider): Provider { - const providerEngine = new Web3ProviderEngine(); - providerEngine.addProvider(new EmptyWalletSubprovider()); - const currentSubproviders = (p as any)._providers; - for (const subprovider of currentSubproviders) { - providerEngine.addProvider(subprovider); - } - providerEngine.start(); - return providerEngine; -} -- cgit