diff options
-rw-r--r-- | packages/0x.js/CHANGELOG.md | 1 | ||||
-rw-r--r-- | packages/0x.js/src/contract_wrappers/exchange_wrapper.ts | 3 | ||||
-rw-r--r-- | packages/0x.js/test/exchange_wrapper_test.ts | 13 | ||||
-rw-r--r-- | packages/react-shared/CHANGELOG.md | 3 | ||||
-rw-r--r-- | packages/react-shared/src/utils/colors.ts | 3 | ||||
-rw-r--r-- | packages/website/ts/components/eth_wrappers.tsx | 59 | ||||
-rw-r--r-- | packages/website/ts/components/portal.tsx | 40 | ||||
-rw-r--r-- | packages/website/ts/components/portal_menu.tsx | 13 | ||||
-rw-r--r-- | packages/website/ts/components/token_balances.tsx | 9 | ||||
-rw-r--r-- | packages/website/ts/components/wallet.tsx | 373 | ||||
-rw-r--r-- | packages/website/ts/types.ts | 15 |
11 files changed, 485 insertions, 47 deletions
diff --git a/packages/0x.js/CHANGELOG.md b/packages/0x.js/CHANGELOG.md index 7ce7126cb..fa3b3db91 100644 --- a/packages/0x.js/CHANGELOG.md +++ b/packages/0x.js/CHANGELOG.md @@ -3,6 +3,7 @@ ## v0.34.0 - _TBD_ * Update Kovan EtherToken artifact address to match TokenRegistry. + * Fix the bug causing `zeroEx.exchange.fillOrdersUpToAsync` validation to fail if there were some extra orders passed (#470) ## v0.33.2 - _March 18, 2018_ diff --git a/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts b/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts index 6414985e6..124750d81 100644 --- a/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts +++ b/packages/0x.js/src/contract_wrappers/exchange_wrapper.ts @@ -281,6 +281,9 @@ export class ExchangeWrapper extends ContractWrapper { zrxTokenAddress, ); filledTakerTokenAmount = filledTakerTokenAmount.plus(singleFilledTakerTokenAmount); + if (filledTakerTokenAmount.eq(fillTakerTokenAmount)) { + break; + } } } diff --git a/packages/0x.js/test/exchange_wrapper_test.ts b/packages/0x.js/test/exchange_wrapper_test.ts index 0a4ea608d..cfc390bae 100644 --- a/packages/0x.js/test/exchange_wrapper_test.ts +++ b/packages/0x.js/test/exchange_wrapper_test.ts @@ -596,6 +596,19 @@ describe('ExchangeWrapper', () => { const remainingFillAmount = fillableAmount.minus(1); expect(anotherFilledAmount).to.be.bignumber.equal(remainingFillAmount); }); + it('should successfully fill up to specified amount and leave the rest of the orders untouched', async () => { + const txHash = await zeroEx.exchange.fillOrdersUpToAsync( + signedOrders, + fillableAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + takerAddress, + ); + await zeroEx.awaitTransactionMinedAsync(txHash); + const filledAmount = await zeroEx.exchange.getFilledTakerAmountAsync(signedOrderHashHex); + const zeroAmount = await zeroEx.exchange.getFilledTakerAmountAsync(anotherOrderHashHex); + expect(filledAmount).to.be.bignumber.equal(fillableAmount); + expect(zeroAmount).to.be.bignumber.equal(0); + }); it('should successfully fill up to specified amount even if filling all orders would fail', async () => { const missingBalance = new BigNumber(1); // User will still have enough balance to fill up to 9, // but won't have 10 to fully fill all orders in a batch. diff --git a/packages/react-shared/CHANGELOG.md b/packages/react-shared/CHANGELOG.md index ae13588dc..51fb8e4b6 100644 --- a/packages/react-shared/CHANGELOG.md +++ b/packages/react-shared/CHANGELOG.md @@ -1,5 +1,6 @@ # CHANGELOG -## v0.0.2 - _TBD_ +## v0.1.0 - _TBD, 2018_ + * Added new colors (#468) * Fix section and menuItem text display to replace dashes with spaces. diff --git a/packages/react-shared/src/utils/colors.ts b/packages/react-shared/src/utils/colors.ts index 2eead95c7..ea0165305 100644 --- a/packages/react-shared/src/utils/colors.ts +++ b/packages/react-shared/src/utils/colors.ts @@ -45,4 +45,7 @@ export const colors = { orange: '#E69D00', amber800: '#FF8F00', darkYellow: '#caca03', + walletBoxShadow: 'rgba(56, 59, 137, 0.2)', + walletBorder: '#f5f5f6', + walletDefaultItemBackground: '#fbfbfc', }; diff --git a/packages/website/ts/components/eth_wrappers.tsx b/packages/website/ts/components/eth_wrappers.tsx index b12c637e5..59afeb50b 100644 --- a/packages/website/ts/components/eth_wrappers.tsx +++ b/packages/website/ts/components/eth_wrappers.tsx @@ -10,7 +10,14 @@ import ReactTooltip = require('react-tooltip'); import { Blockchain } from 'ts/blockchain'; import { EthWethConversionButton } from 'ts/components/eth_weth_conversion_button'; import { Dispatcher } from 'ts/redux/dispatcher'; -import { OutdatedWrappedEtherByNetworkId, Side, Token, TokenByAddress, TokenState } from 'ts/types'; +import { + OutdatedWrappedEtherByNetworkId, + Side, + Token, + TokenByAddress, + TokenState, + TokenStateByAddress, +} from 'ts/types'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; import { utils } from 'ts/utils/utils'; @@ -20,13 +27,6 @@ const ICON_DIMENSION = 40; const ETHER_ICON_PATH = '/images/ether.png'; const OUTDATED_WETH_ICON_PATH = '/images/wrapped_eth_gray.png'; -interface OutdatedWETHAddressToIsStateLoaded { - [address: string]: boolean; -} -interface OutdatedWETHStateByAddress { - [address: string]: TokenState; -} - interface EthWrappersProps { networkId: number; blockchain: Blockchain; @@ -39,9 +39,7 @@ interface EthWrappersProps { interface EthWrappersState { ethTokenState: TokenState; - isWethStateLoaded: boolean; - outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded; - outdatedWETHStateByAddress: OutdatedWETHStateByAddress; + outdatedWETHStateByAddress: TokenStateByAddress; } export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersState> { @@ -50,22 +48,20 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt super(props); this._isUnmounted = false; const outdatedWETHAddresses = this._getOutdatedWETHAddresses(); - const outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded = {}; - const outdatedWETHStateByAddress: OutdatedWETHStateByAddress = {}; + const outdatedWETHStateByAddress: TokenStateByAddress = {}; _.each(outdatedWETHAddresses, outdatedWETHAddress => { - outdatedWETHAddressToIsStateLoaded[outdatedWETHAddress] = false; outdatedWETHStateByAddress[outdatedWETHAddress] = { balance: new BigNumber(0), allowance: new BigNumber(0), + isLoaded: false, }; }); this.state = { - outdatedWETHAddressToIsStateLoaded, outdatedWETHStateByAddress, - isWethStateLoaded: false, ethTokenState: { balance: new BigNumber(0), allowance: new BigNumber(0), + isLoaded: false, }, }; } @@ -169,7 +165,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt {this._renderTokenLink(tokenLabel, etherscanUrl)} </TableRowColumn> <TableRowColumn> - {this.state.isWethStateLoaded ? ( + {this.state.ethTokenState.isLoaded ? ( `${wethBalance.toFixed(configs.AMOUNT_DISPLAY_PRECSION)} WETH` ) : ( <i className="zmdi zmdi-spinner zmdi-hc-spin" /> @@ -183,7 +179,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt networkId={this.props.networkId} isOutdatedWrappedEther={false} direction={Side.Receive} - isDisabled={!this.state.isWethStateLoaded} + isDisabled={!this.state.ethTokenState.isLoaded} ethToken={etherToken} dispatcher={this.props.dispatcher} blockchain={this.props.blockchain} @@ -266,8 +262,8 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt ...etherToken, address: outdatedWETHIfExists.address, }; - const isStateLoaded = this.state.outdatedWETHAddressToIsStateLoaded[outdatedWETHIfExists.address]; const outdatedEtherTokenState = this.state.outdatedWETHStateByAddress[outdatedWETHIfExists.address]; + const isStateLoaded = outdatedEtherTokenState.isLoaded; const balanceInEthIfExists = isStateLoaded ? ZeroEx.toUnitAmount(outdatedEtherTokenState.balance, constants.DECIMAL_PLACES_ETH).toFixed( configs.AMOUNT_DISPLAY_PRECSION, @@ -345,10 +341,15 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt ); } private async _onOutdatedConversionSuccessfulAsync(outdatedWETHAddress: string) { + const currentOutdatedWETHState = this.state.outdatedWETHStateByAddress[outdatedWETHAddress]; this.setState({ - outdatedWETHAddressToIsStateLoaded: { - ...this.state.outdatedWETHAddressToIsStateLoaded, - [outdatedWETHAddress]: false, + outdatedWETHStateByAddress: { + ...this.state.outdatedWETHStateByAddress, + [outdatedWETHAddress]: { + balance: currentOutdatedWETHState.balance, + allowance: currentOutdatedWETHState.allowance, + isLoaded: false, + }, }, }); const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress; @@ -357,15 +358,12 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt outdatedWETHAddress, ); this.setState({ - outdatedWETHAddressToIsStateLoaded: { - ...this.state.outdatedWETHAddressToIsStateLoaded, - [outdatedWETHAddress]: true, - }, outdatedWETHStateByAddress: { ...this.state.outdatedWETHStateByAddress, [outdatedWETHAddress]: { balance, allowance, + isLoaded: true, }, }, }); @@ -380,8 +378,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt ); const outdatedWETHAddresses = this._getOutdatedWETHAddresses(); - const outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded = {}; - const outdatedWETHStateByAddress: OutdatedWETHStateByAddress = {}; + const outdatedWETHStateByAddress: TokenStateByAddress = {}; for (const address of outdatedWETHAddresses) { const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( userAddressIfExists, @@ -390,18 +387,17 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt outdatedWETHStateByAddress[address] = { balance, allowance, + isLoaded: true, }; - outdatedWETHAddressToIsStateLoaded[address] = true; } if (!this._isUnmounted) { this.setState({ outdatedWETHStateByAddress, - outdatedWETHAddressToIsStateLoaded, ethTokenState: { balance: wethBalance, allowance: wethAllowance, + isLoaded: true, }, - isWethStateLoaded: true, }); } } @@ -434,6 +430,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt ethTokenState: { balance, allowance, + isLoaded: true, }, }); } diff --git a/packages/website/ts/components/portal.tsx b/packages/website/ts/components/portal.tsx index 5bdb5bde9..59eaca67e 100644 --- a/packages/website/ts/components/portal.tsx +++ b/packages/website/ts/components/portal.tsx @@ -19,12 +19,22 @@ import { TokenBalances } from 'ts/components/token_balances'; import { TopBar } from 'ts/components/top_bar/top_bar'; import { TradeHistory } from 'ts/components/trade_history/trade_history'; import { FlashMessage } from 'ts/components/ui/flash_message'; +import { Wallet } from 'ts/components/wallet'; import { GenerateOrderForm } from 'ts/containers/generate_order_form'; import { localStorage } from 'ts/local_storage/local_storage'; import { Dispatcher } from 'ts/redux/dispatcher'; import { portalOrderSchema } from 'ts/schemas/portal_order_schema'; import { validator } from 'ts/schemas/validator'; -import { BlockchainErrs, HashData, Order, ProviderType, ScreenWidths, TokenByAddress, WebsitePaths } from 'ts/types'; +import { + BlockchainErrs, + Environments, + HashData, + Order, + ProviderType, + ScreenWidths, + TokenByAddress, + WebsitePaths, +} from 'ts/types'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; import { Translate } from 'ts/utils/translate'; @@ -194,6 +204,12 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { <div className="py2" style={{ backgroundColor: colors.grey50 }}> {this.props.blockchainIsLoaded ? ( <Switch> + {configs.ENVIRONMENT === Environments.DEVELOPMENT && ( + <Route + path={`${WebsitePaths.Portal}/wallet`} + render={this._renderWallet.bind(this)} + /> + )} <Route path={`${WebsitePaths.Portal}/weth`} render={this._renderEthWrapper.bind(this)} @@ -272,6 +288,28 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { isLedgerDialogOpen: !this.state.isLedgerDialogOpen, }); } + private _renderWallet() { + const allTokens = _.values(this.props.tokenByAddress); + const trackedTokens = _.filter(allTokens, t => t.isTracked); + return ( + <div className="flex flex-center"> + <div className="mx-auto"> + <Wallet + userAddress={this.props.userAddress} + networkId={this.props.networkId} + blockchain={this._blockchain} + blockchainIsLoaded={this.props.blockchainIsLoaded} + blockchainErr={this.props.blockchainErr} + dispatcher={this.props.dispatcher} + tokenByAddress={this.props.tokenByAddress} + trackedTokens={trackedTokens} + userEtherBalanceInWei={this.props.userEtherBalanceInWei} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + /> + </div> + </div> + ); + } private _renderEthWrapper() { return ( <EthWrappers diff --git a/packages/website/ts/components/portal_menu.tsx b/packages/website/ts/components/portal_menu.tsx index a2f9340c8..9ab611556 100644 --- a/packages/website/ts/components/portal_menu.tsx +++ b/packages/website/ts/components/portal_menu.tsx @@ -1,7 +1,8 @@ import * as _ from 'lodash'; import * as React from 'react'; import { MenuItem } from 'ts/components/ui/menu_item'; -import { WebsitePaths } from 'ts/types'; +import { Environments, WebsitePaths } from 'ts/types'; +import { configs } from 'ts/utils/configs'; export interface PortalMenuProps { menuItemStyle: React.CSSProperties; @@ -57,6 +58,16 @@ export class PortalMenu extends React.Component<PortalMenuProps, PortalMenuState > {this._renderMenuItemWithIcon('Wrap ETH', 'zmdi-circle-o')} </MenuItem> + {configs.ENVIRONMENT === Environments.DEVELOPMENT && ( + <MenuItem + style={this.props.menuItemStyle} + className="py2" + to={`${WebsitePaths.Portal}/wallet`} + onClick={this.props.onClick.bind(this)} + > + {this._renderMenuItemWithIcon('Wallet', 'zmdi-balance-wallet')} + </MenuItem> + )} </div> ); } diff --git a/packages/website/ts/components/token_balances.tsx b/packages/website/ts/components/token_balances.tsx index 186393c4f..b4a710ef4 100644 --- a/packages/website/ts/components/token_balances.tsx +++ b/packages/website/ts/components/token_balances.tsx @@ -37,6 +37,7 @@ import { ScreenWidths, Token, TokenByAddress, + TokenStateByAddress, TokenVisibility, } from 'ts/types'; import { configs } from 'ts/utils/configs'; @@ -61,14 +62,6 @@ const styles: Styles = { }, }; -interface TokenStateByAddress { - [address: string]: { - balance: BigNumber; - allowance: BigNumber; - isLoaded: boolean; - }; -} - interface TokenBalancesProps { blockchain: Blockchain; blockchainErr: BlockchainErrs; diff --git a/packages/website/ts/components/wallet.tsx b/packages/website/ts/components/wallet.tsx new file mode 100644 index 000000000..8c6ef9cad --- /dev/null +++ b/packages/website/ts/components/wallet.tsx @@ -0,0 +1,373 @@ +import { ZeroEx } from '0x.js'; +import { + colors, + constants as sharedConstants, + EtherscanLinkSuffixes, + Styles, + utils as sharedUtils, +} from '@0xproject/react-shared'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; +import FlatButton from 'material-ui/FlatButton'; +import { List, ListItem } from 'material-ui/List'; +import NavigationArrowDownward from 'material-ui/svg-icons/navigation/arrow-downward'; +import NavigationArrowUpward from 'material-ui/svg-icons/navigation/arrow-upward'; +import * as React from 'react'; +import ReactTooltip = require('react-tooltip'); +import firstBy = require('thenby'); + +import { Blockchain } from 'ts/blockchain'; +import { AllowanceToggle } from 'ts/components/inputs/allowance_toggle'; +import { Identicon } from 'ts/components/ui/identicon'; +import { TokenIcon } from 'ts/components/ui/token_icon'; +import { Dispatcher } from 'ts/redux/dispatcher'; +import { BalanceErrs, BlockchainErrs, Token, TokenByAddress, TokenState, TokenStateByAddress } from 'ts/types'; +import { constants } from 'ts/utils/constants'; +import { utils } from 'ts/utils/utils'; + +export interface WalletProps { + userAddress?: string; + networkId?: number; + blockchain: Blockchain; + blockchainIsLoaded: boolean; + blockchainErr: BlockchainErrs; + dispatcher: Dispatcher; + tokenByAddress: TokenByAddress; + trackedTokens: Token[]; + userEtherBalanceInWei: BigNumber; + lastForceTokenStateRefetch: number; +} + +interface WalletState { + trackedTokenStateByAddress: TokenStateByAddress; +} + +enum WrappedEtherAction { + Wrap, + Unwrap, +} + +interface AllowanceToggleConfig { + token: Token; + tokenState: TokenState; +} + +interface AccessoryItemConfig { + wrappedEtherAction?: WrappedEtherAction; + allowanceToggleConfig?: AllowanceToggleConfig; +} + +const styles: Styles = { + wallet: { + width: 346, + backgroundColor: colors.white, + borderBottomRightRadius: 10, + borderBottomLeftRadius: 10, + borderTopRightRadius: 10, + borderTopLeftRadius: 10, + boxShadow: `0px 4px 6px ${colors.walletBoxShadow}`, + overflow: 'hidden', + }, + list: { + padding: 0, + }, + tokenItemInnerDiv: { + paddingLeft: 60, + }, + headerItemInnerDiv: { + paddingLeft: 65, + }, + footerItemInnerDiv: { + paddingLeft: 24, + }, + borderedItem: { + borderBottomColor: colors.walletBorder, + borderBottomStyle: 'solid', + borderWidth: 1, + }, + tokenItem: { + backgroundColor: colors.walletDefaultItemBackground, + paddingTop: 8, + paddingBottom: 8, + }, + headerItem: { + paddingTop: 8, + paddingBottom: 8, + }, + wrappedEtherButtonLabel: { + fontSize: 12, + }, + amountLabel: { + fontWeight: 'bold', + color: colors.black, + }, +}; + +const ETHER_ICON_PATH = '/images/ether.png'; +const ETHER_TOKEN_SYMBOL = 'WETH'; +const ZRX_TOKEN_SYMBOL = 'ZRX'; +const ETHER_SYMBOL = 'ETH'; +const ICON_DIMENSION = 24; +const TOKEN_AMOUNT_DISPLAY_PRECISION = 3; + +export class Wallet extends React.Component<WalletProps, WalletState> { + private _isUnmounted: boolean; + constructor(props: WalletProps) { + super(props); + this._isUnmounted = false; + const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(props.trackedTokens); + this.state = { + trackedTokenStateByAddress: initialTrackedTokenStateByAddress, + }; + } + public componentWillMount() { + const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress); + // tslint:disable-next-line:no-floating-promises + this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses); + } + public componentWillUnmount() { + this._isUnmounted = true; + } + public componentWillReceiveProps(nextProps: WalletProps) { + if ( + nextProps.userAddress !== this.props.userAddress || + nextProps.networkId !== this.props.networkId || + nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch + ) { + const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress); + // tslint:disable-next-line:no-floating-promises + this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses); + } + if (!_.isEqual(nextProps.trackedTokens, this.props.trackedTokens)) { + const newTokens = _.difference(nextProps.trackedTokens, this.props.trackedTokens); + const newTokenAddresses = _.map(newTokens, token => token.address); + // Add placeholder entry for this token to the state, since fetching the + // balance/allowance is asynchronous + const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress; + _.each(newTokenAddresses, (tokenAddress: string) => { + trackedTokenStateByAddress[tokenAddress] = { + balance: new BigNumber(0), + allowance: new BigNumber(0), + isLoaded: false, + }; + }); + this.setState({ + trackedTokenStateByAddress, + }); + // Fetch the actual balance/allowance. + // tslint:disable-next-line:no-floating-promises + this._fetchBalancesAndAllowancesAsync(newTokenAddresses); + } + } + public render() { + const isReadyToRender = this.props.blockchainIsLoaded && this.props.blockchainErr === BlockchainErrs.NoError; + return <div style={styles.wallet}>{isReadyToRender && this._renderRows()}</div>; + } + private _renderRows() { + return ( + <List style={styles.list}> + {_.concat( + this._renderHeaderRows(), + this._renderEthRows(), + this._renderTokenRows(), + this._renderFooterRows(), + )} + </List> + ); + } + private _renderHeaderRows() { + const userAddress = this.props.userAddress; + const primaryText = utils.getAddressBeginAndEnd(userAddress); + return ( + <ListItem + primaryText={primaryText} + leftIcon={<Identicon address={userAddress} diameter={ICON_DIMENSION} />} + style={{ ...styles.headerItem, ...styles.borderedItem }} + innerDivStyle={styles.headerItemInnerDiv} + /> + ); + } + private _renderFooterRows() { + const primaryText = '+ other tokens'; + return ( + <ListItem primaryText={primaryText} style={styles.borderedItem} innerDivStyle={styles.footerItemInnerDiv} /> + ); + } + private _renderEthRows() { + const primaryText = this._renderAmount( + this.props.userEtherBalanceInWei, + constants.DECIMAL_PLACES_ETH, + ETHER_SYMBOL, + ); + const accessoryItemConfig = { + wrappedEtherAction: WrappedEtherAction.Wrap, + }; + return ( + <ListItem + primaryText={primaryText} + leftIcon={<img style={{ width: ICON_DIMENSION, height: ICON_DIMENSION }} src={ETHER_ICON_PATH} />} + rightAvatar={this._renderAccessoryItems(accessoryItemConfig)} + style={{ ...styles.tokenItem, ...styles.borderedItem }} + innerDivStyle={styles.tokenItemInnerDiv} + /> + ); + } + private _renderTokenRows() { + const trackedTokens = this.props.trackedTokens; + const trackedTokensStartingWithEtherToken = trackedTokens.sort( + firstBy((t: Token) => t.symbol !== ETHER_TOKEN_SYMBOL) + .thenBy((t: Token) => t.symbol !== ZRX_TOKEN_SYMBOL) + .thenBy('address'), + ); + return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this)); + } + private _renderTokenRow(token: Token) { + const tokenState = this.state.trackedTokenStateByAddress[token.address]; + const tokenLink = sharedUtils.getEtherScanLinkIfExists( + token.address, + this.props.networkId, + EtherscanLinkSuffixes.Address, + ); + const amount = this._renderAmount(tokenState.balance, token.decimals, token.symbol); + const wrappedEtherAction = token.symbol === ETHER_TOKEN_SYMBOL ? WrappedEtherAction.Unwrap : undefined; + const accessoryItemConfig: AccessoryItemConfig = { + wrappedEtherAction, + allowanceToggleConfig: { + token, + tokenState, + }, + }; + return ( + <ListItem + primaryText={amount} + leftIcon={this._renderTokenIcon(token, tokenLink)} + rightAvatar={this._renderAccessoryItems(accessoryItemConfig)} + style={{ ...styles.tokenItem, ...styles.borderedItem }} + innerDivStyle={styles.tokenItemInnerDiv} + /> + ); + } + private _renderAccessoryItems(config: AccessoryItemConfig) { + const shouldShowWrappedEtherAction = !_.isUndefined(config.wrappedEtherAction); + const shouldShowToggle = !_.isUndefined(config.allowanceToggleConfig); + return ( + <div style={{ width: 160 }}> + <div className="flex"> + <div className="flex-auto"> + {shouldShowWrappedEtherAction && this._renderWrappedEtherButton(config.wrappedEtherAction)} + </div> + <div className="flex-last py1"> + {shouldShowToggle && this._renderAllowanceToggle(config.allowanceToggleConfig)} + </div> + </div> + </div> + ); + } + private _renderAllowanceToggle(config: AllowanceToggleConfig) { + return ( + <AllowanceToggle + networkId={this.props.networkId} + blockchain={this.props.blockchain} + dispatcher={this.props.dispatcher} + token={config.token} + tokenState={config.tokenState} + onErrorOccurred={_.noop} // TODO: Error handling + userAddress={this.props.userAddress} + isDisabled={!config.tokenState.isLoaded} + refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, config.token.address)} + /> + ); + } + private _renderAmount(amount: BigNumber, decimals: number, symbol: string) { + const unitAmount = ZeroEx.toUnitAmount(amount, decimals); + const formattedAmount = unitAmount.toPrecision(TOKEN_AMOUNT_DISPLAY_PRECISION); + const result = `${formattedAmount} ${symbol}`; + return <div style={styles.amountLabel}>{result}</div>; + } + private _renderTokenIcon(token: Token, tokenLink?: string) { + const tooltipId = `tooltip-${token.address}`; + const tokenIcon = <TokenIcon token={token} diameter={ICON_DIMENSION} />; + if (_.isUndefined(tokenLink)) { + return tokenIcon; + } else { + return ( + <a href={tokenLink} target="_blank" style={{ textDecoration: 'none' }}> + {tokenIcon} + </a> + ); + } + } + private _renderWrappedEtherButton(action: WrappedEtherAction) { + let buttonLabel; + let buttonIcon; + switch (action) { + case WrappedEtherAction.Wrap: + buttonLabel = 'wrap'; + buttonIcon = <NavigationArrowDownward />; + break; + case WrappedEtherAction.Unwrap: + buttonLabel = 'unwrap'; + buttonIcon = <NavigationArrowUpward />; + break; + default: + throw utils.spawnSwitchErr('wrappedEtherAction', action); + } + return ( + <FlatButton + label={buttonLabel} + labelPosition="after" + primary={true} + icon={buttonIcon} + labelStyle={styles.wrappedEtherButtonLabel} + /> + ); + } + private _getInitialTrackedTokenStateByAddress(trackedTokens: Token[]) { + const trackedTokenStateByAddress: TokenStateByAddress = {}; + _.each(trackedTokens, token => { + trackedTokenStateByAddress[token.address] = { + balance: new BigNumber(0), + allowance: new BigNumber(0), + isLoaded: false, + }; + }); + return trackedTokenStateByAddress; + } + private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]) { + const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress; + const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress; + for (const tokenAddress of tokenAddresses) { + const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( + userAddressIfExists, + tokenAddress, + ); + trackedTokenStateByAddress[tokenAddress] = { + balance, + allowance, + isLoaded: true, + }; + } + if (!this._isUnmounted) { + this.setState({ + trackedTokenStateByAddress, + }); + } + } + private async _refetchTokenStateAsync(tokenAddress: string) { + const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress; + const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( + userAddressIfExists, + tokenAddress, + ); + this.setState({ + trackedTokenStateByAddress: { + ...this.state.trackedTokenStateByAddress, + [tokenAddress]: { + balance, + allowance, + isLoaded: true, + }, + }, + }); + } +} diff --git a/packages/website/ts/types.ts b/packages/website/ts/types.ts index 24b4f5214..104d2e50f 100644 --- a/packages/website/ts/types.ts +++ b/packages/website/ts/types.ts @@ -21,11 +21,6 @@ export interface TokenByAddress { [address: string]: Token; } -export interface TokenState { - allowance: BigNumber; - balance: BigNumber; -} - export interface AssetToken { address?: string; amount?: BigNumber; @@ -484,4 +479,14 @@ export interface OutdatedWrappedEtherByNetworkId { timestampMsRange: TimestampMsRange; }; } + +export interface TokenStateByAddress { + [address: string]: TokenState; +} + +export interface TokenState { + balance: BigNumber; + allowance: BigNumber; + isLoaded: boolean; +} // tslint:disable:max-file-line-count |