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/token_wrapper.ts | |
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/token_wrapper.ts')
-rw-r--r-- | packages/0x.js/src/contract_wrappers/token_wrapper.ts | 313 |
1 files changed, 313 insertions, 0 deletions
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; + } +} |