diff options
Diffstat (limited to 'packages/website/ts')
18 files changed, 866 insertions, 176 deletions
diff --git a/packages/website/ts/artifacts/Exchange.json b/packages/website/ts/artifacts/Exchange.json new file mode 100644 index 000000000..af8db7360 --- /dev/null +++ b/packages/website/ts/artifacts/Exchange.json @@ -0,0 +1,610 @@ +{ + "contract_name": "Exchange", + "abi": [ + { + "constant": true, + "inputs": [ + { + "name": "numerator", + "type": "uint256" + }, + { + "name": "denominator", + "type": "uint256" + }, + { + "name": "target", + "type": "uint256" + } + ], + "name": "isRoundingError", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "name": "filled", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "name": "cancelled", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "orderAddresses", + "type": "address[5][]" + }, + { + "name": "orderValues", + "type": "uint256[6][]" + }, + { + "name": "fillTakerTokenAmount", + "type": "uint256" + }, + { + "name": "shouldThrowOnInsufficientBalanceOrAllowance", + "type": "bool" + }, + { + "name": "v", + "type": "uint8[]" + }, + { + "name": "r", + "type": "bytes32[]" + }, + { + "name": "s", + "type": "bytes32[]" + } + ], + "name": "fillOrdersUpTo", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "orderAddresses", + "type": "address[5]" + }, + { + "name": "orderValues", + "type": "uint256[6]" + }, + { + "name": "cancelTakerTokenAmount", + "type": "uint256" + } + ], + "name": "cancelOrder", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "ZRX_TOKEN_CONTRACT", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "orderAddresses", + "type": "address[5][]" + }, + { + "name": "orderValues", + "type": "uint256[6][]" + }, + { + "name": "fillTakerTokenAmounts", + "type": "uint256[]" + }, + { + "name": "v", + "type": "uint8[]" + }, + { + "name": "r", + "type": "bytes32[]" + }, + { + "name": "s", + "type": "bytes32[]" + } + ], + "name": "batchFillOrKillOrders", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "orderAddresses", + "type": "address[5]" + }, + { + "name": "orderValues", + "type": "uint256[6]" + }, + { + "name": "fillTakerTokenAmount", + "type": "uint256" + }, + { + "name": "v", + "type": "uint8" + }, + { + "name": "r", + "type": "bytes32" + }, + { + "name": "s", + "type": "bytes32" + } + ], + "name": "fillOrKillOrder", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "getUnavailableTakerTokenAmount", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "signer", + "type": "address" + }, + { + "name": "hash", + "type": "bytes32" + }, + { + "name": "v", + "type": "uint8" + }, + { + "name": "r", + "type": "bytes32" + }, + { + "name": "s", + "type": "bytes32" + } + ], + "name": "isValidSignature", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "numerator", + "type": "uint256" + }, + { + "name": "denominator", + "type": "uint256" + }, + { + "name": "target", + "type": "uint256" + } + ], + "name": "getPartialAmount", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "TOKEN_TRANSFER_PROXY_CONTRACT", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "orderAddresses", + "type": "address[5][]" + }, + { + "name": "orderValues", + "type": "uint256[6][]" + }, + { + "name": "fillTakerTokenAmounts", + "type": "uint256[]" + }, + { + "name": "shouldThrowOnInsufficientBalanceOrAllowance", + "type": "bool" + }, + { + "name": "v", + "type": "uint8[]" + }, + { + "name": "r", + "type": "bytes32[]" + }, + { + "name": "s", + "type": "bytes32[]" + } + ], + "name": "batchFillOrders", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "orderAddresses", + "type": "address[5][]" + }, + { + "name": "orderValues", + "type": "uint256[6][]" + }, + { + "name": "cancelTakerTokenAmounts", + "type": "uint256[]" + } + ], + "name": "batchCancelOrders", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "orderAddresses", + "type": "address[5]" + }, + { + "name": "orderValues", + "type": "uint256[6]" + }, + { + "name": "fillTakerTokenAmount", + "type": "uint256" + }, + { + "name": "shouldThrowOnInsufficientBalanceOrAllowance", + "type": "bool" + }, + { + "name": "v", + "type": "uint8" + }, + { + "name": "r", + "type": "bytes32" + }, + { + "name": "s", + "type": "bytes32" + } + ], + "name": "fillOrder", + "outputs": [ + { + "name": "filledTakerTokenAmount", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "orderAddresses", + "type": "address[5]" + }, + { + "name": "orderValues", + "type": "uint256[6]" + } + ], + "name": "getOrderHash", + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "EXTERNAL_QUERY_GAS_LIMIT", + "outputs": [ + { + "name": "", + "type": "uint16" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "VERSION", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "type": "function" + }, + { + "inputs": [ + { + "name": "_zrxToken", + "type": "address" + }, + { + "name": "_tokenTransferProxy", + "type": "address" + } + ], + "payable": false, + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "maker", + "type": "address" + }, + { + "indexed": false, + "name": "taker", + "type": "address" + }, + { + "indexed": true, + "name": "feeRecipient", + "type": "address" + }, + { + "indexed": false, + "name": "makerToken", + "type": "address" + }, + { + "indexed": false, + "name": "takerToken", + "type": "address" + }, + { + "indexed": false, + "name": "filledMakerTokenAmount", + "type": "uint256" + }, + { + "indexed": false, + "name": "filledTakerTokenAmount", + "type": "uint256" + }, + { + "indexed": false, + "name": "paidMakerFee", + "type": "uint256" + }, + { + "indexed": false, + "name": "paidTakerFee", + "type": "uint256" + }, + { + "indexed": true, + "name": "tokens", + "type": "bytes32" + }, + { + "indexed": false, + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "LogFill", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "maker", + "type": "address" + }, + { + "indexed": true, + "name": "feeRecipient", + "type": "address" + }, + { + "indexed": false, + "name": "makerToken", + "type": "address" + }, + { + "indexed": false, + "name": "takerToken", + "type": "address" + }, + { + "indexed": false, + "name": "cancelledMakerTokenAmount", + "type": "uint256" + }, + { + "indexed": false, + "name": "cancelledTakerTokenAmount", + "type": "uint256" + }, + { + "indexed": true, + "name": "tokens", + "type": "bytes32" + }, + { + "indexed": false, + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "LogCancel", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "errorId", + "type": "uint8" + }, + { + "indexed": true, + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "LogError", + "type": "event" + } + ], + "networks": { + "1": { + "address": "0x12459c951127e0c374ff9105dda097662a027093" + }, + "3": { + "address": "0x479cc461fecd078f766ecc58533d6f69580cf3ac" + }, + "4": { + "address": "0x1d16ef40fac01cec8adac2ac49427b9384192c05" + }, + "42": { + "address": "0x90fe2af704b34e0224bf2299c838e04d4dcf1364" + }, + "50": { + "address": "0x48bacb9266a570d521063ef5dd96e61686dbe788" + } + } +} diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts index cc2afa28a..fde134b18 100644 --- a/packages/website/ts/blockchain.ts +++ b/packages/website/ts/blockchain.ts @@ -66,6 +66,9 @@ import RpcSubprovider = require('web3-provider-engine/subproviders/rpc'); import * as MintableArtifacts from '../contracts/Mintable.json'; +// HACK: remove this hard-coded abi and use @0xproject/contract-wrappers +import * as Exchange from './artifacts/Exchange.json'; + const BLOCK_NUMBER_BACK_TRACK = 50; const GWEI_IN_WEI = 1000000000; @@ -89,6 +92,7 @@ export class Blockchain { private _userAddressIfExists: string; private _ledgerSubprovider: LedgerSubprovider; private _defaultGasPrice: BigNumber; + private _watchGasPriceIntervalId: NodeJS.Timer; private static _getNameGivenProvider(provider: Provider): string { const providerType = utils.getProviderType(provider); const providerNameIfExists = providerToName[providerType]; @@ -196,13 +200,11 @@ export class Blockchain { } constructor(dispatcher: Dispatcher) { this._dispatcher = dispatcher; - const defaultGasPrice = GWEI_IN_WEI * 30; + const defaultGasPrice = GWEI_IN_WEI * 40; this._defaultGasPrice = new BigNumber(defaultGasPrice); // We need a unique reference to this function so we can use it to unsubcribe. this._injectedProviderUpdateHandler = this._handleInjectedProviderUpdateAsync.bind(this); // tslint:disable-next-line:no-floating-promises - this._updateDefaultGasPriceAsync(); - // tslint:disable-next-line:no-floating-promises this._onPageLoadInitFireAndForgetAsync(); } public async networkIdUpdatedFireAndForgetAsync(newNetworkId: number): Promise<void> { @@ -537,6 +539,7 @@ export class Blockchain { this._blockchainWatcher.destroy(); this._injectedProviderObservable.unsubscribe(this._injectedProviderUpdateHandler); this._stopWatchingExchangeLogFillEvents(); + this._stopWatchingGasPrice(); } public async fetchTokenInformationAsync(): Promise<void> { utils.assert( @@ -624,7 +627,9 @@ export class Blockchain { ); const provider = this._contractWrappers.getProvider(); const web3Wrapper = new Web3Wrapper(provider); - web3Wrapper.abiDecoder.addABI(this._contractWrappers.exchange.abi); + // HACK: remove this hard-coded abi and use @0xproject/contract-wrappers + const exchangeAbi = _.get(Exchange, 'abi', []); + web3Wrapper.abiDecoder.addABI(exchangeAbi); const receipt = await web3Wrapper.awaitTransactionSuccessAsync(txHash); return receipt; } @@ -769,7 +774,7 @@ export class Blockchain { _.each(tokenRegistryTokens, (t: ZeroExToken) => { // HACK: For now we have a hard-coded list of iconUrls for the dummyTokens // TODO: Refactor this out and pull the iconUrl directly from the TokenRegistry - const iconUrl = configs.ICON_URL_BY_SYMBOL[t.symbol]; + const iconUrl = utils.getTokenIconUrl(t.symbol); const token: Token = { iconUrl, address: t.address, @@ -798,8 +803,30 @@ export class Blockchain { this._updateProviderName(injectedWeb3IfExists); const shouldPollUserAddress = true; const shouldUseLedgerProvider = false; + this._startWatchingGasPrice(); await this._resetOrInitializeAsync(this.networkId, shouldPollUserAddress, shouldUseLedgerProvider); } + private _startWatchingGasPrice(): void { + if (!_.isUndefined(this._watchGasPriceIntervalId)) { + return; // we are already watching + } + const oneMinuteInMs = 60000; + // tslint:disable-next-line:no-floating-promises + this._updateDefaultGasPriceAsync(); + this._watchGasPriceIntervalId = intervalUtils.setAsyncExcludingInterval( + this._updateDefaultGasPriceAsync.bind(this), + oneMinuteInMs, + (err: Error) => { + logUtils.log(`Watching gas price failed: ${err.stack}`); + this._stopWatchingGasPrice(); + }, + ); + } + private _stopWatchingGasPrice(): void { + if (!_.isUndefined(this._watchGasPriceIntervalId)) { + intervalUtils.clearAsyncExcludingInterval(this._watchGasPriceIntervalId); + } + } private async _resetOrInitializeAsync( networkId: number, shouldPollUserAddress: boolean = false, @@ -895,7 +922,7 @@ export class Blockchain { private async _updateDefaultGasPriceAsync(): Promise<void> { try { const gasInfo = await backendClient.getGasInfoAsync(); - const gasPriceInGwei = new BigNumber(gasInfo.average / 10); + const gasPriceInGwei = new BigNumber(gasInfo.fast / 10); const gasPriceInWei = gasPriceInGwei.mul(1000000000); this._defaultGasPrice = gasPriceInWei; } catch (err) { diff --git a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx index 38e4732a4..d2f373d67 100644 --- a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx +++ b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx @@ -29,7 +29,7 @@ interface LedgerConfigDialogProps { toggleDialogFn: (isOpen: boolean) => void; dispatcher: Dispatcher; blockchain: Blockchain; - networkId: number; + networkId?: number; providerType: ProviderType; } @@ -44,6 +44,9 @@ interface LedgerConfigDialogState { } export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, LedgerConfigDialogState> { + public static defaultProps = { + networkId: 1, + }; constructor(props: LedgerConfigDialogProps) { super(props); const derivationPathIfExists = props.blockchain.getLedgerDerivationPathIfExists(); diff --git a/packages/website/ts/components/eth_wrappers.tsx b/packages/website/ts/components/eth_wrappers.tsx index 20b446155..0b282b2a1 100644 --- a/packages/website/ts/components/eth_wrappers.tsx +++ b/packages/website/ts/components/eth_wrappers.tsx @@ -20,6 +20,7 @@ import { } from 'ts/types'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; +import { utils } from 'ts/utils/utils'; const DATE_FORMAT = 'D/M/YY'; const ICON_DIMENSION = 40; @@ -95,7 +96,11 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt this.props.networkId, EtherscanLinkSuffixes.Address, ); - const tokenLabel = this._renderToken('Wrapped Ether', etherToken.address, configs.ICON_URL_BY_SYMBOL.WETH); + const tokenLabel = this._renderToken( + 'Wrapped Ether', + etherToken.address, + utils.getTokenIconUrl(etherToken.symbol), + ); const userEtherBalanceInEth = !_.isUndefined(this.props.userEtherBalanceInWei) ? Web3Wrapper.toUnitAmount(this.props.userEtherBalanceInWei, constants.DECIMAL_PLACES_ETH) : undefined; diff --git a/packages/website/ts/components/generate_order/asset_picker.tsx b/packages/website/ts/components/generate_order/asset_picker.tsx index 3d53a9e7d..5eada37b6 100644 --- a/packages/website/ts/components/generate_order/asset_picker.tsx +++ b/packages/website/ts/components/generate_order/asset_picker.tsx @@ -3,6 +3,8 @@ import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; import * as moment from 'moment'; import * as React from 'react'; +import firstBy = require('thenby'); + import { Blockchain } from 'ts/blockchain'; import { NewTokenForm } from 'ts/components/generate_order/new_token_form'; import { TrackTokenConfirmation } from 'ts/components/track_token_confirmation'; @@ -87,10 +89,10 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt return ( <Dialog title={dialogConfigs.title} - titleStyle={{ fontWeight: 100 }} modal={dialogConfigs.isModal} open={this.props.isOpen} actions={dialogConfigs.actions} + autoScrollBodyContent={true} onRequestClose={this._onCloseDialog.bind(this)} > {this.state.assetView === AssetViews.ASSET_PICKER && this._renderAssetPicker()} @@ -121,9 +123,8 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt <div className="flex flex-wrap" style={{ - overflowY: 'auto', - maxWidth: 720, - maxHeight: 356, + maxWidth: 1000, + maxHeight: 600, marginBottom: 10, }} > @@ -134,15 +135,28 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt private _renderGridTiles(): React.ReactNode { let isHovered; let tileStyles; - const gridTiles = _.map(this.props.tokenByAddress, (token: Token, address: string) => { - if ( - (this.props.tokenVisibility === TokenVisibility.TRACKED && !utils.isTokenTracked(token)) || - (this.props.tokenVisibility === TokenVisibility.UNTRACKED && utils.isTokenTracked(token)) || - token.symbol === constants.ZRX_TOKEN_SYMBOL || - token.symbol === constants.ETHER_TOKEN_SYMBOL - ) { - return null; // Skip - } + const allTokens = _.values(this.props.tokenByAddress); + // filter tokens based on visibility specified in props, do not show ZRX or ETHER as tracked or untracked + const filteredTokens = + this.props.tokenVisibility === TokenVisibility.ALL + ? allTokens + : _.filter(allTokens, token => { + return ( + token.symbol !== constants.ZRX_TOKEN_SYMBOL && + token.symbol !== constants.ETHER_TOKEN_SYMBOL && + ((this.props.tokenVisibility === TokenVisibility.TRACKED && utils.isTokenTracked(token)) || + (this.props.tokenVisibility === TokenVisibility.UNTRACKED && + !utils.isTokenTracked(token))) + ); + }); + // if we are showing tracked tokens, sort by date added, otherwise sort by symbol + const sortKey = this.props.tokenVisibility === TokenVisibility.TRACKED ? 'trackedTimestamp' : 'symbol'; + const sortedTokens = filteredTokens.sort(firstBy(sortKey)); + if (_.isEmpty(sortedTokens)) { + return <div className="mx-auto p4 h2">No tokens to remove.</div>; + } + const gridTiles = _.map(sortedTokens, token => { + const address = token.address; isHovered = this.state.hoveredAddress === address; tileStyles = { cursor: 'pointer', diff --git a/packages/website/ts/components/onboarding/add_eth_onboarding_step.tsx b/packages/website/ts/components/onboarding/add_eth_onboarding_step.tsx index bccdc0c18..ca71fcd50 100644 --- a/packages/website/ts/components/onboarding/add_eth_onboarding_step.tsx +++ b/packages/website/ts/components/onboarding/add_eth_onboarding_step.tsx @@ -1,10 +1,10 @@ import { BigNumber } from '@0xproject/utils'; import * as React from 'react'; +import { Balance } from 'ts/components/ui/balance'; import { Container } from 'ts/components/ui/container'; import { Image } from 'ts/components/ui/image'; import { Text } from 'ts/components/ui/text'; import { constants } from 'ts/utils/constants'; -import { utils } from 'ts/utils/utils'; export interface AddEthOnboardingStepProps { userEthBalanceInWei: BigNumber; @@ -15,13 +15,11 @@ export const AddEthOnboardingStep: React.StatelessComponent<AddEthOnboardingStep <div className="flex items-center flex-column"> <Text> Great! Looks like you already have{' '} - <b> - {utils.getFormattedAmount( - props.userEthBalanceInWei, - constants.DECIMAL_PLACES_ETH, - constants.ETHER_SYMBOL, - )}{' '} - </b> + <Balance + amount={props.userEthBalanceInWei} + decimals={constants.DECIMAL_PLACES_ETH} + symbol={constants.ETHER_SYMBOL} + />{' '} in your wallet. </Text> <Container marginTop="15px" marginBottom="15px"> diff --git a/packages/website/ts/components/onboarding/onboarding_card.tsx b/packages/website/ts/components/onboarding/onboarding_card.tsx index ba5b3d6ea..e1b0f304b 100644 --- a/packages/website/ts/components/onboarding/onboarding_card.tsx +++ b/packages/website/ts/components/onboarding/onboarding_card.tsx @@ -12,6 +12,7 @@ export type ContinueButtonDisplay = 'enabled' | 'disabled'; export interface OnboardingCardProps { title?: string; + shouldCenterTitle?: boolean; content: React.ReactNode; isLastStep: boolean; onClose: () => void; @@ -23,10 +24,13 @@ export interface OnboardingCardProps { shouldHideNextButton?: boolean; continueButtonText?: string; borderRadius?: string; + // Used for super-custom content. + shouldRemoveExtraSpacing?: boolean; } export const OnboardingCard: React.StatelessComponent<OnboardingCardProps> = ({ title, + shouldCenterTitle, content, continueButtonDisplay, continueButtonText, @@ -37,55 +41,75 @@ export const OnboardingCard: React.StatelessComponent<OnboardingCardProps> = ({ shouldHideBackButton, shouldHideNextButton, borderRadius, -}) => ( - <Island borderRadius={borderRadius}> - <Container paddingRight="30px" paddingLeft="30px" paddingTop="15px" paddingBottom="15px"> - <div className="flex flex-column"> - <div className="flex justify-between"> - <Title>{title}</Title> - <Container position="relative" bottom="20px" left="15px"> - <IconButton color={colors.grey} iconName="zmdi-close" onClick={onClose}> - Close - </IconButton> + shouldRemoveExtraSpacing, +}) => { + const padding = shouldRemoveExtraSpacing + ? {} + : { + paddingRight: '30px', + paddingLeft: '30px', + paddingTop: '15px', + paddingBottom: '15px', + }; + const closeIconPositioning = shouldRemoveExtraSpacing + ? { right: '15px', bottom: '3px' } + : { bottom: '20px', left: '15px' }; + return ( + <Island borderRadius={borderRadius}> + <Container {...padding}> + <div className="flex flex-column"> + <Container className="flex justify-between"> + <Container width="100%"> + <Title center={shouldCenterTitle}>{title}</Title> + </Container> + <Container position="relative" {...closeIconPositioning}> + <IconButton color={colors.grey} iconName="zmdi-close" onClick={onClose}> + Close + </IconButton> + </Container> </Container> + <Container marginBottom={shouldRemoveExtraSpacing ? undefined : '15px'}> + <Text>{content}</Text> + </Container> + {continueButtonDisplay && ( + <Button + isDisabled={continueButtonDisplay === 'disabled'} + onClick={!_.isUndefined(onContinueButtonClick) ? onContinueButtonClick : onClickNext} + fontColor={colors.white} + fontSize="15px" + backgroundColor={colors.mediumBlue} + > + {continueButtonText} + </Button> + )} + {!(shouldHideBackButton && shouldHideNextButton) && ( + <Container className="clearfix" marginTop="15px"> + <div className="left"> + {!shouldHideBackButton && ( + <Text fontColor={colors.grey} onClick={onClickBack}> + Back + </Text> + )} + </div> + <div className="right"> + {!shouldHideNextButton && ( + <Text fontColor={colors.grey} onClick={onClickNext}> + Skip + </Text> + )} + </div> + </Container> + )} </div> - <Container marginBottom="15px"> - <Text>{content}</Text> - </Container> - {continueButtonDisplay && ( - <Button - isDisabled={continueButtonDisplay === 'disabled'} - onClick={!_.isUndefined(onContinueButtonClick) ? onContinueButtonClick : onClickNext} - fontColor={colors.white} - fontSize="15px" - backgroundColor={colors.mediumBlue} - > - {continueButtonText} - </Button> - )} - <Container className="clearfix" marginTop="15px"> - <div className="left"> - {!shouldHideBackButton && ( - <Text fontColor={colors.grey} onClick={onClickBack}> - Back - </Text> - )} - </div> - <div className="right"> - {!shouldHideNextButton && ( - <Text fontColor={colors.grey} onClick={onClickNext}> - Skip - </Text> - )} - </div> - </Container> - </div> - </Container> - </Island> -); + </Container> + </Island> + ); +}; OnboardingCard.defaultProps = { continueButtonText: 'Continue', + shouldCenterTitle: false, + shouldRemoveExtraSpacing: false, }; OnboardingCard.displayName = 'OnboardingCard'; diff --git a/packages/website/ts/components/onboarding/onboarding_flow.tsx b/packages/website/ts/components/onboarding/onboarding_flow.tsx index c2b4a4ca7..91d5f2476 100644 --- a/packages/website/ts/components/onboarding/onboarding_flow.tsx +++ b/packages/website/ts/components/onboarding/onboarding_flow.tsx @@ -2,11 +2,14 @@ import * as React from 'react'; import { Placement, Popper, PopperChildrenProps } from 'react-popper'; import { OnboardingCard } from 'ts/components/onboarding/onboarding_card'; -import { ContinueButtonDisplay, OnboardingTooltip } from 'ts/components/onboarding/onboarding_tooltip'; +import { + ContinueButtonDisplay, + OnboardingTooltip, + TooltipPointerDisplay, +} from 'ts/components/onboarding/onboarding_tooltip'; import { Animation } from 'ts/components/ui/animation'; import { Container } from 'ts/components/ui/container'; import { Overlay } from 'ts/components/ui/overlay'; -import { PointerDirection } from 'ts/components/ui/pointer'; import { zIndex } from 'ts/style/z_index'; export interface FixedPositionSettings { @@ -15,7 +18,7 @@ export interface FixedPositionSettings { bottom?: string; left?: string; right?: string; - pointerDirection?: PointerDirection; + tooltipPointerDisplay?: TooltipPointerDisplay; } export interface TargetPositionSettings { @@ -28,12 +31,15 @@ export interface Step { // Provide either a CSS selector, or fixed position settings. Only applies to desktop. position: TargetPositionSettings | FixedPositionSettings; title?: string; + shouldCenterTitle?: boolean; content: React.ReactNode; shouldHideBackButton?: boolean; shouldHideNextButton?: boolean; continueButtonDisplay?: ContinueButtonDisplay; continueButtonText?: string; onContinueButtonClick?: () => void; + // Only used for very custom steps. + shouldRemoveExtraSpacing?: boolean; } export interface OnboardingFlowProps { @@ -44,12 +50,14 @@ export interface OnboardingFlowProps { updateOnboardingStep: (stepIndex: number) => void; disableOverlay?: boolean; isMobile: boolean; + disableCloseOnClickOutside?: boolean; } export class OnboardingFlow extends React.Component<OnboardingFlowProps> { public static defaultProps = { disableOverlay: false, isMobile: false, + disableCloseOnClickOutside: false, }; public render(): React.ReactNode { if (!this.props.isRunning) { @@ -67,7 +75,7 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> { </Popper> ); } else if (currentStep.position.type === 'fixed') { - const { top, right, bottom, left, pointerDirection } = currentStep.position; + const { top, right, bottom, left, tooltipPointerDisplay } = currentStep.position; onboardingElement = ( <Container position="fixed" @@ -77,7 +85,7 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> { bottom={bottom} left={left} > - {this._renderToolTip(pointerDirection)} + {this._renderToolTip(tooltipPointerDisplay)} </Container> ); } @@ -86,7 +94,7 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> { } return ( <div> - <Overlay onClick={this.props.onClose} /> + <Overlay onClick={this.props.disableCloseOnClickOutside ? undefined : this.props.onClose} /> {onboardingElement} </div> ); @@ -101,7 +109,7 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> { </div> ); } - private _renderToolTip(pointerDirection?: PointerDirection): React.ReactNode { + private _renderToolTip(tooltipPointerDisplay?: TooltipPointerDisplay): React.ReactNode { const { steps, stepIndex } = this.props; const step = steps[stepIndex]; const isLastStep = steps.length - 1 === stepIndex; @@ -109,6 +117,7 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> { <Container marginLeft="30px" width="400px"> <OnboardingTooltip title={step.title} + shouldCenterTitle={step.shouldCenterTitle} content={step.content} isLastStep={isLastStep} shouldHideBackButton={step.shouldHideBackButton} @@ -119,7 +128,8 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> { continueButtonDisplay={step.continueButtonDisplay} continueButtonText={step.continueButtonText} onContinueButtonClick={step.onContinueButtonClick} - pointerDirection={pointerDirection} + pointerDisplay={tooltipPointerDisplay} + shouldRemoveExtraSpacing={step.shouldRemoveExtraSpacing} /> </Container> ); @@ -133,6 +143,7 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> { <Container position="relative" zIndex={1}> <OnboardingCard title={step.title} + shouldCenterTitle={step.shouldCenterTitle} content={step.content} isLastStep={isLastStep} shouldHideBackButton={step.shouldHideBackButton} @@ -144,6 +155,7 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> { continueButtonText={step.continueButtonText} onContinueButtonClick={step.onContinueButtonClick} borderRadius="10px 10px 0px 0px" + shouldRemoveExtraSpacing={step.shouldRemoveExtraSpacing} /> </Container> ); diff --git a/packages/website/ts/components/onboarding/onboarding_tooltip.tsx b/packages/website/ts/components/onboarding/onboarding_tooltip.tsx index d8065625d..15d47908d 100644 --- a/packages/website/ts/components/onboarding/onboarding_tooltip.tsx +++ b/packages/website/ts/components/onboarding/onboarding_tooltip.tsx @@ -4,22 +4,27 @@ import { OnboardingCard, OnboardingCardProps } from 'ts/components/onboarding/on import { Pointer, PointerDirection } from 'ts/components/ui/pointer'; export type ContinueButtonDisplay = 'enabled' | 'disabled'; +export type TooltipPointerDisplay = PointerDirection | 'none'; export interface OnboardingTooltipProps extends OnboardingCardProps { className?: string; - pointerDirection?: PointerDirection; + pointerDisplay?: TooltipPointerDisplay; } export const OnboardingTooltip: React.StatelessComponent<OnboardingTooltipProps> = props => { - const { pointerDirection, className, ...cardProps } = props; + const { pointerDisplay, className, ...cardProps } = props; + const card = <OnboardingCard {...cardProps} />; + if (pointerDisplay === 'none') { + return card; + } return ( - <Pointer className={className} direction={pointerDirection}> + <Pointer className={className} direction={pointerDisplay}> <OnboardingCard {...cardProps} /> </Pointer> ); }; OnboardingTooltip.defaultProps = { - pointerDirection: 'left', + pointerDisplay: 'left', }; OnboardingTooltip.displayName = 'OnboardingTooltip'; diff --git a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx index 1c2c92fd1..20a8f0a32 100644 --- a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx +++ b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx @@ -23,7 +23,7 @@ import { WrapEthOnboardingStep3, } from 'ts/components/onboarding/wrap_eth_onboarding_step'; import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle'; -import { ProviderType, ScreenWidths, Token, TokenByAddress, TokenStateByAddress } from 'ts/types'; +import { BrowserType, ProviderType, ScreenWidths, Token, TokenByAddress, TokenStateByAddress } from 'ts/types'; import { analytics } from 'ts/utils/analytics'; import { utils } from 'ts/utils/utils'; @@ -68,6 +68,7 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp } } public render(): React.ReactNode { + const browserType = utils.getBrowserType(); return ( <OnboardingFlow steps={this._getSteps()} @@ -77,6 +78,8 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp updateOnboardingStep={this._updateOnboardingStep.bind(this)} disableOverlay={this.props.screenWidth === ScreenWidths.Sm} isMobile={this.props.screenWidth === ScreenWidths.Sm} + // This is necessary to ensure onboarding stays open once the user unlocks metamask and clicks away + disableCloseOnClickOutside={browserType === BrowserType.Firefox || browserType === BrowserType.Opera} /> ); } @@ -88,9 +91,9 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp }; const underMetamaskExtension: FixedPositionSettings = { type: 'fixed', - top: '30px', + top: '10px', right: '10px', - pointerDirection: 'top', + tooltipPointerDisplay: 'none', }; const steps: Step[] = [ { @@ -102,10 +105,12 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp }, { position: underMetamaskExtension, - title: '0x Ecosystem Setup', + title: 'Please Unlock Metamask...', content: <UnlockWalletOnboardingStep />, shouldHideBackButton: true, shouldHideNextButton: true, + shouldCenterTitle: true, + shouldRemoveExtraSpacing: true, }, { position: nextToWalletPosition, @@ -137,13 +142,7 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp { position: nextToWalletPosition, title: 'Step 2: Wrap ETH', - content: ( - <WrapEthOnboardingStep3 - formattedWethBalanceIfExists={ - this._userHasVisibleWeth() ? this._getFormattedWethBalance() : undefined - } - /> - ), + content: <WrapEthOnboardingStep3 wethAmount={this._getWethBalance()} />, continueButtonDisplay: this._userHasVisibleWeth() ? 'enabled' : 'disabled', }, { @@ -184,11 +183,6 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp const ethTokenState = this.props.trackedTokenStateByAddress[ethToken.address]; return ethTokenState.balance; } - private _getFormattedWethBalance(): string { - const ethToken = utils.getEthToken(this.props.tokenByAddress); - const ethTokenState = this.props.trackedTokenStateByAddress[ethToken.address]; - return utils.getFormattedAmountFromToken(ethToken, ethTokenState); - } private _userHasVisibleWeth(): boolean { return this._getWethBalance() > new BigNumber(0); } diff --git a/packages/website/ts/components/onboarding/unlock_wallet_onboarding_step.tsx b/packages/website/ts/components/onboarding/unlock_wallet_onboarding_step.tsx index 4ed7137d4..358141520 100644 --- a/packages/website/ts/components/onboarding/unlock_wallet_onboarding_step.tsx +++ b/packages/website/ts/components/onboarding/unlock_wallet_onboarding_step.tsx @@ -1,16 +1,8 @@ import * as React from 'react'; -import { Container } from 'ts/components/ui/container'; -import { Text } from 'ts/components/ui/text'; +import { Image } from 'ts/components/ui/image'; export interface UnlockWalletOnboardingStepProps {} export const UnlockWalletOnboardingStep: React.StatelessComponent<UnlockWalletOnboardingStepProps> = () => ( - <div className="flex items-center flex-column"> - <div className="flex items-center flex-column"> - <Container marginTop="15px" marginBottom="15px"> - <img src="/images/metamask_icon.png" height="50px" width="50px" /> - </Container> - <Text center={true}>Unlock your MetaMask extension to get started.</Text> - </div> - </div> + <Image src="/images/unlock-mm.png" /> ); diff --git a/packages/website/ts/components/onboarding/wrap_eth_onboarding_step.tsx b/packages/website/ts/components/onboarding/wrap_eth_onboarding_step.tsx index 4d336c80f..e4332de75 100644 --- a/packages/website/ts/components/onboarding/wrap_eth_onboarding_step.tsx +++ b/packages/website/ts/components/onboarding/wrap_eth_onboarding_step.tsx @@ -1,8 +1,11 @@ import { colors } from '@0xproject/react-shared'; +import { BigNumber } from '@0xproject/utils'; import * as React from 'react'; +import { Balance } from 'ts/components/ui/balance'; import { Container } from 'ts/components/ui/container'; import { IconButton } from 'ts/components/ui/icon_button'; import { Text } from 'ts/components/ui/text'; +import { constants } from 'ts/utils/constants'; export interface WrapEthOnboardingStep1Props {} @@ -51,16 +54,20 @@ export const WrapEthOnboardingStep2: React.StatelessComponent<WrapEthOnboardingS ); export interface WrapEthOnboardingStep3Props { - formattedWethBalanceIfExists?: string; + wethAmount: BigNumber; } -export const WrapEthOnboardingStep3: React.StatelessComponent<WrapEthOnboardingStep3Props> = ({ - formattedWethBalanceIfExists, -}) => ( +export const WrapEthOnboardingStep3: React.StatelessComponent<WrapEthOnboardingStep3Props> = ({ wethAmount }) => ( <div className="flex items-center flex-column"> <Text> - You have <b>{formattedWethBalanceIfExists || '0 WETH'}</b> in your wallet. - {formattedWethBalanceIfExists && ' Great!'} + You have{' '} + <Balance + amount={wethAmount} + decimals={constants.DECIMAL_PLACES_ETH} + symbol={constants.ETHER_TOKEN_SYMBOL} + />{' '} + in your wallet. + {wethAmount.gt(0) && ' Great!'} </Text> <Container width="100%" marginTop="25px" marginBottom="15px" className="flex justify-center"> <div className="flex flex-column items-center"> diff --git a/packages/website/ts/components/ui/balance.tsx b/packages/website/ts/components/ui/balance.tsx new file mode 100644 index 000000000..9e5a256b6 --- /dev/null +++ b/packages/website/ts/components/ui/balance.tsx @@ -0,0 +1,27 @@ +import { BigNumber } from '@0xproject/utils'; +import * as React from 'react'; +import { Container } from 'ts/components/ui/container'; +import { Text } from 'ts/components/ui/text'; +import { utils } from 'ts/utils/utils'; + +export interface BalanceProps { + amount: BigNumber; + decimals: number; + symbol: string; +} + +export const Balance: React.StatelessComponent<BalanceProps> = ({ amount, decimals, symbol }) => { + const formattedAmout = utils.getFormattedAmount(amount, decimals); + return ( + <span> + <Text Tag="span" fontSize="16px" fontWeight="700" lineHeight="1em"> + {formattedAmout} + </Text> + <Container marginLeft="0.3em" Tag="span"> + <Text Tag="span" fontSize="12px" fontWeight="700" lineHeight="1em"> + {symbol} + </Text> + </Container> + </span> + ); +}; diff --git a/packages/website/ts/components/ui/container.tsx b/packages/website/ts/components/ui/container.tsx index edbf8814b..427cc6cc7 100644 --- a/packages/website/ts/components/ui/container.tsx +++ b/packages/website/ts/components/ui/container.tsx @@ -2,6 +2,8 @@ import * as React from 'react'; type StringOrNum = string | number; +export type ContainerTag = 'div' | 'span'; + export interface ContainerProps { marginTop?: StringOrNum; marginBottom?: StringOrNum; @@ -28,15 +30,21 @@ export interface ContainerProps { right?: string; bottom?: string; zIndex?: number; + Tag?: ContainerTag; } -export const Container: React.StatelessComponent<ContainerProps> = ({ children, className, isHidden, ...style }) => { +export const Container: React.StatelessComponent<ContainerProps> = props => { + const { children, className, Tag, isHidden, ...style } = props; const visibility = isHidden ? 'hidden' : undefined; return ( - <div style={{ ...style, visibility }} className={className}> + <Tag style={{ ...style, visibility }} className={className}> {children} - </div> + </Tag> ); }; +Container.defaultProps = { + Tag: 'div', +}; + Container.displayName = 'Container'; diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx index de3b91ad0..6c1c495d7 100644 --- a/packages/website/ts/components/wallet/wallet.tsx +++ b/packages/website/ts/components/wallet/wallet.tsx @@ -8,6 +8,7 @@ import firstBy = require('thenby'); import { Blockchain } from 'ts/blockchain'; import { AccountConnection } from 'ts/components/ui/account_connection'; +import { Balance } from 'ts/components/ui/balance'; import { Container } from 'ts/components/ui/container'; import { DropDown, DropdownMouseEvent } from 'ts/components/ui/drop_down'; import { IconButton } from 'ts/components/ui/icon_button'; @@ -269,8 +270,8 @@ export class Wallet extends React.Component<WalletProps, WalletState> { position: 'relative', overflowY: this.state.isHoveringSidebar ? 'scroll' : 'hidden', marginRight: this.state.isHoveringSidebar ? 0 : 4, - // TODO: make this completely responsive - maxHeight: this.props.screenWidth !== ScreenWidths.Sm ? 475 : undefined, + minHeight: '250px', + maxHeight: !utils.isMobileWidth(this.props.screenWidth) ? 'calc(90vh - 300px)' : undefined, }; } private _onSidebarHover(_event: React.FormEvent<HTMLInputElement>): void { @@ -436,12 +437,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { </PlaceHolder> ); } else { - const result = utils.getFormattedAmount(amount, decimals, symbol); - return ( - <Text fontSize="16px" fontWeight="bold" lineHeight="1em"> - {result} - </Text> - ); + return <Balance amount={amount} decimals={decimals} symbol={symbol} />; } } private _renderValue( diff --git a/packages/website/ts/types.ts b/packages/website/ts/types.ts index f7324e87a..e8dc694f6 100644 --- a/packages/website/ts/types.ts +++ b/packages/website/ts/types.ts @@ -243,8 +243,8 @@ export enum BlockchainCallErrs { } export enum Environments { - DEVELOPMENT, - PRODUCTION, + DEVELOPMENT = 'DEVELOPMENT', + PRODUCTION = 'PRODUCTION', } export type ContractInstance = any; // TODO: add type definition for Contract @@ -552,7 +552,10 @@ export interface WebsiteBackendTokenInfo { } export interface WebsiteBackendGasInfo { + safeSlow: number; average: number; + fast: number; + fastest: number; } export interface WebsiteBackendJobInfo { diff --git a/packages/website/ts/utils/configs.ts b/packages/website/ts/utils/configs.ts index e8a486c35..97aabd13d 100644 --- a/packages/website/ts/utils/configs.ts +++ b/packages/website/ts/utils/configs.ts @@ -22,50 +22,9 @@ export const configs = { DOMAIN_DEVELOPMENT: '0xproject.localhost:3572', DOMAIN_PRODUCTION: '0xproject.com', ENVIRONMENT: isDevelopment ? Environments.DEVELOPMENT : Environments.PRODUCTION, - ICON_URL_BY_SYMBOL: { - REP: '/images/token_icons/augur.png', - DGD: '/images/token_icons/digixdao.png', - WETH: '/images/token_icons/ether_erc20.png', - MLN: '/images/token_icons/melon.png', - GNT: '/images/token_icons/golem.png', - MKR: '/images/token_icons/makerdao.png', - ZRX: '/images/token_icons/zero_ex.png', - ANT: '/images/token_icons/aragon.png', - BNT: '/images/token_icons/bancor.png', - BAT: '/images/token_icons/basicattentiontoken.png', - CVC: '/images/token_icons/civic.png', - EOS: '/images/token_icons/eos.png', - FUN: '/images/token_icons/funfair.png', - GNO: '/images/token_icons/gnosis.png', - ICN: '/images/token_icons/iconomi.png', - OMG: '/images/token_icons/omisego.png', - SNT: '/images/token_icons/status.png', - STORJ: '/images/token_icons/storjcoinx.png', - PAY: '/images/token_icons/tenx.png', - QTUM: '/images/token_icons/qtum.png', - DNT: '/images/token_icons/district0x.png', - SNGLS: '/images/token_icons/singularity.png', - EDG: '/images/token_icons/edgeless.png', - '1ST': '/images/token_icons/firstblood.jpg', - WINGS: '/images/token_icons/wings.png', - BQX: '/images/token_icons/bitquence.png', - LUN: '/images/token_icons/lunyr.png', - RLC: '/images/token_icons/iexec.png', - MCO: '/images/token_icons/monaco.png', - ADT: '/images/token_icons/adtoken.png', - CFI: '/images/token_icons/cofound-it.png', - ROL: '/images/token_icons/etheroll.png', - WGNT: '/images/token_icons/golem.png', - MTL: '/images/token_icons/metal.png', - NMR: '/images/token_icons/numeraire.png', - SAN: '/images/token_icons/santiment.png', - TAAS: '/images/token_icons/taas.png', - TKN: '/images/token_icons/tokencard.png', - TRST: '/images/token_icons/trust.png', - } as { [symbol: string]: string }, GOOGLE_ANALYTICS_ID: 'UA-98720122-1', LAST_LOCAL_STORAGE_FILL_CLEARANCE_DATE: '2017-11-22', - LAST_LOCAL_STORAGE_TRACKED_TOKEN_CLEARANCE_DATE: '2018-6-25', + LAST_LOCAL_STORAGE_TRACKED_TOKEN_CLEARANCE_DATE: '2018-7-5', OUTDATED_WRAPPED_ETHERS: [ { 42: { diff --git a/packages/website/ts/utils/utils.ts b/packages/website/ts/utils/utils.ts index 623819fc9..8c76a7592 100644 --- a/packages/website/ts/utils/utils.ts +++ b/packages/website/ts/utils/utils.ts @@ -359,7 +359,9 @@ export const utils = { }, isDogfood, shouldShowPortalV2(): boolean { - return this.isDevelopment() || this.isStaging() || this.isDogfood(); + // return this.isDevelopment() || this.isStaging() || this.isDogfood(); + // TODO: Remove this method entirely after launch. + return true; }, shouldShowJobsPage(): boolean { return this.isDevelopment() || this.isStaging() || this.isDogfood(); @@ -381,9 +383,9 @@ export const utils = { return trackedTokens; }, getFormattedAmountFromToken(token: Token, tokenState: TokenState): string { - return utils.getFormattedAmount(tokenState.balance, token.decimals, token.symbol); + return utils.getFormattedAmount(tokenState.balance, token.decimals); }, - getFormattedAmount(amount: BigNumber, decimals: number, symbol: string): string { + getFormattedAmount(amount: BigNumber, decimals: number): string { const unitAmount = Web3Wrapper.toUnitAmount(amount, decimals); // if the unit amount is less than 1, show the natural number of decimal places with a max of 4 // if the unit amount is greater than or equal to 1, show only 2 decimal places @@ -392,7 +394,7 @@ export const utils = { : 2; const format = `0,0.${_.repeat('0', precision)}`; const formattedAmount = numeral(unitAmount).format(format); - return `${formattedAmount} ${symbol}`; + return formattedAmount; }, getUsdValueFormattedAmount(amount: BigNumber, decimals: number, price: BigNumber): string { const unitAmount = Web3Wrapper.toUnitAmount(amount, decimals); @@ -474,4 +476,8 @@ export const utils = { } return [downloadLink, isOnMobile]; }, + getTokenIconUrl(symbol: string): string { + const result = `/images/token_icons/${symbol}.png`; + return result; + }, }; |