Merge branch 'development' into addExtraDocs
* development: Change blockchain prop to not optional Other style changes Updated padding and colors Refactor TokenState type Fix a bug causing fillOrdersUpTo validation to fail because of some extra orders being passed Implement initial wallet interface # Conflicts: # packages/react-shared/CHANGELOG.md # packages/website/ts/types.ts
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
## v0.34.0 - _TBD_
|
## v0.34.0 - _TBD_
|
||||||
|
|
||||||
* Update Kovan EtherToken artifact address to match TokenRegistry.
|
* 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_
|
## v0.33.2 - _March 18, 2018_
|
||||||
|
|
||||||
|
|||||||
@@ -281,6 +281,9 @@ export class ExchangeWrapper extends ContractWrapper {
|
|||||||
zrxTokenAddress,
|
zrxTokenAddress,
|
||||||
);
|
);
|
||||||
filledTakerTokenAmount = filledTakerTokenAmount.plus(singleFilledTakerTokenAmount);
|
filledTakerTokenAmount = filledTakerTokenAmount.plus(singleFilledTakerTokenAmount);
|
||||||
|
if (filledTakerTokenAmount.eq(fillTakerTokenAmount)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -596,6 +596,19 @@ describe('ExchangeWrapper', () => {
|
|||||||
const remainingFillAmount = fillableAmount.minus(1);
|
const remainingFillAmount = fillableAmount.minus(1);
|
||||||
expect(anotherFilledAmount).to.be.bignumber.equal(remainingFillAmount);
|
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 () => {
|
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,
|
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.
|
// but won't have 10 to fully fill all orders in a batch.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# CHANGELOG
|
# 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.
|
* Fix section and menuItem text display to replace dashes with spaces.
|
||||||
|
|||||||
@@ -45,4 +45,7 @@ export const colors = {
|
|||||||
orange: '#E69D00',
|
orange: '#E69D00',
|
||||||
amber800: '#FF8F00',
|
amber800: '#FF8F00',
|
||||||
darkYellow: '#caca03',
|
darkYellow: '#caca03',
|
||||||
|
walletBoxShadow: 'rgba(56, 59, 137, 0.2)',
|
||||||
|
walletBorder: '#f5f5f6',
|
||||||
|
walletDefaultItemBackground: '#fbfbfc',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,14 @@ import ReactTooltip = require('react-tooltip');
|
|||||||
import { Blockchain } from 'ts/blockchain';
|
import { Blockchain } from 'ts/blockchain';
|
||||||
import { EthWethConversionButton } from 'ts/components/eth_weth_conversion_button';
|
import { EthWethConversionButton } from 'ts/components/eth_weth_conversion_button';
|
||||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
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 { configs } from 'ts/utils/configs';
|
||||||
import { constants } from 'ts/utils/constants';
|
import { constants } from 'ts/utils/constants';
|
||||||
import { utils } from 'ts/utils/utils';
|
import { utils } from 'ts/utils/utils';
|
||||||
@@ -20,13 +27,6 @@ const ICON_DIMENSION = 40;
|
|||||||
const ETHER_ICON_PATH = '/images/ether.png';
|
const ETHER_ICON_PATH = '/images/ether.png';
|
||||||
const OUTDATED_WETH_ICON_PATH = '/images/wrapped_eth_gray.png';
|
const OUTDATED_WETH_ICON_PATH = '/images/wrapped_eth_gray.png';
|
||||||
|
|
||||||
interface OutdatedWETHAddressToIsStateLoaded {
|
|
||||||
[address: string]: boolean;
|
|
||||||
}
|
|
||||||
interface OutdatedWETHStateByAddress {
|
|
||||||
[address: string]: TokenState;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EthWrappersProps {
|
interface EthWrappersProps {
|
||||||
networkId: number;
|
networkId: number;
|
||||||
blockchain: Blockchain;
|
blockchain: Blockchain;
|
||||||
@@ -39,9 +39,7 @@ interface EthWrappersProps {
|
|||||||
|
|
||||||
interface EthWrappersState {
|
interface EthWrappersState {
|
||||||
ethTokenState: TokenState;
|
ethTokenState: TokenState;
|
||||||
isWethStateLoaded: boolean;
|
outdatedWETHStateByAddress: TokenStateByAddress;
|
||||||
outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded;
|
|
||||||
outdatedWETHStateByAddress: OutdatedWETHStateByAddress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersState> {
|
export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersState> {
|
||||||
@@ -50,22 +48,20 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
|
|||||||
super(props);
|
super(props);
|
||||||
this._isUnmounted = false;
|
this._isUnmounted = false;
|
||||||
const outdatedWETHAddresses = this._getOutdatedWETHAddresses();
|
const outdatedWETHAddresses = this._getOutdatedWETHAddresses();
|
||||||
const outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded = {};
|
const outdatedWETHStateByAddress: TokenStateByAddress = {};
|
||||||
const outdatedWETHStateByAddress: OutdatedWETHStateByAddress = {};
|
|
||||||
_.each(outdatedWETHAddresses, outdatedWETHAddress => {
|
_.each(outdatedWETHAddresses, outdatedWETHAddress => {
|
||||||
outdatedWETHAddressToIsStateLoaded[outdatedWETHAddress] = false;
|
|
||||||
outdatedWETHStateByAddress[outdatedWETHAddress] = {
|
outdatedWETHStateByAddress[outdatedWETHAddress] = {
|
||||||
balance: new BigNumber(0),
|
balance: new BigNumber(0),
|
||||||
allowance: new BigNumber(0),
|
allowance: new BigNumber(0),
|
||||||
|
isLoaded: false,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
this.state = {
|
this.state = {
|
||||||
outdatedWETHAddressToIsStateLoaded,
|
|
||||||
outdatedWETHStateByAddress,
|
outdatedWETHStateByAddress,
|
||||||
isWethStateLoaded: false,
|
|
||||||
ethTokenState: {
|
ethTokenState: {
|
||||||
balance: new BigNumber(0),
|
balance: new BigNumber(0),
|
||||||
allowance: 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)}
|
{this._renderTokenLink(tokenLabel, etherscanUrl)}
|
||||||
</TableRowColumn>
|
</TableRowColumn>
|
||||||
<TableRowColumn>
|
<TableRowColumn>
|
||||||
{this.state.isWethStateLoaded ? (
|
{this.state.ethTokenState.isLoaded ? (
|
||||||
`${wethBalance.toFixed(configs.AMOUNT_DISPLAY_PRECSION)} WETH`
|
`${wethBalance.toFixed(configs.AMOUNT_DISPLAY_PRECSION)} WETH`
|
||||||
) : (
|
) : (
|
||||||
<i className="zmdi zmdi-spinner zmdi-hc-spin" />
|
<i className="zmdi zmdi-spinner zmdi-hc-spin" />
|
||||||
@@ -183,7 +179,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
|
|||||||
networkId={this.props.networkId}
|
networkId={this.props.networkId}
|
||||||
isOutdatedWrappedEther={false}
|
isOutdatedWrappedEther={false}
|
||||||
direction={Side.Receive}
|
direction={Side.Receive}
|
||||||
isDisabled={!this.state.isWethStateLoaded}
|
isDisabled={!this.state.ethTokenState.isLoaded}
|
||||||
ethToken={etherToken}
|
ethToken={etherToken}
|
||||||
dispatcher={this.props.dispatcher}
|
dispatcher={this.props.dispatcher}
|
||||||
blockchain={this.props.blockchain}
|
blockchain={this.props.blockchain}
|
||||||
@@ -266,8 +262,8 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
|
|||||||
...etherToken,
|
...etherToken,
|
||||||
address: outdatedWETHIfExists.address,
|
address: outdatedWETHIfExists.address,
|
||||||
};
|
};
|
||||||
const isStateLoaded = this.state.outdatedWETHAddressToIsStateLoaded[outdatedWETHIfExists.address];
|
|
||||||
const outdatedEtherTokenState = this.state.outdatedWETHStateByAddress[outdatedWETHIfExists.address];
|
const outdatedEtherTokenState = this.state.outdatedWETHStateByAddress[outdatedWETHIfExists.address];
|
||||||
|
const isStateLoaded = outdatedEtherTokenState.isLoaded;
|
||||||
const balanceInEthIfExists = isStateLoaded
|
const balanceInEthIfExists = isStateLoaded
|
||||||
? ZeroEx.toUnitAmount(outdatedEtherTokenState.balance, constants.DECIMAL_PLACES_ETH).toFixed(
|
? ZeroEx.toUnitAmount(outdatedEtherTokenState.balance, constants.DECIMAL_PLACES_ETH).toFixed(
|
||||||
configs.AMOUNT_DISPLAY_PRECSION,
|
configs.AMOUNT_DISPLAY_PRECSION,
|
||||||
@@ -345,10 +341,15 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
private async _onOutdatedConversionSuccessfulAsync(outdatedWETHAddress: string) {
|
private async _onOutdatedConversionSuccessfulAsync(outdatedWETHAddress: string) {
|
||||||
|
const currentOutdatedWETHState = this.state.outdatedWETHStateByAddress[outdatedWETHAddress];
|
||||||
this.setState({
|
this.setState({
|
||||||
outdatedWETHAddressToIsStateLoaded: {
|
outdatedWETHStateByAddress: {
|
||||||
...this.state.outdatedWETHAddressToIsStateLoaded,
|
...this.state.outdatedWETHStateByAddress,
|
||||||
[outdatedWETHAddress]: false,
|
[outdatedWETHAddress]: {
|
||||||
|
balance: currentOutdatedWETHState.balance,
|
||||||
|
allowance: currentOutdatedWETHState.allowance,
|
||||||
|
isLoaded: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
|
const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
|
||||||
@@ -357,15 +358,12 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
|
|||||||
outdatedWETHAddress,
|
outdatedWETHAddress,
|
||||||
);
|
);
|
||||||
this.setState({
|
this.setState({
|
||||||
outdatedWETHAddressToIsStateLoaded: {
|
|
||||||
...this.state.outdatedWETHAddressToIsStateLoaded,
|
|
||||||
[outdatedWETHAddress]: true,
|
|
||||||
},
|
|
||||||
outdatedWETHStateByAddress: {
|
outdatedWETHStateByAddress: {
|
||||||
...this.state.outdatedWETHStateByAddress,
|
...this.state.outdatedWETHStateByAddress,
|
||||||
[outdatedWETHAddress]: {
|
[outdatedWETHAddress]: {
|
||||||
balance,
|
balance,
|
||||||
allowance,
|
allowance,
|
||||||
|
isLoaded: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -380,8 +378,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
|
|||||||
);
|
);
|
||||||
|
|
||||||
const outdatedWETHAddresses = this._getOutdatedWETHAddresses();
|
const outdatedWETHAddresses = this._getOutdatedWETHAddresses();
|
||||||
const outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded = {};
|
const outdatedWETHStateByAddress: TokenStateByAddress = {};
|
||||||
const outdatedWETHStateByAddress: OutdatedWETHStateByAddress = {};
|
|
||||||
for (const address of outdatedWETHAddresses) {
|
for (const address of outdatedWETHAddresses) {
|
||||||
const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
|
const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
|
||||||
userAddressIfExists,
|
userAddressIfExists,
|
||||||
@@ -390,18 +387,17 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
|
|||||||
outdatedWETHStateByAddress[address] = {
|
outdatedWETHStateByAddress[address] = {
|
||||||
balance,
|
balance,
|
||||||
allowance,
|
allowance,
|
||||||
|
isLoaded: true,
|
||||||
};
|
};
|
||||||
outdatedWETHAddressToIsStateLoaded[address] = true;
|
|
||||||
}
|
}
|
||||||
if (!this._isUnmounted) {
|
if (!this._isUnmounted) {
|
||||||
this.setState({
|
this.setState({
|
||||||
outdatedWETHStateByAddress,
|
outdatedWETHStateByAddress,
|
||||||
outdatedWETHAddressToIsStateLoaded,
|
|
||||||
ethTokenState: {
|
ethTokenState: {
|
||||||
balance: wethBalance,
|
balance: wethBalance,
|
||||||
allowance: wethAllowance,
|
allowance: wethAllowance,
|
||||||
|
isLoaded: true,
|
||||||
},
|
},
|
||||||
isWethStateLoaded: true,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,6 +430,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
|
|||||||
ethTokenState: {
|
ethTokenState: {
|
||||||
balance,
|
balance,
|
||||||
allowance,
|
allowance,
|
||||||
|
isLoaded: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,22 @@ import { TokenBalances } from 'ts/components/token_balances';
|
|||||||
import { TopBar } from 'ts/components/top_bar/top_bar';
|
import { TopBar } from 'ts/components/top_bar/top_bar';
|
||||||
import { TradeHistory } from 'ts/components/trade_history/trade_history';
|
import { TradeHistory } from 'ts/components/trade_history/trade_history';
|
||||||
import { FlashMessage } from 'ts/components/ui/flash_message';
|
import { FlashMessage } from 'ts/components/ui/flash_message';
|
||||||
|
import { Wallet } from 'ts/components/wallet';
|
||||||
import { GenerateOrderForm } from 'ts/containers/generate_order_form';
|
import { GenerateOrderForm } from 'ts/containers/generate_order_form';
|
||||||
import { localStorage } from 'ts/local_storage/local_storage';
|
import { localStorage } from 'ts/local_storage/local_storage';
|
||||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||||
import { portalOrderSchema } from 'ts/schemas/portal_order_schema';
|
import { portalOrderSchema } from 'ts/schemas/portal_order_schema';
|
||||||
import { validator } from 'ts/schemas/validator';
|
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 { configs } from 'ts/utils/configs';
|
||||||
import { constants } from 'ts/utils/constants';
|
import { constants } from 'ts/utils/constants';
|
||||||
import { Translate } from 'ts/utils/translate';
|
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 }}>
|
<div className="py2" style={{ backgroundColor: colors.grey50 }}>
|
||||||
{this.props.blockchainIsLoaded ? (
|
{this.props.blockchainIsLoaded ? (
|
||||||
<Switch>
|
<Switch>
|
||||||
|
{configs.ENVIRONMENT === Environments.DEVELOPMENT && (
|
||||||
|
<Route
|
||||||
|
path={`${WebsitePaths.Portal}/wallet`}
|
||||||
|
render={this._renderWallet.bind(this)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Route
|
<Route
|
||||||
path={`${WebsitePaths.Portal}/weth`}
|
path={`${WebsitePaths.Portal}/weth`}
|
||||||
render={this._renderEthWrapper.bind(this)}
|
render={this._renderEthWrapper.bind(this)}
|
||||||
@@ -272,6 +288,28 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
|
|||||||
isLedgerDialogOpen: !this.state.isLedgerDialogOpen,
|
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() {
|
private _renderEthWrapper() {
|
||||||
return (
|
return (
|
||||||
<EthWrappers
|
<EthWrappers
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { MenuItem } from 'ts/components/ui/menu_item';
|
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 {
|
export interface PortalMenuProps {
|
||||||
menuItemStyle: React.CSSProperties;
|
menuItemStyle: React.CSSProperties;
|
||||||
@@ -57,6 +58,16 @@ export class PortalMenu extends React.Component<PortalMenuProps, PortalMenuState
|
|||||||
>
|
>
|
||||||
{this._renderMenuItemWithIcon('Wrap ETH', 'zmdi-circle-o')}
|
{this._renderMenuItemWithIcon('Wrap ETH', 'zmdi-circle-o')}
|
||||||
</MenuItem>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
ScreenWidths,
|
ScreenWidths,
|
||||||
Token,
|
Token,
|
||||||
TokenByAddress,
|
TokenByAddress,
|
||||||
|
TokenStateByAddress,
|
||||||
TokenVisibility,
|
TokenVisibility,
|
||||||
} from 'ts/types';
|
} from 'ts/types';
|
||||||
import { configs } from 'ts/utils/configs';
|
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 {
|
interface TokenBalancesProps {
|
||||||
blockchain: Blockchain;
|
blockchain: Blockchain;
|
||||||
blockchainErr: BlockchainErrs;
|
blockchainErr: BlockchainErrs;
|
||||||
|
|||||||
373
packages/website/ts/components/wallet.tsx
Normal file
373
packages/website/ts/components/wallet.tsx
Normal file
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,11 +21,6 @@ export interface TokenByAddress {
|
|||||||
[address: string]: Token;
|
[address: string]: Token;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TokenState {
|
|
||||||
allowance: BigNumber;
|
|
||||||
balance: BigNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AssetToken {
|
export interface AssetToken {
|
||||||
address?: string;
|
address?: string;
|
||||||
amount?: BigNumber;
|
amount?: BigNumber;
|
||||||
@@ -484,4 +479,14 @@ export interface OutdatedWrappedEtherByNetworkId {
|
|||||||
timestampMsRange: TimestampMsRange;
|
timestampMsRange: TimestampMsRange;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TokenStateByAddress {
|
||||||
|
[address: string]: TokenState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenState {
|
||||||
|
balance: BigNumber;
|
||||||
|
allowance: BigNumber;
|
||||||
|
isLoaded: boolean;
|
||||||
|
}
|
||||||
// tslint:disable:max-file-line-count
|
// tslint:disable:max-file-line-count
|
||||||
|
|||||||
Reference in New Issue
Block a user