diff options
Diffstat (limited to 'packages/website')
21 files changed, 341 insertions, 123 deletions
diff --git a/packages/website/md/docs/sol_cov/usage.md b/packages/website/md/docs/sol_cov/usage.md index 63a88f595..59638f611 100644 --- a/packages/website/md/docs/sol_cov/usage.md +++ b/packages/website/md/docs/sol_cov/usage.md @@ -2,8 +2,47 @@ Sol-cov uses transaction traces in order to figure out which lines of Solidity s The CoverageSubprovider eavesdrops on the `eth_sendTransaction` and `eth_call` RPC calls and collects traces after each call using `debug_traceTransaction`. `eth_call`'s' don't generate traces - so we take a snapshot, re-submit it as a transaction, get the trace and then revert the snapshot. +Coverage subprovider needs some info about your contracts (`srcMap`, `bytecode`). It gets that info from your project's artifacts. Some frameworks have their own artifact format. Some artifact formats don't actually contain all the neccessary data. + +In order to use `CoverageSubprovider` with your favorite framework you need to pass an `artifactsAdapter` to it. + +### Sol-compiler + +If you are generating your artifacts with [@0xproject/sol-compiler](LINK) you can use the `SolCompilerArtifactsAdapter` we've implemented for you. + +```typescript +<<<<<<< HEAD +import { CoverageSubprovider } from '@0xproject/sol-cov'; +======= +import { SolCompilerArtifactsAdapter } from '@0xproject/sol-cov'; +const artifactsPath = 'src/artifacts'; +const contractsPath = 'src/contracts'; +const artifactsAdapter = new SolCompilerArtifactsAdapter(artifactsPath, contractsPath); +``` + +### Truffle + +If your project is using [Truffle](LINK), we've written a `TruffleArtifactsAdapter`for you. + +```typescript +import { TruffleArtifactAdapter } from '@0xproject/sol-cov'; +const contractsPath = 'src/contracts'; +const artifactAdapter = new TruffleArtifactAdapter(contractsDir); +``` + +Because truffle artifacts don't have all the data we need - we actually will recompile your contracts under the hood. That's why you don't need to pass an `artifactsPath`. + +### Other framework/toolset + +You'll need to write your own artifacts adapter. It should extend `AbstractArtifactsAdapter`. +Look at the code of the two adapters above for examples. + +### Usage + ```typescript import { CoverageSubprovider } from '@0xproject/sol-cov'; +import ProviderEngine = require('web3-provider-engine'); +>>>>>>> Improve sol-cov docs const provider = new ProviderEngine(); @@ -12,15 +51,20 @@ const contractsPath = 'src/contracts'; const networkId = 50; // Some calls might not have `from` address specified. Nevertheless - transactions need to be submitted from an address with at least some funds. defaultFromAddress is the address that will be used to submit those calls as transactions from. const defaultFromAddress = '0x5409ed021d9299bf6814279a6a1411a7e866a631'; +<<<<<<< HEAD const coverageSubprovider = new CoverageSubprovider(artifactsPath, contractsPath, defaultFromAddress); +======= +const isVerbose = true; +const coverageSubprovider = new CoverageSubprovider(artifactsAdapter, defaultFromAddress, isVerbose); +>>>>>>> Improve sol-cov docs provider.addProvider(coverageSubprovider); ``` -After your test suite is complete (e.g global `after` hook), you'll need to call: +After your test suite is complete (e.g in the Mocha global `after` hook), you'll need to call: ```typescript await coverageSubprovider.writeCoverageAsync(); ``` -This will create a `coverage.json` file in the `coverage` directory. This file has an [Istanbul format](https://github.com/gotwarlost/istanbul/blob/master/coverage.json.md) - so you can use any of the existing Instanbul reporters. +This will create a `coverage.json` file in a `coverage` directory. This file has an [Istanbul format](https://github.com/gotwarlost/istanbul/blob/master/coverage.json.md) - so you can use it with any of the existing Istanbul reporters. diff --git a/packages/website/package.json b/packages/website/package.json index 95804f988..a17964f2b 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -57,11 +57,13 @@ "web3": "^0.20.0", "web3-provider-engine": "^14.0.4", "whatwg-fetch": "^2.0.3", - "xml-js": "^1.3.2" + "xml-js": "^1.6.4" }, "devDependencies": { "@types/accounting": "^0.4.1", + "@types/blockies": "^0.0.0", "@types/deep-equal": "^1.0.0", + "@types/find-versions": "^2.0.0", "@types/jsonschema": "^1.1.1", "@types/lodash": "4.14.104", "@types/material-ui": "0.18.0", diff --git a/packages/website/ts/blockchain_watcher.ts b/packages/website/ts/blockchain_watcher.ts index c420a98a4..0d376bc74 100644 --- a/packages/website/ts/blockchain_watcher.ts +++ b/packages/website/ts/blockchain_watcher.ts @@ -10,7 +10,7 @@ export class BlockchainWatcher { private _prevNetworkId: number; private _shouldPollUserAddress: boolean; private _watchNetworkAndBalanceIntervalId: NodeJS.Timer; - private _prevUserEtherBalanceInWei: BigNumber; + private _prevUserEtherBalanceInWei?: BigNumber; private _prevUserAddressIfExists: string; constructor( dispatcher: Dispatcher, @@ -41,7 +41,7 @@ export class BlockchainWatcher { } let prevNodeVersion: string; - this._prevUserEtherBalanceInWei = new BigNumber(0); + this._prevUserEtherBalanceInWei = undefined; this._dispatcher.updateNetworkId(this._prevNetworkId); this._watchNetworkAndBalanceIntervalId = intervalUtils.setAsyncExcludingInterval( async () => { @@ -94,7 +94,7 @@ export class BlockchainWatcher { } private async _updateUserWeiBalanceAsync(userAddress: string): Promise<void> { const balanceInWei = await this._web3Wrapper.getBalanceInWeiAsync(userAddress); - if (!balanceInWei.eq(this._prevUserEtherBalanceInWei)) { + if (_.isUndefined(this._prevUserEtherBalanceInWei) || !balanceInWei.eq(this._prevUserEtherBalanceInWei)) { this._prevUserEtherBalanceInWei = balanceInWei; this._dispatcher.updateUserWeiBalance(balanceInWei); } diff --git a/packages/website/ts/components/eth_weth_conversion_button.tsx b/packages/website/ts/components/eth_weth_conversion_button.tsx index 4b91a2ebd..2fb35cc1c 100644 --- a/packages/website/ts/components/eth_weth_conversion_button.tsx +++ b/packages/website/ts/components/eth_weth_conversion_button.tsx @@ -18,7 +18,7 @@ interface EthWethConversionButtonProps { ethToken: Token; dispatcher: Dispatcher; blockchain: Blockchain; - userEtherBalanceInWei: BigNumber; + userEtherBalanceInWei?: BigNumber; isOutdatedWrappedEther: boolean; onConversionSuccessful?: () => void; isDisabled?: boolean; diff --git a/packages/website/ts/components/eth_wrappers.tsx b/packages/website/ts/components/eth_wrappers.tsx index a5758a66a..1db5ff77f 100644 --- a/packages/website/ts/components/eth_wrappers.tsx +++ b/packages/website/ts/components/eth_wrappers.tsx @@ -33,7 +33,7 @@ interface EthWrappersProps { dispatcher: Dispatcher; tokenByAddress: TokenByAddress; userAddress: string; - userEtherBalanceInWei: BigNumber; + userEtherBalanceInWei?: BigNumber; lastForceTokenStateRefetch: number; } diff --git a/packages/website/ts/components/legacy_portal/legacy_portal.tsx b/packages/website/ts/components/legacy_portal/legacy_portal.tsx index a5ea95629..e5d152e3e 100644 --- a/packages/website/ts/components/legacy_portal/legacy_portal.tsx +++ b/packages/website/ts/components/legacy_portal/legacy_portal.tsx @@ -38,6 +38,7 @@ import { } from 'ts/types'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; +import { orderParser } from 'ts/utils/order_parser'; import { Translate } from 'ts/utils/translate'; import { utils } from 'ts/utils/utils'; @@ -55,7 +56,7 @@ export interface LegacyPortalProps { providerType: ProviderType; screenWidth: ScreenWidths; tokenByAddress: TokenByAddress; - userEtherBalanceInWei: BigNumber; + userEtherBalanceInWei?: BigNumber; userAddress: string; shouldBlockchainErrDialogBeOpen: boolean; userSuppliedOrderCache: Order; @@ -86,7 +87,7 @@ export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPorta } constructor(props: LegacyPortalProps) { super(props); - this._sharedOrderIfExists = this._getSharedOrderIfExists(); + this._sharedOrderIfExists = orderParser.parse(window.location.search); this._throttledScreenWidthUpdate = _.throttle(this._updateScreenWidth.bind(this), THROTTLE_TIMEOUT); const isViewingBalances = _.includes(props.location.pathname, `${WebsitePaths.Portal}/balances`); @@ -362,32 +363,6 @@ export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPorta isWethNoticeDialogOpen: false, }); } - private _getSharedOrderIfExists(): Order | undefined { - const queryString = window.location.search; - if (queryString.length === 0) { - return undefined; - } - const queryParams = queryString.substring(1).split('&'); - const orderQueryParam = _.find(queryParams, queryParam => { - const queryPair = queryParam.split('='); - return queryPair[0] === 'order'; - }); - if (_.isUndefined(orderQueryParam)) { - return undefined; - } - const orderPair = orderQueryParam.split('='); - if (orderPair.length !== 2) { - return undefined; - } - - const order = JSON.parse(decodeURIComponent(orderPair[1])); - const validationResult = validator.validate(order, portalOrderSchema); - if (validationResult.errors.length > 0) { - logUtils.log(`Invalid shared order: ${validationResult.errors}`); - return undefined; - } - return order; - } private _updateScreenWidth(): void { const newScreenWidth = utils.getScreenWidth(); this.props.dispatcher.updateScreenWidth(newScreenWidth); diff --git a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx index edaeb3736..2014dd7b0 100644 --- a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx +++ b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx @@ -14,7 +14,7 @@ export interface PortalOnboardingFlowProps { providerType: ProviderType; injectedProviderName: string; blockchainIsLoaded: boolean; - userEthBalanceInWei: BigNumber; + userEtherBalanceInWei?: BigNumber; tokenByAddress: TokenByAddress; updateIsRunning: (isRunning: boolean) => void; updateOnboardingStep: (stepIndex: number) => void; @@ -85,7 +85,7 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr } private _userHasEth(): boolean { - return this.props.userEthBalanceInWei > new BigNumber(0); + return this.props.userEtherBalanceInWei > new BigNumber(0); } private _userHasWeth(): boolean { diff --git a/packages/website/ts/components/portal/menu.tsx b/packages/website/ts/components/portal/menu.tsx index 6a3301549..6e97ee37e 100644 --- a/packages/website/ts/components/portal/menu.tsx +++ b/packages/website/ts/components/portal/menu.tsx @@ -43,9 +43,14 @@ export const defaultMenuItemEntries: MenuItemEntry[] = [ iconName: 'zmdi-circle-o', }, { - to: `${WebsitePaths.Portal}/direct`, - labelText: 'Trade direct', - iconName: 'zmdi-swap', + to: `${WebsitePaths.Portal}/generate`, + labelText: 'Generate order', + iconName: 'zmdi-arrow-right-top', + }, + { + to: `${WebsitePaths.Portal}/fill`, + labelText: 'Fill order', + iconName: 'zmdi-arrow-left-bottom', }, ]; diff --git a/packages/website/ts/components/portal/portal.tsx b/packages/website/ts/components/portal/portal.tsx index 0e1506e17..589ad00ad 100644 --- a/packages/website/ts/components/portal/portal.tsx +++ b/packages/website/ts/components/portal/portal.tsx @@ -10,6 +10,7 @@ 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 { EthWrappers } from 'ts/components/eth_wrappers'; +import { FillOrder } from 'ts/components/fill_order'; import { AssetPicker } from 'ts/components/generate_order/asset_picker'; import { BackButton } from 'ts/components/portal/back_button'; import { Loading } from 'ts/components/portal/loading'; @@ -42,6 +43,7 @@ import { } from 'ts/types'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; +import { orderParser } from 'ts/utils/order_parser'; import { Translate } from 'ts/utils/translate'; import { utils } from 'ts/utils/utils'; @@ -57,7 +59,7 @@ export interface PortalProps { providerType: ProviderType; screenWidth: ScreenWidths; tokenByAddress: TokenByAddress; - userEtherBalanceInWei: BigNumber; + userEtherBalanceInWei?: BigNumber; userAddress: string; shouldBlockchainErrDialogBeOpen: boolean; userSuppliedOrderCache: Order; @@ -116,9 +118,11 @@ const styles: Styles = { export class Portal extends React.Component<PortalProps, PortalState> { private _blockchain: Blockchain; + private _sharedOrderIfExists: Order; private _throttledScreenWidthUpdate: () => void; constructor(props: PortalProps) { super(props); + this._sharedOrderIfExists = orderParser.parse(window.location.search); this._throttledScreenWidthUpdate = _.throttle(this._updateScreenWidth.bind(this), THROTTLE_TIMEOUT); const didAcceptPortalDisclaimer = localStorage.getItemIfExists(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER); const hasAcceptedDisclaimer = @@ -336,9 +340,14 @@ export class Portal extends React.Component<PortalProps, PortalState> { render: this._renderTradeHistory.bind(this), }, { - pathName: `${WebsitePaths.Portal}/direct`, - headerText: 'Trade Direct', - render: this._renderTradeDirect.bind(this), + pathName: `${WebsitePaths.Portal}/generate`, + headerText: 'Generate Order', + render: this._renderGenerateOrderForm.bind(this), + }, + { + pathName: `${WebsitePaths.Portal}/fill`, + headerText: 'Fill Order', + render: this._renderFillOrder.bind(this), }, ]; return ( @@ -386,7 +395,7 @@ export class Portal extends React.Component<PortalProps, PortalState> { /> ); } - private _renderTradeDirect(match: any, location: Location, history: History): React.ReactNode { + private _renderGenerateOrderForm(): React.ReactNode { return ( <GenerateOrderForm blockchain={this._blockchain} @@ -395,6 +404,25 @@ export class Portal extends React.Component<PortalProps, PortalState> { /> ); } + private _renderFillOrder(): React.ReactNode { + const initialFillOrder = !_.isUndefined(this.props.userSuppliedOrderCache) + ? this.props.userSuppliedOrderCache + : this._sharedOrderIfExists; + return ( + <FillOrder + blockchain={this._blockchain} + blockchainErr={this.props.blockchainErr} + initialOrder={initialFillOrder} + isOrderInUrl={!_.isUndefined(this._sharedOrderIfExists)} + orderFillAmount={this.props.orderFillAmount} + networkId={this.props.networkId} + userAddress={this.props.userAddress} + tokenByAddress={this.props.tokenByAddress} + dispatcher={this.props.dispatcher} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + /> + ); + } private _renderTokenBalances(): React.ReactNode { const allTokens = _.values(this.props.tokenByAddress); const trackedTokens = _.filter(allTokens, t => t.isTracked); diff --git a/packages/website/ts/components/token_balances.tsx b/packages/website/ts/components/token_balances.tsx index f5a51dabb..7a0742bbe 100644 --- a/packages/website/ts/components/token_balances.tsx +++ b/packages/website/ts/components/token_balances.tsx @@ -85,6 +85,9 @@ interface TokenBalancesState { } export class TokenBalances extends React.Component<TokenBalancesProps, TokenBalancesState> { + public static defaultProps: Partial<TokenBalancesProps> = { + userEtherBalanceInWei: new BigNumber(0), + }; private _isUnmounted: boolean; public constructor(props: TokenBalancesProps) { super(props); diff --git a/packages/website/ts/components/top_bar/provider_display.tsx b/packages/website/ts/components/top_bar/provider_display.tsx index 679ec07dc..8a337119a 100644 --- a/packages/website/ts/components/top_bar/provider_display.tsx +++ b/packages/website/ts/components/top_bar/provider_display.tsx @@ -1,5 +1,6 @@ import { Styles } from '@0xproject/react-shared'; import * as _ from 'lodash'; +import CircularProgress from 'material-ui/CircularProgress'; import RaisedButton from 'material-ui/RaisedButton'; import * as React from 'react'; @@ -23,7 +24,8 @@ export interface ProviderDisplayProps { injectedProviderName: string; providerType: ProviderType; onToggleLedgerDialog: () => void; - blockchain: Blockchain; + blockchain?: Blockchain; + blockchainIsLoaded: boolean; } interface ProviderDisplayState {} @@ -44,11 +46,18 @@ export class ProviderDisplay extends React.Component<ProviderDisplayProps, Provi this.props.providerType, this.props.injectedProviderName, ); - const displayAddress = isAddressAvailable - ? utils.getAddressBeginAndEnd(this.props.userAddress) - : isExternallyInjectedProvider - ? 'Account locked' - : '0x0000...0000'; + let displayMessage; + if (!this._isBlockchainReady()) { + displayMessage = 'loading account'; + } else if (isAddressAvailable) { + displayMessage = utils.getAddressBeginAndEnd(this.props.userAddress); + // tslint:disable-next-line: prefer-conditional-expression + } else if (isExternallyInjectedProvider) { + displayMessage = 'Account locked'; + } else { + displayMessage = '0x0000...0000'; + } + // If the "injected" provider is our fallback public node, then we want to // show the "connect a wallet" message instead of the providerName const injectedProviderName = isExternallyInjectedProvider @@ -60,10 +69,14 @@ export class ProviderDisplay extends React.Component<ProviderDisplayProps, Provi const hoverActiveNode = ( <div className="flex right lg-pr0 md-pr2 sm-pr2 p1" style={styles.root}> <div> - <Identicon address={this.props.userAddress} diameter={ROOT_HEIGHT} /> + {this._isBlockchainReady() ? ( + <Identicon address={this.props.userAddress} diameter={ROOT_HEIGHT} /> + ) : ( + <CircularProgress size={ROOT_HEIGHT} thickness={2} /> + )} </div> <div style={{ marginLeft: 12, paddingTop: 3 }}> - <div style={{ fontSize: 16, color: colors.darkGrey }}>{displayAddress}</div> + <div style={{ fontSize: 16, color: colors.darkGrey }}>{displayMessage}</div> </div> {isProviderMetamask && ( <div style={{ marginLeft: 16 }}> @@ -87,7 +100,9 @@ export class ProviderDisplay extends React.Component<ProviderDisplayProps, Provi ); } public renderPopoverContent(hasInjectedProvider: boolean, hasLedgerProvider: boolean): React.ReactNode { - if (hasInjectedProvider || hasLedgerProvider) { + if (!this._isBlockchainReady()) { + return null; + } else if (hasInjectedProvider || hasLedgerProvider) { return ( <ProviderPicker dispatcher={this.props.dispatcher} @@ -159,4 +174,7 @@ export class ProviderDisplay extends React.Component<ProviderDisplayProps, Provi ); } } + private _isBlockchainReady(): boolean { + return this.props.blockchainIsLoaded && !_.isUndefined(this.props.blockchain); + } } diff --git a/packages/website/ts/components/top_bar/top_bar.tsx b/packages/website/ts/components/top_bar/top_bar.tsx index db8e3cb82..e2d791ae3 100644 --- a/packages/website/ts/components/top_bar/top_bar.tsx +++ b/packages/website/ts/components/top_bar/top_bar.tsx @@ -261,7 +261,7 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { </div> </div> )} - {this.props.blockchainIsLoaded && ( + {this._isViewingPortal() && ( <div className="sm-hide xs-hide col col-5" style={{ paddingTop: 8, marginRight: 36 }}> <ProviderDisplay dispatcher={this.props.dispatcher} @@ -271,6 +271,7 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { providerType={this.props.providerType} onToggleLedgerDialog={this.props.onToggleLedgerDialog} blockchain={this.props.blockchain} + blockchainIsLoaded={this.props.blockchainIsLoaded} /> </div> )} diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx index 30d1285f4..18dada22f 100644 --- a/packages/website/ts/components/wallet/wallet.tsx +++ b/packages/website/ts/components/wallet/wallet.tsx @@ -7,6 +7,7 @@ import { import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as _ from 'lodash'; +import CircularProgress from 'material-ui/CircularProgress'; import FlatButton from 'material-ui/FlatButton'; import FloatingActionButton from 'material-ui/FloatingActionButton'; import { ListItem } from 'material-ui/List'; @@ -23,6 +24,7 @@ import firstBy = require('thenby'); import { Blockchain } from 'ts/blockchain'; import { AllowanceToggle } from 'ts/components/inputs/allowance_toggle'; +import { Container } from 'ts/components/ui/container'; import { IconButton } from 'ts/components/ui/icon_button'; import { Identicon } from 'ts/components/ui/identicon'; import { Island } from 'ts/components/ui/island'; @@ -59,7 +61,7 @@ export interface WalletProps { dispatcher: Dispatcher; tokenByAddress: TokenByAddress; trackedTokens: Token[]; - userEtherBalanceInWei: BigNumber; + userEtherBalanceInWei?: BigNumber; lastForceTokenStateRefetch: number; injectedProviderName: string; providerType: ProviderType; @@ -92,9 +94,6 @@ const styles: Styles = { zIndex: zIndex.aboveOverlay, position: 'relative', }, - headerItemInnerDiv: { - paddingLeft: 65, - }, footerItemInnerDiv: { paddingLeft: 24, borderTopColor: colors.walletBorder, @@ -108,6 +107,7 @@ const styles: Styles = { }, tokenItem: { backgroundColor: colors.walletDefaultItemBackground, + minHeight: 85, }, amountLabel: { fontWeight: 'bold', @@ -129,10 +129,13 @@ const styles: Styles = { color: colors.mediumBlue, fontWeight: 'bold', }, + loadingBody: { + height: 381, + }, }; const ETHER_ICON_PATH = '/images/ether.png'; -const ICON_DIMENSION = 24; +const ICON_DIMENSION = 28; const TOKEN_AMOUNT_DISPLAY_PRECISION = 3; const BODY_ITEM_KEY = 'BODY'; const HEADER_ITEM_KEY = 'HEADER'; @@ -191,26 +194,40 @@ export class Wallet extends React.Component<WalletProps, WalletState> { } } public render(): React.ReactNode { - const isReadyToRender = this.props.blockchainIsLoaded && this.props.blockchainErr === BlockchainErrs.NoError; - const isAddressAvailable = !_.isEmpty(this.props.userAddress); + const isBlockchainLoaded = this.props.blockchainIsLoaded && this.props.blockchainErr === BlockchainErrs.NoError; return ( <Island className="flex flex-column wallet" style={styles.root}> - {isReadyToRender && isAddressAvailable - ? _.concat(this._renderConnectedHeaderRows(), this._renderBody(), this._renderFooterRows()) - : _.concat(this._renderDisconnectedHeaderRows(), this._renderDisconnectedRows())} + {isBlockchainLoaded ? this._renderLoadedRows() : this._renderLoadingRows()} </Island> ); } + private _renderLoadedRows(): React.ReactNode { + const isAddressAvailable = !_.isEmpty(this.props.userAddress); + return isAddressAvailable + ? _.concat(this._renderConnectedHeaderRows(), this._renderBody(), this._renderFooterRows()) + : _.concat(this._renderDisconnectedHeaderRows(), this._renderDisconnectedRows()); + } + private _renderLoadingRows(): React.ReactNode { + return _.concat(this._renderDisconnectedHeaderRows(), this._renderLoadingBodyRows()); + } + private _renderLoadingBodyRows(): React.ReactElement<{}> { + return ( + <div key={BODY_ITEM_KEY} className="flex items-center" style={styles.loadingBody}> + <div className="mx-auto"> + <CircularProgress size={40} thickness={5} /> + </div> + </div> + ); + } private _renderDisconnectedHeaderRows(): React.ReactElement<{}> { const userAddress = this.props.userAddress; const primaryText = 'wallet'; return ( - <ListItem + <StandardIconRow key={HEADER_ITEM_KEY} - primaryText={primaryText.toUpperCase()} - leftIcon={<ActionAccountBalanceWallet color={colors.mediumBlue} />} - style={styles.paddedItem} - innerDivStyle={styles.headerItemInnerDiv} + icon={<ActionAccountBalanceWallet color={colors.mediumBlue} />} + main={primaryText.toUpperCase()} + style={styles.borderedItem} /> ); } @@ -229,11 +246,10 @@ export class Wallet extends React.Component<WalletProps, WalletState> { const primaryText = utils.getAddressBeginAndEnd(userAddress); return ( <Link key={HEADER_ITEM_KEY} to={ACCOUNT_PATH} style={{ textDecoration: 'none' }}> - <ListItem - primaryText={primaryText} - leftIcon={<Identicon address={userAddress} diameter={ICON_DIMENSION} />} - style={{ ...styles.paddedItem, ...styles.borderedItem }} - innerDivStyle={styles.headerItemInnerDiv} + <StandardIconRow + icon={<Identicon address={userAddress} diameter={ICON_DIMENSION} />} + main={primaryText} + style={styles.borderedItem} /> </Link> ); @@ -320,26 +336,23 @@ export class Wallet extends React.Component<WalletProps, WalletState> { private _renderEthRows(): React.ReactNode { const icon = <img style={{ width: ICON_DIMENSION, height: ICON_DIMENSION }} src={ETHER_ICON_PATH} />; const primaryText = this._renderAmount( - this.props.userEtherBalanceInWei, + this.props.userEtherBalanceInWei || new BigNumber(0), constants.DECIMAL_PLACES_ETH, constants.ETHER_SYMBOL, + _.isUndefined(this.props.userEtherBalanceInWei), ); const etherToken = this._getEthToken(); - const etherPrice = this.state.trackedTokenStateByAddress[etherToken.address].price; + const etherTokenState = this.state.trackedTokenStateByAddress[etherToken.address]; + const etherPrice = etherTokenState.price; const secondaryText = this._renderValue( - this.props.userEtherBalanceInWei, + this.props.userEtherBalanceInWei || new BigNumber(0), constants.DECIMAL_PLACES_ETH, etherPrice, + _.isUndefined(this.props.userEtherBalanceInWei) || !etherTokenState.isLoaded, ); const accessoryItemConfig = { wrappedEtherDirection: Side.Deposit, }; - const isInWrappedEtherState = - !_.isUndefined(this.state.wrappedEtherDirection) && - this.state.wrappedEtherDirection === accessoryItemConfig.wrappedEtherDirection; - const style = isInWrappedEtherState - ? { ...walletItemStyles.focusedItem, ...styles.paddedItem } - : { ...styles.tokenItem, ...styles.borderedItem, ...styles.paddedItem }; const key = ETHER_ITEM_KEY; return this._renderBalanceRow(key, icon, primaryText, secondaryText, accessoryItemConfig, 'eth-row'); } @@ -360,10 +373,15 @@ export class Wallet extends React.Component<WalletProps, WalletState> { EtherscanLinkSuffixes.Address, ); const icon = <TokenIcon token={token} diameter={ICON_DIMENSION} link={tokenLink} />; - const primaryText = this._renderAmount(tokenState.balance, token.decimals, token.symbol); - const secondaryText = this._renderValue(tokenState.balance, token.decimals, tokenState.price); const isWeth = token.symbol === constants.ETHER_TOKEN_SYMBOL; const wrappedEtherDirection = isWeth ? Side.Receive : undefined; + const primaryText = this._renderAmount(tokenState.balance, token.decimals, token.symbol, !tokenState.isLoaded); + const secondaryText = this._renderValue( + tokenState.balance, + token.decimals, + tokenState.price, + !tokenState.isLoaded, + ); const accessoryItemConfig: AccessoryItemConfig = { wrappedEtherDirection, allowanceToggleConfig: { @@ -391,22 +409,24 @@ export class Wallet extends React.Component<WalletProps, WalletState> { ): React.ReactNode { const shouldShowWrapEtherItem = !_.isUndefined(this.state.wrappedEtherDirection) && - this.state.wrappedEtherDirection === accessoryItemConfig.wrappedEtherDirection; - const style = shouldShowWrapEtherItem - ? { ...walletItemStyles.focusedItem, ...styles.paddedItem } - : { ...styles.tokenItem, ...styles.borderedItem, ...styles.paddedItem }; + this.state.wrappedEtherDirection === accessoryItemConfig.wrappedEtherDirection && + !_.isUndefined(this.props.userEtherBalanceInWei); + const additionalStyle = shouldShowWrapEtherItem ? walletItemStyles.focusedItem : styles.borderedItem; + const style = { ...styles.tokenItem, ...additionalStyle }; const etherToken = this._getEthToken(); return ( <div key={key} className={`flex flex-column ${className || ''}`}> - <div className="flex items-center" style={style}> - <div className="px2">{icon}</div> - <div className="flex-none pr2 pt2 pb2"> - {primaryText} - {secondaryText} - </div> - <div className="flex-auto" /> - <div>{this._renderAccessoryItems(accessoryItemConfig)}</div> - </div> + <StandardIconRow + icon={icon} + main={ + <div className="flex flex-column"> + {primaryText} + <Container marginTop="3px">{secondaryText}</Container> + </div> + } + accessory={this._renderAccessoryItems(accessoryItemConfig)} + style={style} + /> {shouldShowWrapEtherItem && ( <WrapEtherItem userAddress={this.props.userAddress} @@ -458,21 +478,45 @@ export class Wallet extends React.Component<WalletProps, WalletState> { /> ); } - private _renderAmount(amount: BigNumber, decimals: number, symbol: string): React.ReactNode { + private _renderAmount( + amount: BigNumber, + decimals: number, + symbol: string, + isLoading: boolean = false, + ): React.ReactNode { const unitAmount = Web3Wrapper.toUnitAmount(amount, decimals); const formattedAmount = unitAmount.toPrecision(TOKEN_AMOUNT_DISPLAY_PRECISION); const result = `${formattedAmount} ${symbol}`; - return <div style={styles.amountLabel}>{result}</div>; + return ( + <PlaceHolder hideChildren={isLoading}> + <div style={styles.amountLabel}>{result}</div> + </PlaceHolder> + ); } - private _renderValue(amount: BigNumber, decimals: number, price?: BigNumber): React.ReactNode { - if (_.isUndefined(price)) { - return null; + private _renderValue( + amount: BigNumber, + decimals: number, + price?: BigNumber, + isLoading: boolean = false, + ): React.ReactNode { + let result; + if (!isLoading) { + if (_.isUndefined(price)) { + result = '--'; + } else { + const unitAmount = Web3Wrapper.toUnitAmount(amount, decimals); + const value = unitAmount.mul(price); + const formattedAmount = value.toFixed(USD_DECIMAL_PLACES); + result = `$${formattedAmount}`; + } + } else { + result = '$0.00'; } - const unitAmount = Web3Wrapper.toUnitAmount(amount, decimals); - const value = unitAmount.mul(price); - const formattedAmount = value.toFixed(USD_DECIMAL_PLACES); - const result = `$${formattedAmount}`; - return <div style={styles.valueLabel}>{result}</div>; + return ( + <PlaceHolder hideChildren={isLoading}> + <div style={styles.valueLabel}>{result}</div> + </PlaceHolder> + ); } private _renderWrappedEtherButton(wrappedEtherDirection: Side): React.ReactNode { const isWrappedEtherDirectionOpen = this.state.wrappedEtherDirection === wrappedEtherDirection; @@ -589,4 +633,41 @@ export class Wallet extends React.Component<WalletProps, WalletState> { private _getEthToken(): Token { return utils.getEthToken(this.props.tokenByAddress); } -} // tslint:disable:max-file-line-count +} + +interface StandardIconRowProps { + icon: React.ReactNode; + main: React.ReactNode; + accessory?: React.ReactNode; + style?: React.CSSProperties; +} +const StandardIconRow = (props: StandardIconRowProps) => { + return ( + <div className="flex items-center" style={props.style}> + <div className="p2">{props.icon}</div> + <div className="flex-none pr2 pt2 pb2">{props.main}</div> + <div className="flex-auto" /> + <div>{props.accessory}</div> + </div> + ); +}; +interface PlaceHolderProps { + hideChildren: React.ReactNode; + children?: React.ReactNode; +} +const PlaceHolder = (props: PlaceHolderProps) => { + const rootBackgroundColor = props.hideChildren ? colors.lightGrey : 'transparent'; + const rootStyle: React.CSSProperties = { + backgroundColor: rootBackgroundColor, + display: 'inline-block', + borderRadius: 2, + }; + const childrenVisibility = props.hideChildren ? 'hidden' : 'visible'; + const childrenStyle: React.CSSProperties = { visibility: childrenVisibility }; + return ( + <div style={rootStyle}> + <div style={childrenStyle}>{props.children}</div> + </div> + ); +}; +// tslint:disable:max-file-line-count diff --git a/packages/website/ts/components/wallet/wallet_disconnected_item.tsx b/packages/website/ts/components/wallet/wallet_disconnected_item.tsx index 39a62e1fb..17fd8a19e 100644 --- a/packages/website/ts/components/wallet/wallet_disconnected_item.tsx +++ b/packages/website/ts/components/wallet/wallet_disconnected_item.tsx @@ -31,7 +31,7 @@ const styles: Styles = { }, }; -const ITEM_HEIGHT = 292; +const ITEM_HEIGHT = 381; const METAMASK_ICON_WIDTH = 35; const LEDGER_ICON_WIDTH = 30; const BUTTON_BOTTOM_PADDING = 80; diff --git a/packages/website/ts/containers/legacy_portal.ts b/packages/website/ts/containers/legacy_portal.ts index 3b1172a44..eae450c21 100644 --- a/packages/website/ts/containers/legacy_portal.ts +++ b/packages/website/ts/containers/legacy_portal.ts @@ -24,7 +24,7 @@ interface ConnectedState { providerType: ProviderType; tokenByAddress: TokenByAddress; lastForceTokenStateRefetch: number; - userEtherBalanceInWei: BigNumber; + userEtherBalanceInWei?: BigNumber; screenWidth: ScreenWidths; shouldBlockchainErrDialogBeOpen: boolean; userAddress: string; diff --git a/packages/website/ts/containers/portal.ts b/packages/website/ts/containers/portal.ts index 3f0feb6e9..b8c8fb999 100644 --- a/packages/website/ts/containers/portal.ts +++ b/packages/website/ts/containers/portal.ts @@ -21,7 +21,7 @@ interface ConnectedState { providerType: ProviderType; tokenByAddress: TokenByAddress; lastForceTokenStateRefetch: number; - userEtherBalanceInWei: BigNumber; + userEtherBalanceInWei?: BigNumber; screenWidth: ScreenWidths; shouldBlockchainErrDialogBeOpen: boolean; userAddress: string; diff --git a/packages/website/ts/containers/portal_onboarding_flow.ts b/packages/website/ts/containers/portal_onboarding_flow.ts index 84739192f..8202fb2ae 100644 --- a/packages/website/ts/containers/portal_onboarding_flow.ts +++ b/packages/website/ts/containers/portal_onboarding_flow.ts @@ -17,7 +17,7 @@ interface ConnectedState { providerType: ProviderType; injectedProviderName: string; blockchainIsLoaded: boolean; - userEthBalanceInWei: BigNumber; + userEtherBalanceInWei?: BigNumber; tokenByAddress: TokenByAddress; } @@ -33,7 +33,7 @@ const mapStateToProps = (state: State): ConnectedState => ({ providerType: state.providerType, injectedProviderName: state.injectedProviderName, blockchainIsLoaded: state.blockchainIsLoaded, - userEthBalanceInWei: state.userEtherBalanceInWei, + userEtherBalanceInWei: state.userEtherBalanceInWei, tokenByAddress: state.tokenByAddress, hasBeenSeen: state.hasPortalOnboardingBeenSeen, }); diff --git a/packages/website/ts/containers/sol_cov_documentation.ts b/packages/website/ts/containers/sol_cov_documentation.ts index 58755c1e0..bc05b6854 100644 --- a/packages/website/ts/containers/sol_cov_documentation.ts +++ b/packages/website/ts/containers/sol_cov_documentation.ts @@ -22,6 +22,9 @@ const docSections = { installation: 'installation', usage: 'usage', coverageSubprovider: 'coverageSubprovider', + abstractArtifactAdapter: 'abstractArtifactAdapter', + solCompilerArtifactAdapter: 'solCompilerArtifactAdapter', + truffleArtifactAdapter: 'truffleArtifactAdapter', types: docConstants.TYPES_SECTION_NAME, }; @@ -34,7 +37,10 @@ const docsInfoConfig: DocsInfoConfig = { introduction: [docSections.introduction], install: [docSections.installation], usage: [docSections.usage], - coverageSubprovider: [docSections.coverageSubprovider], + 'coverage-subprovider': [docSections.coverageSubprovider], + 'abstract-artifact-adapter': [docSections.abstractArtifactAdapter], + 'sol-compiler-artifact-adapter': [docSections.solCompilerArtifactAdapter], + 'truffle-artifact-adapter': [docSections.truffleArtifactAdapter], types: [docSections.types], }, sectionNameToMarkdown: { @@ -44,18 +50,40 @@ const docsInfoConfig: DocsInfoConfig = { }, sectionNameToModulePath: { [docSections.coverageSubprovider]: ['"sol-cov/src/coverage_subprovider"'], + [docSections.abstractArtifactAdapter]: ['"sol-cov/src/artifact_adapters/abstract_artifact_adapter"'], + [docSections.solCompilerArtifactAdapter]: ['"sol-cov/src/artifact_adapters/sol_compiler_artifact_adapter"'], + [docSections.truffleArtifactAdapter]: ['"sol-cov/src/artifact_adapters/truffle_artifact_adapter"'], [docSections.types]: ['"subproviders/src/types"', '"types/src/index"'], }, menuSubsectionToVersionWhenIntroduced: {}, sections: docSections, - visibleConstructors: [docSections.coverageSubprovider], + visibleConstructors: [ + docSections.coverageSubprovider, + docSections.abstractArtifactAdapter, + docSections.solCompilerArtifactAdapter, + docSections.truffleArtifactAdapter, + ], typeConfigs: { // Note: This needs to be kept in sync with the types exported in index.ts. Unfortunately there is // currently no way to extract the re-exported types from index.ts via TypeDoc :( - publicTypes: ['NextCallback', 'OnNextCompleted', 'ErrorCallback', 'JSONRPCRequestPayload'], + publicTypes: [ + 'JSONRPCRequestPayload', + 'NextCallback', + 'ErrorCallback', + 'AbstractArtifactAdapter', + 'CoverageSubprovider', + 'TruffleArtifactAdapter', + 'SolCompilerArtifactAdapter', + 'ContractData', + ], typeNameToExternalLink: {}, typeNameToPrefix: {}, - typeNameToDocSection: {}, + typeNameToDocSection: { + AbstractArtifactAdapter: docSections.abstractArtifactAdapter, + CoverageSubprovider: docSections.coverageSubprovider, + TruffleArtifactAdapter: docSections.truffleArtifactAdapter, + SolCompilerArtifactAdapter: docSections.solCompilerArtifactAdapter, + }, }, }; const docsInfo = new DocsInfo(docsInfoConfig); diff --git a/packages/website/ts/redux/dispatcher.ts b/packages/website/ts/redux/dispatcher.ts index 0b4cc3938..e0ce43ae5 100644 --- a/packages/website/ts/redux/dispatcher.ts +++ b/packages/website/ts/redux/dispatcher.ts @@ -155,7 +155,7 @@ export class Dispatcher { type: ActionTypes.UpdateOrderECSignature, }); } - public updateUserWeiBalance(balance: BigNumber): void { + public updateUserWeiBalance(balance?: BigNumber): void { this._dispatch({ data: balance, type: ActionTypes.UpdateUserEtherBalance, diff --git a/packages/website/ts/redux/reducer.ts b/packages/website/ts/redux/reducer.ts index 5c57792f7..9d3d8f7d9 100644 --- a/packages/website/ts/redux/reducer.ts +++ b/packages/website/ts/redux/reducer.ts @@ -39,7 +39,7 @@ export interface State { tokenByAddress: TokenByAddress; lastForceTokenStateRefetch: number; userAddress: string; - userEtherBalanceInWei: BigNumber; + userEtherBalanceInWei?: BigNumber; portalOnboardingStep: number; isPortalOnboardingShowing: boolean; hasPortalOnboardingBeenSeen: boolean; @@ -81,7 +81,7 @@ export const INITIAL_STATE: State = { tokenByAddress: {}, lastForceTokenStateRefetch: moment().unix(), userAddress: '', - userEtherBalanceInWei: new BigNumber(0), + userEtherBalanceInWei: undefined, userSuppliedOrderCache: undefined, portalOnboardingStep: 0, isPortalOnboardingShowing: false, diff --git a/packages/website/ts/utils/order_parser.ts b/packages/website/ts/utils/order_parser.ts new file mode 100644 index 000000000..be08da80e --- /dev/null +++ b/packages/website/ts/utils/order_parser.ts @@ -0,0 +1,33 @@ +import { logUtils } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { portalOrderSchema } from 'ts/schemas/portal_order_schema'; +import { validator } from 'ts/schemas/validator'; +import { Order } from 'ts/types'; + +export const orderParser = { + parse(queryString: string): Order | undefined { + if (queryString.length === 0) { + return undefined; + } + const queryParams = queryString.substring(1).split('&'); + const orderQueryParam = _.find(queryParams, queryParam => { + const queryPair = queryParam.split('='); + return queryPair[0] === 'order'; + }); + if (_.isUndefined(orderQueryParam)) { + return undefined; + } + const orderPair = orderQueryParam.split('='); + if (orderPair.length !== 2) { + return undefined; + } + const order = JSON.parse(decodeURIComponent(orderPair[1])); + const validationResult = validator.validate(order, portalOrderSchema); + if (validationResult.errors.length > 0) { + logUtils.log(`Invalid shared order: ${validationResult.errors}`); + return undefined; + } + return order; + }, +}; |