diff options
author | Fabio Berger <me@fabioberger.com> | 2018-05-10 23:08:07 +0800 |
---|---|---|
committer | Fabio Berger <me@fabioberger.com> | 2018-05-10 23:08:07 +0800 |
commit | cd5f00ac4d41221c99eb8ce767e63e09a6de6a11 (patch) | |
tree | 2b1865b5fa6ef86965ea753c032ffaba48403921 /packages/website | |
parent | 23c4027c83b9f30fad352615386b988084f8b39f (diff) | |
parent | c64ad1af28ef116e210aafb3ea6ad2138361cd7c (diff) | |
download | dexon-sol-tools-cd5f00ac4d41221c99eb8ce767e63e09a6de6a11.tar dexon-sol-tools-cd5f00ac4d41221c99eb8ce767e63e09a6de6a11.tar.gz dexon-sol-tools-cd5f00ac4d41221c99eb8ce767e63e09a6de6a11.tar.bz2 dexon-sol-tools-cd5f00ac4d41221c99eb8ce767e63e09a6de6a11.tar.lz dexon-sol-tools-cd5f00ac4d41221c99eb8ce767e63e09a6de6a11.tar.xz dexon-sol-tools-cd5f00ac4d41221c99eb8ce767e63e09a6de6a11.tar.zst dexon-sol-tools-cd5f00ac4d41221c99eb8ce767e63e09a6de6a11.zip |
Merge branch 'development' into breakUp0xjs
* development: (38 commits)
Add fallback image support to relayer grid tile
Clear relayer grid state when fetching
Configure the compiler to generate artifacts with deployedBytecode
Implement loading and error state for relayer grid
Fallback image for relayer grid tile
Change relayer grid tile to link on header
Display top tokens from backend
Remove overflowZ property from portal
Suggestions and fix bad merge
Fix typo
Only show untracked tokens
Make wallet scrollable
Add token flow
Update The Ocean logo
Fix artifacts paths
Create an artifacts folder
Introduce a var
Add removeHexPrefix util method
CHeck if ABI exists
Improve the readability of the check for should compile
...
# Conflicts:
# .gitignore
# packages/contracts/test/multi_sig_with_time_lock.ts
# packages/contracts/test/multi_sig_with_time_lock_except_remove_auth_addr.ts
# packages/contracts/util/artifacts.ts
Diffstat (limited to 'packages/website')
-rw-r--r-- | packages/website/public/images/landing/project_logos/the_ocean.png | bin | 4766 -> 6322 bytes | |||
-rw-r--r-- | packages/website/ts/components/legacy_portal/legacy_portal.tsx | 47 | ||||
-rw-r--r-- | packages/website/ts/components/legacy_portal/legacy_portal_menu.tsx | 20 | ||||
-rw-r--r-- | packages/website/ts/components/portal/portal.tsx | 263 | ||||
-rw-r--r-- | packages/website/ts/components/relayer_index/relayer_grid_tile.tsx | 102 | ||||
-rw-r--r-- | packages/website/ts/components/relayer_index/relayer_index.tsx | 70 | ||||
-rw-r--r-- | packages/website/ts/components/relayer_index/relayer_top_tokens.tsx | 16 | ||||
-rw-r--r-- | packages/website/ts/components/top_bar/provider_display.tsx | 36 | ||||
-rw-r--r-- | packages/website/ts/components/top_bar/top_bar.tsx | 68 | ||||
-rw-r--r-- | packages/website/ts/components/wallet/wallet.tsx | 67 | ||||
-rw-r--r-- | packages/website/ts/containers/portal.ts | 88 | ||||
-rw-r--r-- | packages/website/ts/index.tsx | 11 | ||||
-rw-r--r-- | packages/website/ts/types.ts | 11 |
13 files changed, 596 insertions, 203 deletions
diff --git a/packages/website/public/images/landing/project_logos/the_ocean.png b/packages/website/public/images/landing/project_logos/the_ocean.png Binary files differindex 74678b5d4..0677abc29 100644 --- a/packages/website/public/images/landing/project_logos/the_ocean.png +++ b/packages/website/public/images/landing/project_logos/the_ocean.png diff --git a/packages/website/ts/components/legacy_portal/legacy_portal.tsx b/packages/website/ts/components/legacy_portal/legacy_portal.tsx index 8942e4356..c45b20365 100644 --- a/packages/website/ts/components/legacy_portal/legacy_portal.tsx +++ b/packages/website/ts/components/legacy_portal/legacy_portal.tsx @@ -154,7 +154,6 @@ export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPorta const updateShouldBlockchainErrDialogBeOpen = this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen.bind( this.props.dispatcher, ); - const isDevelopment = configs.ENVIRONMENT === Environments.DEVELOPMENT; const portalStyle: React.CSSProperties = { minHeight: '100vh', display: 'flex', @@ -204,18 +203,6 @@ export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPorta <div className="py2" style={{ backgroundColor: colors.grey50 }}> {this.props.blockchainIsLoaded ? ( <Switch> - {isDevelopment && ( - <Route - path={`${WebsitePaths.Portal}/wallet`} - render={this._renderWallet.bind(this)} - /> - )} - {isDevelopment && ( - <Route - path={`${WebsitePaths.Portal}/relayers`} - render={this._renderRelayers.bind(this)} - /> - )} <Route path={`${WebsitePaths.Portal}/weth`} render={this._renderEthWrapper.bind(this)} @@ -294,40 +281,6 @@ export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPorta 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} - injectedProviderName={this.props.injectedProviderName} - providerType={this.props.providerType} - onToggleLedgerDialog={this.onToggleLedgerDialog.bind(this)} - /> - </div> - </div> - ); - } - private _renderRelayers() { - return ( - <div className="flex flex-center"> - <div className="mx-auto" style={{ width: 800 }}> - <RelayerIndex networkId={this.props.networkId} /> - </div> - </div> - ); - } private _renderEthWrapper() { return ( <EthWrappers diff --git a/packages/website/ts/components/legacy_portal/legacy_portal_menu.tsx b/packages/website/ts/components/legacy_portal/legacy_portal_menu.tsx index 634d966ed..94113f066 100644 --- a/packages/website/ts/components/legacy_portal/legacy_portal_menu.tsx +++ b/packages/website/ts/components/legacy_portal/legacy_portal_menu.tsx @@ -58,26 +58,6 @@ export class LegacyPortalMenu extends React.Component<LegacyPortalMenuProps, Leg > {this._renderMenuItemWithIcon('Wrap ETH', 'zmdi-circle-o')} </MenuItem> - {configs.ENVIRONMENT === Environments.DEVELOPMENT && ( - <div> - <MenuItem - style={this.props.menuItemStyle} - className="py2" - to={`${WebsitePaths.Portal}/wallet`} - onClick={this.props.onClick.bind(this)} - > - {this._renderMenuItemWithIcon('Wallet', 'zmdi-balance-wallet')} - </MenuItem> - <MenuItem - style={this.props.menuItemStyle} - className="py2" - to={`${WebsitePaths.Portal}/relayers`} - onClick={this.props.onClick.bind(this)} - > - {this._renderMenuItemWithIcon('Relayers', 'zmdi-input-antenna')} - </MenuItem> - </div> - )} </div> ); } diff --git a/packages/website/ts/components/portal/portal.tsx b/packages/website/ts/components/portal/portal.tsx new file mode 100644 index 000000000..b5e8150c4 --- /dev/null +++ b/packages/website/ts/components/portal/portal.tsx @@ -0,0 +1,263 @@ +import { colors, Styles } from '@0xproject/react-shared'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; +import * as React from 'react'; +import * as DocumentTitle from 'react-document-title'; + +import { Blockchain } from 'ts/blockchain'; +import { BlockchainErrDialog } from 'ts/components/dialogs/blockchain_err_dialog'; +import { LedgerConfigDialog } from 'ts/components/dialogs/ledger_config_dialog'; +import { PortalDisclaimerDialog } from 'ts/components/dialogs/portal_disclaimer_dialog'; +import { AssetPicker } from 'ts/components/generate_order/asset_picker'; +import { RelayerIndex } from 'ts/components/relayer_index/relayer_index'; +import { TopBar, TopBarDisplayType } from 'ts/components/top_bar/top_bar'; +import { FlashMessage } from 'ts/components/ui/flash_message'; +import { Wallet } from 'ts/components/wallet/wallet'; +import { localStorage } from 'ts/local_storage/local_storage'; +import { Dispatcher } from 'ts/redux/dispatcher'; +import { BlockchainErrs, HashData, Order, ProviderType, ScreenWidths, TokenByAddress, TokenVisibility } from 'ts/types'; +import { constants } from 'ts/utils/constants'; +import { Translate } from 'ts/utils/translate'; +import { utils } from 'ts/utils/utils'; + +export interface PortalProps { + blockchainErr: BlockchainErrs; + blockchainIsLoaded: boolean; + dispatcher: Dispatcher; + hashData: HashData; + injectedProviderName: string; + networkId: number; + nodeVersion: string; + orderFillAmount: BigNumber; + providerType: ProviderType; + screenWidth: ScreenWidths; + tokenByAddress: TokenByAddress; + userEtherBalanceInWei: BigNumber; + userAddress: string; + shouldBlockchainErrDialogBeOpen: boolean; + userSuppliedOrderCache: Order; + location: Location; + flashMessage?: string | React.ReactNode; + lastForceTokenStateRefetch: number; + translate: Translate; +} + +interface PortalState { + prevNetworkId: number; + prevNodeVersion: string; + prevUserAddress: string; + prevPathname: string; + isDisclaimerDialogOpen: boolean; + isLedgerDialogOpen: boolean; + isAssetPickerDialogOpen: boolean; +} + +const THROTTLE_TIMEOUT = 100; +const TOP_BAR_HEIGHT = TopBar.heightForDisplayType(TopBarDisplayType.Expanded); + +const styles: Styles = { + root: { + width: '100%', + height: '100%', + backgroundColor: colors.lightestGrey, + }, + body: { + height: `calc(100vh - ${TOP_BAR_HEIGHT}px)`, + }, + scrollContainer: { + height: `calc(100vh - ${TOP_BAR_HEIGHT}px)`, + WebkitOverflowScrolling: 'touch', + overflow: 'auto', + }, + title: { + fontWeight: 'bold', + fontSize: 20, + }, +}; + +export class Portal extends React.Component<PortalProps, PortalState> { + private _blockchain: Blockchain; + private _throttledScreenWidthUpdate: () => void; + constructor(props: PortalProps) { + super(props); + this._throttledScreenWidthUpdate = _.throttle(this._updateScreenWidth.bind(this), THROTTLE_TIMEOUT); + const didAcceptPortalDisclaimer = localStorage.getItemIfExists(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER); + const hasAcceptedDisclaimer = + !_.isUndefined(didAcceptPortalDisclaimer) && !_.isEmpty(didAcceptPortalDisclaimer); + this.state = { + prevNetworkId: this.props.networkId, + prevNodeVersion: this.props.nodeVersion, + prevUserAddress: this.props.userAddress, + prevPathname: this.props.location.pathname, + isDisclaimerDialogOpen: !hasAcceptedDisclaimer, + isAssetPickerDialogOpen: false, + isLedgerDialogOpen: false, + }; + } + public componentDidMount() { + window.addEventListener('resize', this._throttledScreenWidthUpdate); + window.scrollTo(0, 0); + } + public componentWillMount() { + this._blockchain = new Blockchain(this.props.dispatcher); + } + public componentWillUnmount() { + this._blockchain.destroy(); + window.removeEventListener('resize', this._throttledScreenWidthUpdate); + // We re-set the entire redux state when the portal is unmounted so that when it is re-rendered + // the initialization process always occurs from the same base state. This helps avoid + // initialization inconsistencies (i.e While the portal was unrendered, the user might have + // become disconnected from their backing Ethereum node, changed user accounts, etc...) + this.props.dispatcher.resetState(); + } + public componentWillReceiveProps(nextProps: PortalProps) { + if (nextProps.networkId !== this.state.prevNetworkId) { + // tslint:disable-next-line:no-floating-promises + this._blockchain.networkIdUpdatedFireAndForgetAsync(nextProps.networkId); + this.setState({ + prevNetworkId: nextProps.networkId, + }); + } + if (nextProps.userAddress !== this.state.prevUserAddress) { + const newUserAddress = _.isEmpty(nextProps.userAddress) ? undefined : nextProps.userAddress; + // tslint:disable-next-line:no-floating-promises + this._blockchain.userAddressUpdatedFireAndForgetAsync(newUserAddress); + this.setState({ + prevUserAddress: nextProps.userAddress, + }); + } + if (nextProps.nodeVersion !== this.state.prevNodeVersion) { + // tslint:disable-next-line:no-floating-promises + this._blockchain.nodeVersionUpdatedFireAndForgetAsync(nextProps.nodeVersion); + } + if (nextProps.location.pathname !== this.state.prevPathname) { + this.setState({ + prevPathname: nextProps.location.pathname, + }); + } + } + public render() { + const updateShouldBlockchainErrDialogBeOpen = this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen.bind( + this.props.dispatcher, + ); + const allTokens = _.values(this.props.tokenByAddress); + const trackedTokens = _.filter(allTokens, t => t.isTracked); + return ( + <div style={styles.root}> + <DocumentTitle title="0x Portal DApp" /> + <TopBar + userAddress={this.props.userAddress} + networkId={this.props.networkId} + injectedProviderName={this.props.injectedProviderName} + onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)} + dispatcher={this.props.dispatcher} + providerType={this.props.providerType} + blockchainIsLoaded={this.props.blockchainIsLoaded} + location={this.props.location} + blockchain={this._blockchain} + translate={this.props.translate} + displayType={TopBarDisplayType.Expanded} + style={{ backgroundColor: colors.lightestGrey }} + /> + <div id="portal" style={styles.body}> + <div className="sm-flex flex-center"> + <div className="flex-last px3"> + <div className="py3" style={styles.title}> + Your Account + </div> + <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} + injectedProviderName={this.props.injectedProviderName} + providerType={this.props.providerType} + onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)} + onAddToken={this._onAddToken.bind(this)} + /> + </div> + <div className="flex-auto px3" style={styles.scrollContainer}> + <div className="py3" style={styles.title}> + Explore 0x Ecosystem + </div> + <RelayerIndex networkId={this.props.networkId} /> + </div> + </div> + <BlockchainErrDialog + blockchain={this._blockchain} + blockchainErr={this.props.blockchainErr} + isOpen={this.props.shouldBlockchainErrDialogBeOpen} + userAddress={this.props.userAddress} + toggleDialogFn={updateShouldBlockchainErrDialogBeOpen} + networkId={this.props.networkId} + /> + <PortalDisclaimerDialog + isOpen={this.state.isDisclaimerDialogOpen} + onToggleDialog={this._onPortalDisclaimerAccepted.bind(this)} + /> + <FlashMessage dispatcher={this.props.dispatcher} flashMessage={this.props.flashMessage} /> + {this.props.blockchainIsLoaded && ( + <LedgerConfigDialog + providerType={this.props.providerType} + networkId={this.props.networkId} + blockchain={this._blockchain} + dispatcher={this.props.dispatcher} + toggleDialogFn={this._onToggleLedgerDialog.bind(this)} + isOpen={this.state.isLedgerDialogOpen} + /> + )} + <AssetPicker + userAddress={this.props.userAddress} + networkId={this.props.networkId} + blockchain={this._blockchain} + dispatcher={this.props.dispatcher} + isOpen={this.state.isAssetPickerDialogOpen} + currentTokenAddress={''} + onTokenChosen={this._onTokenChosen.bind(this)} + tokenByAddress={this.props.tokenByAddress} + tokenVisibility={TokenVisibility.UNTRACKED} + /> + </div> + </div> + ); + } + private _onTokenChosen(tokenAddress: string) { + if (_.isEmpty(tokenAddress)) { + this.setState({ + isAssetPickerDialogOpen: false, + }); + return; + } + const token = this.props.tokenByAddress[tokenAddress]; + this.props.dispatcher.updateTokenByAddress([token]); + this.setState({ + isAssetPickerDialogOpen: false, + }); + } + private _onToggleLedgerDialog() { + this.setState({ + isLedgerDialogOpen: !this.state.isLedgerDialogOpen, + }); + } + private _onAddToken() { + this.setState({ + isAssetPickerDialogOpen: !this.state.isAssetPickerDialogOpen, + }); + } + private _onPortalDisclaimerAccepted() { + localStorage.setItem(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER, 'set'); + this.setState({ + isDisclaimerDialogOpen: false, + }); + } + private _updateScreenWidth() { + const newScreenWidth = utils.getScreenWidth(); + this.props.dispatcher.updateScreenWidth(newScreenWidth); + } +} diff --git a/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx b/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx index 0b9b8165e..d88a59d15 100644 --- a/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx +++ b/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx @@ -12,37 +12,6 @@ export interface RelayerGridTileProps { networkId: number; } -// TODO: Get top tokens from remote -const topTokens = [ - { - address: '0x1dad4783cf3fe3085c1426157ab175a6119a04ba', - decimals: 18, - iconUrl: '/images/token_icons/makerdao.png', - isRegistered: true, - isTracked: true, - name: 'Maker DAO', - symbol: 'MKR', - }, - { - address: '0x323b5d4c32345ced77393b3530b1eed0f346429d', - decimals: 18, - iconUrl: '/images/token_icons/melon.png', - isRegistered: true, - isTracked: true, - name: 'Melon Token', - symbol: 'MLN', - }, - { - address: '0xb18845c260f680d5b9d84649638813e342e4f8c9', - decimals: 18, - iconUrl: '/images/token_icons/augur.png', - isRegistered: true, - isTracked: true, - name: 'Augur Reputation Token', - symbol: 'REP', - }, -]; - const styles: Styles = { root: { backgroundColor: colors.white, @@ -93,28 +62,63 @@ const styles: Styles = { }, }; +const FALLBACK_IMG_SRC = '/images/landing/hero_chip_image.png'; + export const RelayerGridTile: React.StatelessComponent<RelayerGridTileProps> = (props: RelayerGridTileProps) => { const link = props.relayerInfo.appUrl || props.relayerInfo.url; return ( - <a href={link} target="_blank" style={{ textDecoration: 'none' }}> - <GridTile style={styles.root}> - <div style={styles.innerDiv}> - <img src={props.relayerInfo.headerImgUrl} style={styles.header} /> - <div style={styles.body}> - <div className="py1" style={styles.relayerNameLabel}> - {props.relayerInfo.name} - </div> - <div style={styles.dailyTradeVolumeLabel}>{props.relayerInfo.dailyTxnVolume}</div> - <div className="py1" style={styles.subLabel}> - Daily Trade Volume - </div> - <TopTokens tokens={topTokens} networkId={props.networkId} /> - <div className="py1" style={styles.subLabel}> - Top tokens - </div> + <GridTile style={styles.root}> + <div style={styles.innerDiv}> + <a href={link} target="_blank" style={{ textDecoration: 'none' }}> + <ImgWithFallback + src={props.relayerInfo.headerImgUrl} + fallbackSrc={FALLBACK_IMG_SRC} + style={styles.header} + /> + </a> + <div style={styles.body}> + <div className="py1" style={styles.relayerNameLabel}> + {props.relayerInfo.name} + </div> + <div style={styles.dailyTradeVolumeLabel}>{props.relayerInfo.dailyTxnVolume}</div> + <div className="py1" style={styles.subLabel}> + Daily Trade Volume + </div> + <TopTokens tokens={props.relayerInfo.topTokens} networkId={props.networkId} /> + <div className="py1" style={styles.subLabel}> + Top tokens </div> </div> - </GridTile> - </a> + </div> + </GridTile> ); }; + +interface ImgWithFallbackProps { + src?: string; + fallbackSrc: string; + style: React.CSSProperties; +} +interface ImgWithFallbackState { + imageLoadFailed: boolean; +} +class ImgWithFallback extends React.Component<ImgWithFallbackProps, ImgWithFallbackState> { + constructor(props: ImgWithFallbackProps) { + super(props); + this.state = { + imageLoadFailed: false, + }; + } + public render() { + if (this.state.imageLoadFailed || _.isUndefined(this.props.src)) { + return <img src={this.props.fallbackSrc} style={this.props.style} />; + } else { + return <img src={this.props.src} onError={this._onError.bind(this)} style={this.props.style} />; + } + } + private _onError() { + this.setState({ + imageLoadFailed: true, + }); + } +} diff --git a/packages/website/ts/components/relayer_index/relayer_index.tsx b/packages/website/ts/components/relayer_index/relayer_index.tsx index c1ab4227a..dffd0f83f 100644 --- a/packages/website/ts/components/relayer_index/relayer_index.tsx +++ b/packages/website/ts/components/relayer_index/relayer_index.tsx @@ -1,5 +1,7 @@ import { colors, Styles } from '@0xproject/react-shared'; import * as _ from 'lodash'; +import CircularProgress from 'material-ui/CircularProgress'; +import FlatButton from 'material-ui/FlatButton'; import { GridList } from 'material-ui/GridList'; import * as React from 'react'; @@ -32,9 +34,9 @@ const styles: Styles = { }, }; -const CELL_HEIGHT = 260; +const CELL_HEIGHT = 290; const NUMBER_OF_COLUMNS = 4; -const GRID_PADDING = 16; +const GRID_PADDING = 20; export class RelayerIndex extends React.Component<RelayerIndexProps, RelayerIndexState> { private _isUnmounted: boolean; @@ -55,7 +57,24 @@ export class RelayerIndex extends React.Component<RelayerIndexProps, RelayerInde } public render() { const readyToRender = _.isUndefined(this.state.error) && !_.isUndefined(this.state.relayerInfos); - if (readyToRender) { + if (!readyToRender) { + return ( + <div className="col col-12" style={{ ...styles.root, height: '100%' }}> + <div + className="relative sm-px2 sm-pt2 sm-m1" + style={{ height: 122, top: '33%', transform: 'translateY(-50%)' }} + > + <div className="center pb2"> + {_.isUndefined(this.state.error) ? ( + <CircularProgress size={40} thickness={5} /> + ) : ( + <Retry onRetry={this._fetchRelayerInfosAsync.bind(this)} /> + )} + </div> + </div> + </div> + ); + } else { return ( <div style={styles.root}> <GridList @@ -64,23 +83,22 @@ export class RelayerIndex extends React.Component<RelayerIndexProps, RelayerInde padding={GRID_PADDING} style={styles.gridList} > - {this.state.relayerInfos.map((relayerInfo: WebsiteBackendRelayerInfo) => ( - <RelayerGridTile - key={relayerInfo.name} - relayerInfo={relayerInfo} - networkId={this.props.networkId} - /> + {this.state.relayerInfos.map((relayerInfo: WebsiteBackendRelayerInfo, index) => ( + <RelayerGridTile key={index} relayerInfo={relayerInfo} networkId={this.props.networkId} /> ))} </GridList> </div> ); - } else { - // TODO: loading and error states with a scrolling container - return null; } } private async _fetchRelayerInfosAsync(): Promise<void> { try { + if (!this._isUnmounted) { + this.setState({ + relayerInfos: undefined, + error: undefined, + }); + } const relayerInfos = await backendClient.getRelayerInfosAsync(); if (!this._isUnmounted) { this.setState({ @@ -96,3 +114,31 @@ export class RelayerIndex extends React.Component<RelayerIndexProps, RelayerInde } } } + +interface RetryProps { + onRetry: () => void; +} +const Retry = (props: RetryProps) => ( + <div className="clearfix center" style={{ color: colors.black }}> + <div className="mx-auto inline-block align-middle" style={{ lineHeight: '44px', textAlign: 'center' }}> + <div className="h2" style={{ fontFamily: 'Roboto Mono' }}> + Something went wrong. + </div> + <div className="py3"> + <FlatButton + label={'reload'} + backgroundColor={colors.black} + labelStyle={{ + fontSize: 18, + fontFamily: 'Roboto Mono', + fontWeight: 'lighter', + color: colors.white, + textTransform: 'lowercase', + }} + style={{ width: 280, height: 62, borderRadius: 5 }} + onClick={props.onRetry} + /> + </div> + </div> + </div> +); diff --git a/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx b/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx index 233590b78..db4d3a211 100644 --- a/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx +++ b/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx @@ -3,10 +3,10 @@ import * as _ from 'lodash'; import * as React from 'react'; import { TokenIcon } from 'ts/components/ui/token_icon'; -import { Token } from 'ts/types'; +import { WebsiteBackendTokenInfo } from 'ts/types'; export interface TopTokensProps { - tokens: Token[]; + tokens: WebsiteBackendTokenInfo[]; networkId: number; } @@ -23,17 +23,17 @@ const styles: Styles = { export const TopTokens: React.StatelessComponent<TopTokensProps> = (props: TopTokensProps) => { return ( <div className="flex"> - {_.map(props.tokens, (token: Token, index: number) => { + {_.map(props.tokens, (tokenInfo: WebsiteBackendTokenInfo, index: number) => { const firstItemStyle = { ...styles.tokenLabel, ...styles.followingTokenLabel }; const style = index !== 0 ? firstItemStyle : styles.tokenLabel; return ( <a - key={token.address} - href={tokenLinkFromToken(token, props.networkId)} + key={tokenInfo.address} + href={tokenLinkFromToken(tokenInfo, props.networkId)} target="_blank" style={style} > - {token.symbol} + {tokenInfo.symbol} </a> ); })} @@ -41,6 +41,6 @@ export const TopTokens: React.StatelessComponent<TopTokensProps> = (props: TopTo ); }; -function tokenLinkFromToken(token: Token, networkId: number) { - return sharedUtils.getEtherScanLinkIfExists(token.address, networkId, EtherscanLinkSuffixes.Address); +function tokenLinkFromToken(tokenInfo: WebsiteBackendTokenInfo, networkId: number) { + return sharedUtils.getEtherScanLinkIfExists(tokenInfo.address, networkId, EtherscanLinkSuffixes.Address); } diff --git a/packages/website/ts/components/top_bar/provider_display.tsx b/packages/website/ts/components/top_bar/provider_display.tsx index 221c34f8c..4cd50dba9 100644 --- a/packages/website/ts/components/top_bar/provider_display.tsx +++ b/packages/website/ts/components/top_bar/provider_display.tsx @@ -1,4 +1,4 @@ -import { colors } from '@0xproject/react-shared'; +import { colors, Styles } from '@0xproject/react-shared'; import * as _ from 'lodash'; import RaisedButton from 'material-ui/RaisedButton'; import * as React from 'react'; @@ -11,9 +11,9 @@ import { ProviderType } from 'ts/types'; import { constants } from 'ts/utils/constants'; import { utils } from 'ts/utils/utils'; -const IDENTICON_DIAMETER = 32; +const ROOT_HEIGHT = 24; -interface ProviderDisplayProps { +export interface ProviderDisplayProps { dispatcher: Dispatcher; userAddress: string; networkId: number; @@ -25,6 +25,15 @@ interface ProviderDisplayProps { interface ProviderDisplayState {} +const styles: Styles = { + root: { + height: ROOT_HEIGHT, + backgroundColor: colors.white, + borderRadius: ROOT_HEIGHT, + boxShadow: `0px 4px 6px ${colors.walletBoxShadow}`, + }, +}; + export class ProviderDisplay extends React.Component<ProviderDisplayProps, ProviderDisplayState> { public render() { const isAddressAvailable = !_.isEmpty(this.props.userAddress); @@ -40,21 +49,20 @@ export class ProviderDisplay extends React.Component<ProviderDisplayProps, Provi : 'Connect a wallet'; const providerTitle = this.props.providerType === ProviderType.Injected ? injectedProviderName : 'Ledger Nano S'; + const isProviderMetamask = providerTitle === constants.PROVIDER_NAME_METAMASK; const hoverActiveNode = ( - <div className="flex right lg-pr0 md-pr2 sm-pr2" style={{ paddingTop: 16 }}> + <div className="flex right lg-pr0 md-pr2 sm-pr2 p1" style={styles.root}> <div> - <Identicon address={this.props.userAddress} diameter={IDENTICON_DIAMETER} /> - </div> - <div style={{ marginLeft: 12, paddingTop: 1 }}> - <div style={{ fontSize: 12, color: colors.amber800 }}>{providerTitle}</div> - <div style={{ fontSize: 14 }}>{displayAddress}</div> + <Identicon address={this.props.userAddress} diameter={ROOT_HEIGHT} /> </div> - <div - style={{ borderLeft: `1px solid ${colors.grey300}`, marginLeft: 17, paddingTop: 1 }} - className="px2" - > - <i style={{ fontSize: 30, color: colors.grey300 }} className="zmdi zmdi zmdi-chevron-down" /> + <div style={{ marginLeft: 12, paddingTop: 3 }}> + <div style={{ fontSize: 16, color: colors.darkGrey }}>{displayAddress}</div> </div> + {isProviderMetamask && ( + <div style={{ marginLeft: 16 }}> + <img src="/images/metamask_icon.png" style={{ width: ROOT_HEIGHT, height: ROOT_HEIGHT }} /> + </div> + )} </div> ); const hasInjectedProvider = diff --git a/packages/website/ts/components/top_bar/top_bar.tsx b/packages/website/ts/components/top_bar/top_bar.tsx index 7fb476ba0..2502bea6d 100644 --- a/packages/website/ts/components/top_bar/top_bar.tsx +++ b/packages/website/ts/components/top_bar/top_bar.tsx @@ -19,7 +19,12 @@ import { Deco, Key, ProviderType, WebsiteLegacyPaths, WebsitePaths } from 'ts/ty import { constants } from 'ts/utils/constants'; import { Translate } from 'ts/utils/translate'; -interface TopBarProps { +export enum TopBarDisplayType { + Default, + Expanded, +} + +export interface TopBarProps { userAddress?: string; networkId?: number; injectedProviderName?: string; @@ -34,7 +39,7 @@ interface TopBarProps { availableDocVersions?: string[]; menu?: DocsMenu; menuSubsectionsBySection?: MenuSubsectionsBySection; - shouldFullWidth?: boolean; + displayType?: TopBarDisplayType; docsInfo?: DocsInfo; style?: React.CSSProperties; isNightVersion?: boolean; @@ -47,17 +52,8 @@ interface TopBarState { } const styles: Styles = { - address: { - marginRight: 12, - overflow: 'hidden', - paddingTop: 4, - textOverflow: 'ellipsis', - whiteSpace: 'nowrap', - width: 70, - }, topBar: { backgroundColor: colors.white, - height: 59, width: '100%', position: 'relative', top: 0, @@ -78,12 +74,19 @@ const styles: Styles = { }, }; +const DEFAULT_HEIGHT = 59; +const EXPANDED_HEIGHT = 75; + export class TopBar extends React.Component<TopBarProps, TopBarState> { public static defaultProps: Partial<TopBarProps> = { - shouldFullWidth: false, + displayType: TopBarDisplayType.Default, style: {}, isNightVersion: false, }; + public static heightForDisplayType(displayType: TopBarDisplayType) { + const result = displayType === TopBarDisplayType.Expanded ? EXPANDED_HEIGHT : DEFAULT_HEIGHT; + return result + 1; + } constructor(props: TopBarProps) { super(props); this.state = { @@ -92,8 +95,9 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { } public render() { const isNightVersion = this.props.isNightVersion; - const isFullWidthPage = this.props.shouldFullWidth; - const parentClassNames = `flex mx-auto ${isFullWidthPage ? 'pl2' : 'max-width-4'}`; + const isExpandedDisplayType = this.props.displayType === TopBarDisplayType.Expanded; + const parentClassNames = `flex mx-auto ${isExpandedDisplayType ? 'pl3 py1' : 'max-width-4'}`; + const height = isExpandedDisplayType ? EXPANDED_HEIGHT : DEFAULT_HEIGHT; const developerSectionMenuItems = [ <Link key="subMenuItem-zeroEx" to={WebsitePaths.ZeroExJs} className="text-decoration-none"> <MenuItem style={{ fontSize: styles.menuItem.fontSize }} primaryText="0x.js" /> @@ -178,9 +182,11 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { </a>, ]; const bottomBorderStyle = this._shouldDisplayBottomBar() ? styles.bottomBar : {}; - const fullWidthClasses = isFullWidthPage ? 'pr4' : ''; + const fullWidthClasses = isExpandedDisplayType ? 'pr4' : ''; const logoUrl = isNightVersion ? '/images/protocol_logo_white.png' : '/images/protocol_logo_black.png'; - const menuClasses = `col col-${isFullWidthPage ? '4' : '5'} ${fullWidthClasses} lg-pr0 md-pr2 sm-hide xs-hide`; + const menuClasses = `col col-${ + isExpandedDisplayType ? '4' : '5' + } ${fullWidthClasses} lg-pr0 md-pr2 sm-hide xs-hide`; const menuIconStyle = { fontSize: 25, color: isNightVersion ? 'white' : 'black', @@ -197,15 +203,15 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { ); const popoverContent = <Menu style={{ color: colors.darkGrey }}>{developerSectionMenuItems}</Menu>; return ( - <div style={{ ...styles.topBar, ...bottomBorderStyle, ...this.props.style }} className="pb1"> + <div style={{ ...styles.topBar, ...bottomBorderStyle, ...this.props.style, ...{ height } }} className="pb1"> <div className={parentClassNames}> <div className="col col-2 sm-pl1 md-pl2 lg-pl0" style={{ paddingTop: 15 }}> <Link to={`${WebsitePaths.Home}`} className="text-decoration-none"> <img src={logoUrl} height="30" /> </Link> </div> - <div className={`col col-${isFullWidthPage ? '8' : '9'} lg-hide md-hide`} /> - <div className={`col col-${isFullWidthPage ? '6' : '5'} sm-hide xs-hide`} /> + <div className={`col col-${isExpandedDisplayType ? '8' : '9'} lg-hide md-hide`} /> + <div className={`col col-${isExpandedDisplayType ? '6' : '5'} sm-hide xs-hide`} /> {!this._isViewingPortal() && ( <div className={menuClasses}> <div className="flex justify-between"> @@ -242,7 +248,7 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { path={`${WebsitePaths.Portal}`} isPrimary={true} style={styles.menuItem} - className={`${isFullWidthPage && 'md-hide'}`} + className={`${isExpandedDisplayType && 'md-hide'}`} isNightVersion={isNightVersion} isExternal={false} /> @@ -250,7 +256,7 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { </div> )} {this.props.blockchainIsLoaded && ( - <div className="sm-hide xs-hide col col-5"> + <div className="sm-hide xs-hide col col-5" style={{ paddingTop: 8, marginRight: 36 }}> <ProviderDisplay dispatcher={this.props.dispatcher} userAddress={this.props.userAddress} @@ -262,7 +268,7 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { /> </div> )} - <div className={`col ${isFullWidthPage ? 'col-2 pl2' : 'col-1'} md-hide lg-hide`}> + <div className={`col ${isExpandedDisplayType ? 'col-2 pl2' : 'col-1'} md-hide lg-hide`}> <div style={menuIconStyle}> <i className="zmdi zmdi-menu" onClick={this._onMenuButtonClick.bind(this)} /> </div> @@ -441,21 +447,6 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { </div> ); } - private _renderUser() { - const userAddress = this.props.userAddress; - const identiconDiameter = 26; - return ( - <div className="flex right lg-pr0 md-pr2 sm-pr2" style={{ paddingTop: 16 }}> - <div style={styles.address} data-tip={true} data-for="userAddressTooltip"> - {!_.isEmpty(userAddress) ? userAddress : ''} - </div> - <ReactTooltip id="userAddressTooltip">{userAddress}</ReactTooltip> - <div> - <Identicon address={userAddress} diameter={identiconDiameter} /> - </div> - </div> - ); - } private _onMenuButtonClick() { this.setState({ isDrawerOpen: !this.state.isDrawerOpen, @@ -511,7 +502,8 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { this._isViewingJsonSchemasDocs() || this._isViewingSolCovDocs() || this._isViewingSubprovidersDocs() || - this._isViewingConnectDocs() + this._isViewingConnectDocs() || + this._isViewingPortal() ); } } // tslint:disable:max-file-line-count diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx index 057c712e5..a28012aaf 100644 --- a/packages/website/ts/components/wallet/wallet.tsx +++ b/packages/website/ts/components/wallet/wallet.tsx @@ -55,11 +55,13 @@ export interface WalletProps { injectedProviderName: string; providerType: ProviderType; onToggleLedgerDialog: () => void; + onAddToken: () => void; } interface WalletState { trackedTokenStateByAddress: TokenStateByAddress; wrappedEtherDirection?: Side; + isHoveringSidebar: boolean; } interface AllowanceToggleConfig { @@ -94,6 +96,9 @@ const styles: Styles = { }, footerItemInnerDiv: { paddingLeft: 24, + borderTopColor: colors.walletBorder, + borderTopStyle: 'solid', + borderWidth: 1, }, borderedItem: { borderBottomColor: colors.walletBorder, @@ -114,7 +119,17 @@ const styles: Styles = { paddingTop: 8, paddingBottom: 8, }, - accessoryItemsContainer: { width: 150, right: 8 }, + accessoryItemsContainer: { + width: 150, + right: 8, + }, + bodyInnerDiv: { + padding: 0, + // TODO: make this completely responsive + maxHeight: 475, + overflow: 'auto', + WebkitOverflowScrolling: 'touch', + }, }; const ETHER_ICON_PATH = '/images/ether.png'; @@ -139,6 +154,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { this.state = { trackedTokenStateByAddress: initialTrackedTokenStateByAddress, wrappedEtherDirection: undefined, + isHoveringSidebar: false, }; } public componentWillMount() { @@ -184,12 +200,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { return ( <List style={styles.list}> {isAddressAvailable - ? _.concat( - this._renderConnectedHeaderRows(), - this._renderEthRows(), - this._renderTokenRows(), - this._renderFooterRows(), - ) + ? _.concat(this._renderConnectedHeaderRows(), this._renderBody(), this._renderFooterRows()) : _.concat(this._renderDisconnectedHeaderRows(), this._renderDisconnectedRows())} </List> ); @@ -230,9 +241,43 @@ export class Wallet extends React.Component<WalletProps, WalletState> { /> ); } + private _renderBody() { + const bodyStyle: React.CSSProperties = { + ...styles.bodyInnerDiv, + overflow: this.state.isHoveringSidebar ? 'auto' : 'hidden', + }; + return ( + <ListItem + key="body" + innerDivStyle={bodyStyle} + onMouseEnter={this._onSidebarHover.bind(this)} + onMouseLeave={this._onSidebarHoverOff.bind(this)} + > + {this._renderEthRows()} + {this._renderTokenRows()} + </ListItem> + ); + } + private _onSidebarHover(event: React.FormEvent<HTMLInputElement>) { + this.setState({ + isHoveringSidebar: true, + }); + } + private _onSidebarHoverOff() { + this.setState({ + isHoveringSidebar: false, + }); + } private _renderFooterRows() { const primaryText = '+ other tokens'; - return <ListItem key={FOOTER_ITEM_KEY} primaryText={primaryText} innerDivStyle={styles.footerItemInnerDiv} />; + return ( + <ListItem + key={FOOTER_ITEM_KEY} + primaryText={primaryText} + innerDivStyle={styles.footerItemInnerDiv} + onClick={this.props.onAddToken} + /> + ); } private _renderEthRows() { const primaryText = this._renderAmount( @@ -293,7 +338,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { ); return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this)); } - private _renderTokenRow(token: Token) { + private _renderTokenRow(token: Token, index: number) { const tokenState = this.state.trackedTokenStateByAddress[token.address]; const tokenLink = sharedUtils.getEtherScanLinkIfExists( token.address, @@ -310,12 +355,14 @@ export class Wallet extends React.Component<WalletProps, WalletState> { tokenState, }, }; + // if this is the last item in the list, do not render the border, it is rendered by the footer + const borderedStyle = index !== this.props.trackedTokens.length - 1 ? styles.borderedItem : {}; const shouldShowWrapEtherItem = !_.isUndefined(this.state.wrappedEtherDirection) && this.state.wrappedEtherDirection === accessoryItemConfig.wrappedEtherDirection; const style = shouldShowWrapEtherItem ? { ...walletItemStyles.focusedItem, ...styles.paddedItem } - : { ...styles.tokenItem, ...styles.borderedItem, ...styles.paddedItem }; + : { ...styles.tokenItem, ...borderedStyle, ...styles.paddedItem }; const etherToken = this._getEthToken(); return ( <div key={token.address}> diff --git a/packages/website/ts/containers/portal.ts b/packages/website/ts/containers/portal.ts new file mode 100644 index 000000000..3f0feb6e9 --- /dev/null +++ b/packages/website/ts/containers/portal.ts @@ -0,0 +1,88 @@ +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; +import { Portal as PortalComponent, PortalProps as PortalComponentProps } from 'ts/components/portal/portal'; +import { Dispatcher } from 'ts/redux/dispatcher'; +import { State } from 'ts/redux/reducer'; +import { BlockchainErrs, HashData, Order, ProviderType, ScreenWidths, Side, TokenByAddress } from 'ts/types'; +import { constants } from 'ts/utils/constants'; +import { Translate } from 'ts/utils/translate'; + +interface ConnectedState { + blockchainErr: BlockchainErrs; + blockchainIsLoaded: boolean; + hashData: HashData; + injectedProviderName: string; + networkId: number; + nodeVersion: string; + orderFillAmount: BigNumber; + providerType: ProviderType; + tokenByAddress: TokenByAddress; + lastForceTokenStateRefetch: number; + userEtherBalanceInWei: BigNumber; + screenWidth: ScreenWidths; + shouldBlockchainErrDialogBeOpen: boolean; + userAddress: string; + userSuppliedOrderCache: Order; + flashMessage?: string | React.ReactNode; + translate: Translate; +} + +interface ConnectedDispatch { + dispatcher: Dispatcher; +} + +const mapStateToProps = (state: State, ownProps: PortalComponentProps): ConnectedState => { + const receiveAssetToken = state.sideToAssetToken[Side.Receive]; + const depositAssetToken = state.sideToAssetToken[Side.Deposit]; + const receiveAddress = !_.isUndefined(receiveAssetToken.address) + ? receiveAssetToken.address + : constants.NULL_ADDRESS; + const depositAddress = !_.isUndefined(depositAssetToken.address) + ? depositAssetToken.address + : constants.NULL_ADDRESS; + const receiveAmount = !_.isUndefined(receiveAssetToken.amount) ? receiveAssetToken.amount : new BigNumber(0); + const depositAmount = !_.isUndefined(depositAssetToken.amount) ? depositAssetToken.amount : new BigNumber(0); + const hashData = { + depositAmount, + depositTokenContractAddr: depositAddress, + feeRecipientAddress: constants.NULL_ADDRESS, + makerFee: constants.MAKER_FEE, + orderExpiryTimestamp: state.orderExpiryTimestamp, + orderMakerAddress: state.userAddress, + orderTakerAddress: state.orderTakerAddress !== '' ? state.orderTakerAddress : constants.NULL_ADDRESS, + receiveAmount, + receiveTokenContractAddr: receiveAddress, + takerFee: constants.TAKER_FEE, + orderSalt: state.orderSalt, + }; + return { + blockchainErr: state.blockchainErr, + blockchainIsLoaded: state.blockchainIsLoaded, + hashData, + injectedProviderName: state.injectedProviderName, + networkId: state.networkId, + nodeVersion: state.nodeVersion, + orderFillAmount: state.orderFillAmount, + providerType: state.providerType, + screenWidth: state.screenWidth, + shouldBlockchainErrDialogBeOpen: state.shouldBlockchainErrDialogBeOpen, + tokenByAddress: state.tokenByAddress, + lastForceTokenStateRefetch: state.lastForceTokenStateRefetch, + userAddress: state.userAddress, + userEtherBalanceInWei: state.userEtherBalanceInWei, + userSuppliedOrderCache: state.userSuppliedOrderCache, + flashMessage: state.flashMessage, + translate: state.translate, + }; +}; + +const mapDispatchToProps = (dispatch: Dispatch<State>): ConnectedDispatch => ({ + dispatcher: new Dispatcher(dispatch), +}); + +export const Portal: React.ComponentClass<PortalComponentProps> = connect(mapStateToProps, mapDispatchToProps)( + PortalComponent, +); diff --git a/packages/website/ts/index.tsx b/packages/website/ts/index.tsx index 9535dd222..1b1255214 100644 --- a/packages/website/ts/index.tsx +++ b/packages/website/ts/index.tsx @@ -34,9 +34,14 @@ import 'less/all.less'; // cause we only want to import the module when the user navigates to the page. // At the same time webpack statically parses for System.import() to determine bundle chunk split points // so each lazy import needs it's own `System.import()` declaration. -const LazyPortal = createLazyComponent('LegacyPortal', async () => - System.import<any>(/* webpackChunkName: "legacyPortal" */ 'ts/containers/legacy_portal'), -); +const LazyPortal = + utils.isDevelopment() || utils.isStaging() + ? createLazyComponent('Portal', async () => + System.import<any>(/* webpackChunkName: "portal" */ 'ts/containers/portal'), + ) + : createLazyComponent('LegacyPortal', async () => + System.import<any>(/* webpackChunkName: "legacyPortal" */ 'ts/containers/legacy_portal'), + ); const LazyZeroExJSDocumentation = createLazyComponent('Documentation', async () => System.import<any>(/* webpackChunkName: "zeroExDocs" */ 'ts/containers/zero_ex_js_documentation'), ); diff --git a/packages/website/ts/types.ts b/packages/website/ts/types.ts index 2754075a7..67e4a5d7d 100644 --- a/packages/website/ts/types.ts +++ b/packages/website/ts/types.ts @@ -503,19 +503,26 @@ export interface TokenState { price?: BigNumber; } -// TODO: Add topTokens property once available from backend export interface WebsiteBackendRelayerInfo { name: string; dailyTxnVolume: string; url: string; appUrl?: string; - headerImgUrl: string; + headerImgUrl?: string; + topTokens: WebsiteBackendTokenInfo[]; } export interface WebsiteBackendPriceInfo { [symbol: string]: string; } +export interface WebsiteBackendTokenInfo { + address: string; + decimals: number; + name: string; + symbol: string; +} + export interface WebsiteBackendGasInfo { average: number; } |