Merge branch 'development' of https://github.com/0xProject/0x-monorepo into feature/instant/dropdown-ui

This commit is contained in:
fragosti
2018-11-08 11:25:25 -08:00
38 changed files with 690 additions and 497 deletions

View File

@@ -31,7 +31,7 @@ jobs:
- restore_cache:
keys:
- repo-{{ .Environment.CIRCLE_SHA1 }}
- run: cd packages/website && yarn build
- run: cd packages/website && yarn build:prod
test-contracts-ganache:
docker:
- image: circleci/node:9

View File

@@ -50,7 +50,7 @@
},
{
"path": "packages/instant/public/main.bundle.js",
"maxSize": "500kB"
"maxSize": "1000kB"
}
],
"ci": {

View File

@@ -49,6 +49,7 @@
"@0x/asset-buyer": "^2.1.0",
"@0x/json-schemas": "^2.0.0",
"@0x/order-utils": "^2.0.0",
"@0x/subproviders": "^2.1.0",
"@0x/types": "^1.2.0",
"@0x/typescript-typings": "^3.0.3",
"@0x/utils": "^2.0.3",

View File

@@ -0,0 +1,25 @@
/*
CSS file meant to represent an external (integrators) stylesheet and
help ensure that instant looks consistent across environments.
*/
button {
font-size: 50px;
height: 200px;
background-color: red;
}
input {
padding: 100px;
font-size: 50px;
height: 100px;
}
div {
padding: 3px;
}
p {
background-color: green;
margin: 10px;
}

View File

@@ -5,6 +5,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>0x Instant Dev Environment</title>
<link rel="stylesheet" href="/external.css">
<script type="text/javascript" src="/main.bundle.js" charset="utf-8"></script>
<script type="text/javascript" src="https://unpkg.com/jsuri@1.3.1/Uri.js" charset="utf-8"></script>
<script type="text/javascript" src="https://unpkg.com/bignumber.js@4.1.0/bignumber.js" charset="utf-8"></script>

View File

@@ -13,11 +13,10 @@ import { gasPriceEstimator } from '../util/gas_price_estimator';
import { util } from '../util/util';
import { Button } from './ui/button';
import { Text } from './ui/text';
export interface BuyButtonProps {
buyQuote?: BuyQuote;
assetBuyer?: AssetBuyer;
assetBuyer: AssetBuyer;
affiliateInfo?: AffiliateInfo;
onValidationPending: (buyQuote: BuyQuote) => void;
onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void;
@@ -34,23 +33,29 @@ export class BuyButton extends React.Component<BuyButtonProps> {
onBuyFailure: util.boundNoop,
};
public render(): React.ReactNode {
const shouldDisableButton = _.isUndefined(this.props.buyQuote) || _.isUndefined(this.props.assetBuyer);
const shouldDisableButton = _.isUndefined(this.props.buyQuote);
return (
<Button width="100%" onClick={this._handleClick} isDisabled={shouldDisableButton}>
<Text fontColor={ColorOption.white} fontWeight={600} fontSize="20px">
Buy
</Text>
<Button
width="100%"
onClick={this._handleClick}
isDisabled={shouldDisableButton}
fontColor={ColorOption.white}
fontSize="20px"
>
Buy
</Button>
);
}
private readonly _handleClick = async () => {
// The button is disabled when there is no buy quote anyway.
const { buyQuote, assetBuyer, affiliateInfo } = this.props;
if (_.isUndefined(buyQuote) || _.isUndefined(assetBuyer)) {
if (_.isUndefined(buyQuote)) {
return;
}
this.props.onValidationPending(buyQuote);
// TODO(bmillman): move address and balance fetching to the async state
const web3Wrapper = new Web3Wrapper(assetBuyer.provider);
const takerAddress = await getBestAddress(web3Wrapper);

View File

@@ -14,12 +14,12 @@ export const BuyOrderProgress: React.StatelessComponent<BuyOrderProgressProps> =
const { buyOrderState } = props;
if (
buyOrderState.processState === OrderProcessState.PROCESSING ||
buyOrderState.processState === OrderProcessState.SUCCESS ||
buyOrderState.processState === OrderProcessState.FAILURE
buyOrderState.processState === OrderProcessState.Processing ||
buyOrderState.processState === OrderProcessState.Success ||
buyOrderState.processState === OrderProcessState.Failure
) {
const progress = buyOrderState.progress;
const hasEnded = buyOrderState.processState !== OrderProcessState.PROCESSING;
const hasEnded = buyOrderState.processState !== OrderProcessState.Processing;
const expectedTimeMs = progress.expectedEndTimeUnix - progress.startTimeUnix;
return (
<Container padding="20px 20px 0px 20px" width="100%">

View File

@@ -10,12 +10,11 @@ import { SecondaryButton } from './secondary_button';
import { Button } from './ui/button';
import { Flex } from './ui/flex';
import { Text } from './ui/text';
export interface BuyOrderStateButtonProps {
buyQuote?: BuyQuote;
buyOrderProcessingState: OrderProcessState;
assetBuyer?: AssetBuyer;
assetBuyer: AssetBuyer;
affiliateInfo?: AffiliateInfo;
onViewTransaction: () => void;
onValidationPending: (buyQuote: BuyQuote) => void;
@@ -28,13 +27,11 @@ export interface BuyOrderStateButtonProps {
}
export const BuyOrderStateButtons: React.StatelessComponent<BuyOrderStateButtonProps> = props => {
if (props.buyOrderProcessingState === OrderProcessState.FAILURE) {
if (props.buyOrderProcessingState === OrderProcessState.Failure) {
return (
<Flex justify="space-between">
<Button width="48%" onClick={props.onRetry}>
<Text fontColor={ColorOption.white} fontWeight={600} fontSize="16px">
Back
</Text>
<Button width="48%" onClick={props.onRetry} fontColor={ColorOption.white} fontSize="16px">
Back
</Button>
<SecondaryButton width="48%" onClick={props.onViewTransaction}>
Details
@@ -42,11 +39,11 @@ export const BuyOrderStateButtons: React.StatelessComponent<BuyOrderStateButtonP
</Flex>
);
} else if (
props.buyOrderProcessingState === OrderProcessState.SUCCESS ||
props.buyOrderProcessingState === OrderProcessState.PROCESSING
props.buyOrderProcessingState === OrderProcessState.Success ||
props.buyOrderProcessingState === OrderProcessState.Processing
) {
return <SecondaryButton onClick={props.onViewTransaction}>View Transaction</SecondaryButton>;
} else if (props.buyOrderProcessingState === OrderProcessState.VALIDATING) {
} else if (props.buyOrderProcessingState === OrderProcessState.Validating) {
return <PlacingOrderButton />;
}

View File

@@ -0,0 +1,33 @@
import { INJECTED_DIV_CLASS } from '../constants';
import { createGlobalStyle } from '../style/theme';
export interface CSSResetProps {}
/*
* Derived from
* https://github.com/jtrost/Complete-CSS-Reset
*/
export const CSSReset = createGlobalStyle`
.${INJECTED_DIV_CLASS} {
a, abbr, area, article, aside, audio, b, bdo, blockquote, body, button,
canvas, caption, cite, code, col, colgroup, command, datalist, dd, del,
details, dialog, dfn, div, dl, dt, em, embed, fieldset, figure, form,
h1, h2, h3, h4, h5, h6, head, header, hgroup, hr, html, i, iframe, img,
input, ins, keygen, kbd, label, legend, li, map, mark, menu, meter, nav,
noscript, object, ol, optgroup, option, output, p, param, pre, progress,
q, rp, rt, ruby, samp, section, select, small, span, strong, sub, sup,
table, tbody, td, textarea, tfoot, th, thead, time, tr, ul, var, video {
background: transparent;
border: 0;
font-size: 100%;
font: inherit;
margin: 0;
outline: none;
padding: 0;
text-align: left;
text-decoration: none;
vertical-align: baseline;
z-index: 1;
}
}
`;

View File

@@ -77,11 +77,11 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> {
private _renderIcon(): React.ReactNode {
const processState = this.props.buyOrderState.processState;
if (processState === OrderProcessState.FAILURE) {
if (processState === OrderProcessState.Failure) {
return <Icon icon="failed" width={ICON_WIDTH} height={ICON_HEIGHT} color={ICON_COLOR} />;
} else if (processState === OrderProcessState.PROCESSING) {
} else if (processState === OrderProcessState.Processing) {
return <Spinner widthPx={ICON_HEIGHT} heightPx={ICON_HEIGHT} />;
} else if (processState === OrderProcessState.SUCCESS) {
} else if (processState === OrderProcessState.Success) {
return <Icon icon="success" width={ICON_WIDTH} height={ICON_HEIGHT} color={ICON_COLOR} />;
}
return undefined;
@@ -89,11 +89,11 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> {
private _renderTopText(): React.ReactNode {
const processState = this.props.buyOrderState.processState;
if (processState === OrderProcessState.FAILURE) {
if (processState === OrderProcessState.Failure) {
return 'Order failed';
} else if (processState === OrderProcessState.PROCESSING) {
} else if (processState === OrderProcessState.Processing) {
return 'Processing Order...';
} else if (processState === OrderProcessState.SUCCESS) {
} else if (processState === OrderProcessState.Success) {
return 'Tokens received!';
}
@@ -101,7 +101,7 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> {
}
private _renderPlaceholderOrAmount(amountFunction: () => React.ReactNode): React.ReactNode {
if (this.props.quoteRequestState === AsyncProcessState.PENDING) {
if (this.props.quoteRequestState === AsyncProcessState.Pending) {
return <AmountPlaceholder isPulsating={true} color={PLACEHOLDER_COLOR} />;
}
if (_.isUndefined(this.props.selectedAssetAmount)) {

View File

@@ -5,15 +5,12 @@ import { ColorOption } from '../style/theme';
import { Button } from './ui/button';
import { Container } from './ui/container';
import { Spinner } from './ui/spinner';
import { Text } from './ui/text';
export const PlacingOrderButton: React.StatelessComponent<{}> = props => (
<Button isDisabled={true} width="100%">
<Button isDisabled={true} width="100%" fontColor={ColorOption.white} fontSize="20px">
<Container display="inline-block" position="relative" top="3px" marginRight="8px">
<Spinner widthPx={20} heightPx={20} />
</Container>
<Text fontColor={ColorOption.white} fontWeight={600} fontSize="20px">
Placing Order&hellip;
</Text>
Placing Order&hellip;
</Button>
);

View File

@@ -4,7 +4,6 @@ import * as React from 'react';
import { ColorOption } from '../style/theme';
import { Button, ButtonProps } from './ui/button';
import { Text } from './ui/text';
export interface SecondaryButtonProps extends ButtonProps {}
@@ -16,11 +15,11 @@ export const SecondaryButton: React.StatelessComponent<SecondaryButtonProps> = p
borderColor={ColorOption.lightGrey}
width={props.width}
onClick={props.onClick}
fontColor={ColorOption.primaryColor}
fontSize="16px"
{...buttonProps}
>
<Text fontColor={ColorOption.primaryColor} fontWeight={600} fontSize="16px">
{props.children}
</Text>
{props.children}
</Button>
);
};

View File

@@ -70,9 +70,11 @@ export const TimedProgress =
styled.div <
TimedProgressProps >
`
background-color: ${props => props.theme[ColorOption.primaryColor]};
border-radius: 6px;
height: 6px;
animation: ${props => expandingWidthKeyframes(props.fromWidth, props.toWidth)}
${props => props.timeMs}ms linear 1 forwards;
`;
&& {
background-color: ${props => props.theme[ColorOption.primaryColor]};
border-radius: 6px;
height: 6px;
animation: ${props => expandingWidthKeyframes(props.fromWidth, props.toWidth)}
${props => props.timeMs}ms linear 1 forwards;
}
`;

View File

@@ -6,6 +6,8 @@ import { ColorOption, styled } from '../../style/theme';
export interface ButtonProps {
backgroundColor?: ColorOption;
borderColor?: ColorOption;
fontColor?: ColorOption;
fontSize?: string;
width?: string;
padding?: string;
type?: string;
@@ -24,29 +26,39 @@ const darkenOnHoverAmount = 0.1;
const darkenOnActiveAmount = 0.2;
const saturateOnFocusAmount = 0.2;
export const Button = styled(PlainButton)`
cursor: ${props => (props.isDisabled ? 'default' : 'pointer')};
transition: background-color, opacity 0.5s ease;
padding: ${props => props.padding};
border-radius: 3px;
outline: none;
width: ${props => props.width};
background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')};
border: ${props => (props.borderColor ? `1px solid ${props.theme[props.borderColor]}` : 'none')};
&:hover {
background-color: ${props =>
!props.isDisabled
? darken(darkenOnHoverAmount, props.theme[props.backgroundColor || 'white'])
: ''} !important;
}
&:active {
background-color: ${props =>
!props.isDisabled ? darken(darkenOnActiveAmount, props.theme[props.backgroundColor || 'white']) : ''};
}
&:disabled {
opacity: 0.5;
}
&:focus {
background-color: ${props => saturate(saturateOnFocusAmount, props.theme[props.backgroundColor || 'white'])};
&& {
all: initial;
box-sizing: border-box;
font-size: ${props => props.fontSize};
font-family: 'Inter UI', sans-serif;
font-weight: 600;
color: ${props => props.fontColor && props.theme[props.fontColor]};
cursor: ${props => (props.isDisabled ? 'default' : 'pointer')};
transition: background-color, opacity 0.5s ease;
padding: ${props => props.padding};
border-radius: 3px;
text-align: center;
outline: none;
width: ${props => props.width};
background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')};
border: ${props => (props.borderColor ? `1px solid ${props.theme[props.borderColor]}` : 'none')};
&:hover {
background-color: ${props =>
!props.isDisabled
? darken(darkenOnHoverAmount, props.theme[props.backgroundColor || 'white'])
: ''} !important;
}
&:active {
background-color: ${props =>
!props.isDisabled ? darken(darkenOnActiveAmount, props.theme[props.backgroundColor || 'white']) : ''};
}
&:disabled {
opacity: 0.5;
}
&:focus {
background-color: ${props =>
saturate(saturateOnFocusAmount, props.theme[props.backgroundColor || 'white'])};
}
}
`;
@@ -55,7 +67,8 @@ Button.defaultProps = {
borderColor: ColorOption.primaryColor,
width: 'auto',
isDisabled: false,
padding: '1em 2.2em',
padding: '.6em 1.2em',
fontSize: '15px',
};
Button.displayName = 'Button';

View File

@@ -11,10 +11,12 @@ export const Circle = withTheme(
styled.div <
CircleProps >
`
width: ${props => props.diameter}px;
height: ${props => props.diameter}px;
background-color: ${props => (props.rawColor ? props.rawColor : props.theme[props.color || ColorOption.white])};
border-radius: 50%;
&& {
width: ${props => props.diameter}px;
height: ${props => props.diameter}px;
background-color: ${props => (props.rawColor ? props.rawColor : props.theme[props.color || ColorOption.white])};
border-radius: 50%;
}
`,
);

View File

@@ -41,42 +41,45 @@ export const Container =
styled.div <
ContainerProps >
`
box-sizing: border-box;
${props => cssRuleIfExists(props, 'flex-grow')}
${props => cssRuleIfExists(props, 'position')}
${props => cssRuleIfExists(props, 'top')}
${props => cssRuleIfExists(props, 'right')}
${props => cssRuleIfExists(props, 'bottom')}
${props => cssRuleIfExists(props, 'left')}
${props => cssRuleIfExists(props, 'max-width')}
${props => cssRuleIfExists(props, 'margin')}
${props => cssRuleIfExists(props, 'margin-top')}
${props => cssRuleIfExists(props, 'margin-right')}
${props => cssRuleIfExists(props, 'margin-bottom')}
${props => cssRuleIfExists(props, 'margin-left')}
${props => cssRuleIfExists(props, 'padding')}
${props => cssRuleIfExists(props, 'border-radius')}
${props => cssRuleIfExists(props, 'border')}
${props => cssRuleIfExists(props, 'border-top')}
${props => cssRuleIfExists(props, 'border-bottom')}
${props => cssRuleIfExists(props, 'z-index')}
${props => cssRuleIfExists(props, 'white-space')}
${props => cssRuleIfExists(props, 'opacity')}
${props => cssRuleIfExists(props, 'cursor')}
${props => cssRuleIfExists(props, 'overflow')}
${props => (props.hasBoxShadow ? `box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)` : '')};
${props => props.display && stylesForMedia('display', props.display)}
${props => stylesForMedia('width', props.width || 'auto')}
${props => stylesForMedia('height', props.height || 'auto')}
background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')};
border-color: ${props => (props.borderColor ? props.theme[props.borderColor] : 'none')};
&:hover {
${props =>
props.darkenOnHover
? `background-color: ${
props.backgroundColor ? darken(0.05, props.theme[props.backgroundColor]) : 'none'
}`
: ''};
&& {
all: initial;
box-sizing: border-box;
${props => cssRuleIfExists(props, 'flex-grow')}
${props => cssRuleIfExists(props, 'position')}
${props => cssRuleIfExists(props, 'top')}
${props => cssRuleIfExists(props, 'right')}
${props => cssRuleIfExists(props, 'bottom')}
${props => cssRuleIfExists(props, 'left')}
${props => cssRuleIfExists(props, 'max-width')}
${props => cssRuleIfExists(props, 'margin')}
${props => cssRuleIfExists(props, 'margin-top')}
${props => cssRuleIfExists(props, 'margin-right')}
${props => cssRuleIfExists(props, 'margin-bottom')}
${props => cssRuleIfExists(props, 'margin-left')}
${props => cssRuleIfExists(props, 'padding')}
${props => cssRuleIfExists(props, 'border-radius')}
${props => cssRuleIfExists(props, 'border')}
${props => cssRuleIfExists(props, 'border-top')}
${props => cssRuleIfExists(props, 'border-bottom')}
${props => cssRuleIfExists(props, 'z-index')}
${props => cssRuleIfExists(props, 'white-space')}
${props => cssRuleIfExists(props, 'opacity')}
${props => cssRuleIfExists(props, 'cursor')}
${props => cssRuleIfExists(props, 'overflow')}
${props => (props.hasBoxShadow ? `box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)` : '')};
${props => props.display && stylesForMedia('display', props.display)}
${props => (props.width ? stylesForMedia('width', props.width) : '')}
${props => (props.height ? stylesForMedia('height', props.height) : '')}
background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')};
border-color: ${props => (props.borderColor ? props.theme[props.borderColor] : 'none')};
&:hover {
${props =>
props.darkenOnHover
? `background-color: ${
props.backgroundColor ? darken(0.05, props.theme[props.backgroundColor]) : 'none'
}`
: ''};
}
}
`;

View File

@@ -18,15 +18,18 @@ export const Flex =
styled.div <
FlexProps >
`
display: ${props => (props.inline ? 'inline-flex' : 'flex')};
flex-direction: ${props => props.direction};
flex-wrap: ${props => props.flexWrap};
${props => cssRuleIfExists(props, 'flexGrow')}
justify-content: ${props => props.justify};
align-items: ${props => props.align};
background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')};
${props => stylesForMedia('width', props.width || 'auto')}
${props => stylesForMedia('height', props.height || 'auto')}
&& {
all: initial;
display: ${props => (props.inline ? 'inline-flex' : 'flex')};
flex-direction: ${props => props.direction};
flex-wrap: ${props => props.flexWrap};
${props => cssRuleIfExists(props, 'flexGrow')}
justify-content: ${props => props.justify};
align-items: ${props => props.align};
background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')};
${props => (props.width ? stylesForMedia('width', props.width) : '')}
${props => (props.height ? stylesForMedia('height', props.height) : '')}
}
`;
Flex.defaultProps = {

View File

@@ -16,17 +16,20 @@ export const Input =
styled.input <
InputProps >
`
font-size: ${props => props.fontSize};
width: ${props => props.width};
padding: 0.1em 0em;
font-family: 'Inter UI';
color: ${props => props.theme[props.fontColor || 'white']};
background: transparent;
outline: none;
border: none;
&::placeholder {
&& {
all: initial;
font-size: ${props => props.fontSize};
width: ${props => props.width};
padding: 0.1em 0em;
font-family: 'Inter UI';
color: ${props => props.theme[props.fontColor || 'white']};
opacity: 0.5;
background: transparent;
outline: none;
border: none;
&::placeholder {
color: ${props => props.theme[props.fontColor || 'white']};
opacity: 0.5;
}
}
`;

View File

@@ -27,25 +27,28 @@ export const Text =
styled.div <
TextProps >
`
font-family: ${props => props.fontFamily};
font-style: ${props => props.fontStyle};
font-weight: ${props => props.fontWeight};
font-size: ${props => props.fontSize};
opacity: ${props => props.opacity};
text-decoration-line: ${props => props.textDecorationLine};
${props => (props.lineHeight ? `line-height: ${props.lineHeight}` : '')};
${props => (props.center ? 'text-align: center' : '')};
color: ${props => props.fontColor && props.theme[props.fontColor]};
${props => (props.minHeight ? `min-height: ${props.minHeight}` : '')};
${props => (props.onClick ? 'cursor: pointer' : '')};
transition: color 0.5s ease;
${props => (props.noWrap ? 'white-space: nowrap' : '')};
${props => (props.display ? `display: ${props.display}` : '')};
${props => (props.letterSpacing ? `letter-spacing: ${props.letterSpacing}` : '')};
${props => (props.textTransform ? `text-transform: ${props.textTransform}` : '')};
&:hover {
${props =>
props.onClick ? `color: ${darken(darkenOnHoverAmount, props.theme[props.fontColor || 'white'])}` : ''};
&& {
all: initial;
font-family: 'Inter UI', sans-serif;
font-style: ${props => props.fontStyle};
font-weight: ${props => props.fontWeight};
font-size: ${props => props.fontSize};
opacity: ${props => props.opacity};
text-decoration-line: ${props => props.textDecorationLine};
${props => (props.lineHeight ? `line-height: ${props.lineHeight}` : '')};
${props => (props.center ? 'text-align: center' : '')};
color: ${props => props.fontColor && props.theme[props.fontColor]};
${props => (props.minHeight ? `min-height: ${props.minHeight}` : '')};
${props => (props.onClick ? 'cursor: pointer' : '')};
transition: color 0.5s ease;
${props => (props.noWrap ? 'white-space: nowrap' : '')};
${props => (props.display ? `display: ${props.display}` : '')};
${props => (props.letterSpacing ? `letter-spacing: ${props.letterSpacing}` : '')};
${props => (props.textTransform ? `text-transform: ${props.textTransform}` : '')};
&:hover {
${props =>
props.onClick ? `color: ${darken(darkenOnHoverAmount, props.theme[props.fontColor || 'white'])}` : ''};
}
}
`;
@@ -61,14 +64,3 @@ Text.defaultProps = {
};
Text.displayName = 'Text';
export const Title: React.StatelessComponent<TextProps> = props => <Text {...props} />;
Title.defaultProps = {
fontSize: '20px',
fontWeight: 600,
opacity: 1,
fontColor: ColorOption.primaryColor,
};
Title.displayName = 'Title';

View File

@@ -1,5 +1,7 @@
import * as React from 'react';
import { INJECTED_DIV_CLASS } from '../constants';
import { ZeroExInstantContainer } from './zero_ex_instant_container';
import { ZeroExInstantProvider, ZeroExInstantProviderProps } from './zero_ex_instant_provider';
@@ -7,8 +9,10 @@ export type ZeroExInstantProps = ZeroExInstantProviderProps;
export const ZeroExInstant: React.StatelessComponent<ZeroExInstantProps> = props => {
return (
<ZeroExInstantProvider {...props}>
<ZeroExInstantContainer />
</ZeroExInstantProvider>
<div className={INJECTED_DIV_CLASS}>
<ZeroExInstantProvider {...props}>
<ZeroExInstantContainer />
</ZeroExInstantProvider>
</div>
);
};

View File

@@ -10,6 +10,7 @@ import { ColorOption } from '../style/theme';
import { zIndex } from '../style/z_index';
import { SlideAnimationState } from './animations/slide_animation';
import { CSSReset } from './css_reset';
import { PaymentMethod } from './payment_method';
import { SlidingPanel } from './sliding_panel';
import { Container } from './ui/container';
@@ -26,41 +27,44 @@ export class ZeroExInstantContainer extends React.Component<ZeroExInstantContain
};
public render(): React.ReactNode {
return (
<Container
width={{ default: '350px', sm: '100%' }}
height={{ default: 'auto', sm: '100%' }}
position="relative"
>
<Container zIndex={zIndex.errorPopup} position="relative">
<LatestError />
</Container>
<React.Fragment>
<CSSReset />
<Container
zIndex={zIndex.mainContainer}
width={{ default: '350px', sm: '100%' }}
height={{ default: 'auto', sm: '100%' }}
position="relative"
backgroundColor={ColorOption.white}
borderRadius="3px"
hasBoxShadow={true}
overflow="hidden"
height="100%"
>
<Flex direction="column" height="100%" justify="flex-start">
<SelectedAssetInstantHeading onSelectAssetClick={this._handleSymbolClick} />
<PaymentMethod />
<SelectedAssetBuyOrderProgress />
<LatestBuyQuoteOrderDetails />
<Container padding="20px" width="100%">
<SelectedAssetBuyOrderStateButtons />
</Container>
</Flex>
<SlidingPanel
title="Select Token"
animationState={this.state.tokenSelectionPanelAnimationState}
onClose={this._handlePanelClose}
<Container zIndex={zIndex.errorPopup} position="relative">
<LatestError />
</Container>
<Container
zIndex={zIndex.mainContainer}
position="relative"
backgroundColor={ColorOption.white}
borderRadius="3px"
hasBoxShadow={true}
overflow="hidden"
height="100%"
>
<AvailableERC20TokenSelector onTokenSelect={this._handlePanelClose} />
</SlidingPanel>
<Flex direction="column" justify="flex-start" height="100%">
<SelectedAssetInstantHeading onSelectAssetClick={this._handleSymbolClick} />
<SelectedAssetBuyOrderProgress />
<PaymentMethod />
<LatestBuyQuoteOrderDetails />
<Container padding="20px" width="100%">
<SelectedAssetBuyOrderStateButtons />
</Container>
</Flex>
<SlidingPanel
title="Select Token"
animationState={this.state.tokenSelectionPanelAnimationState}
onClose={this._handlePanelClose}
>
<AvailableERC20TokenSelector onTokenSelect={this._handlePanelClose} />
</SlidingPanel>
</Container>
</Container>
</Container>
</React.Fragment>
);
}
private readonly _handleSymbolClick = (): void => {

View File

@@ -1,23 +1,20 @@
import { AssetBuyer } from '@0x/asset-buyer';
import { ObjectMap, SignedOrder } from '@0x/types';
import { ObjectMap } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { Provider } from 'ethereum-types';
import * as _ from 'lodash';
import * as React from 'react';
import { Provider as ReduxProvider } from 'react-redux';
import { oc } from 'ts-optchain';
import { SelectedAssetThemeProvider } from '../containers/selected_asset_theme_provider';
import { asyncData } from '../redux/async_data';
import { INITIAL_STATE, State } from '../redux/reducer';
import { DEFAULT_STATE, DefaultState, State } from '../redux/reducer';
import { store, Store } from '../redux/store';
import { fonts } from '../style/fonts';
import { AffiliateInfo, AssetMetaData, Network } from '../types';
import { AffiliateInfo, AssetMetaData, Network, OrderSource } from '../types';
import { assetUtils } from '../util/asset';
import { errorFlasher } from '../util/error_flasher';
import { gasPriceEstimator } from '../util/gas_price_estimator';
import { getInjectedProvider } from '../util/injected_provider';
import { providerStateFactory } from '../util/provider_state_factory';
fonts.include();
@@ -25,7 +22,7 @@ export type ZeroExInstantProviderProps = ZeroExInstantProviderRequiredProps &
Partial<ZeroExInstantProviderOptionalProps>;
export interface ZeroExInstantProviderRequiredProps {
orderSource: string | SignedOrder[];
orderSource: OrderSource;
}
export interface ZeroExInstantProviderOptionalProps {
@@ -41,30 +38,27 @@ export interface ZeroExInstantProviderOptionalProps {
export class ZeroExInstantProvider extends React.Component<ZeroExInstantProviderProps> {
private readonly _store: Store;
// TODO(fragosti): Write tests for this beast once we inject a provider.
private static _mergeInitialStateWithProps(props: ZeroExInstantProviderProps, state: State = INITIAL_STATE): State {
const networkId = props.networkId || state.network;
// TODO: Proper wallet connect flow
const provider = props.provider || getInjectedProvider();
const assetBuyerOptions = {
private static _mergeDefaultStateWithProps(
props: ZeroExInstantProviderProps,
defaultState: DefaultState = DEFAULT_STATE,
): State {
// use the networkId passed in with the props, otherwise default to that of the default state (1, mainnet)
const networkId = props.networkId || defaultState.network;
// construct the ProviderState
const providerState = providerStateFactory.getInitialProviderState(
props.orderSource,
networkId,
};
let assetBuyer;
if (_.isString(props.orderSource)) {
assetBuyer = AssetBuyer.getAssetBuyerForStandardRelayerAPIUrl(
provider,
props.orderSource,
assetBuyerOptions,
);
} else {
assetBuyer = AssetBuyer.getAssetBuyerForProvidedOrders(provider, props.orderSource, assetBuyerOptions);
}
props.provider,
);
// merge the additional additionalAssetMetaDataMap with our default map
const completeAssetMetaDataMap = {
...props.additionalAssetMetaDataMap,
...state.assetMetaDataMap,
...defaultState.assetMetaDataMap,
};
// construct the final state
const storeStateFromProps: State = {
...state,
assetBuyer,
...defaultState,
providerState,
network: networkId,
selectedAsset: _.isUndefined(props.defaultSelectedAssetData)
? undefined
@@ -74,7 +68,7 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
networkId,
),
selectedAssetAmount: _.isUndefined(props.defaultAssetBuyAmount)
? state.selectedAssetAmount
? undefined
: new BigNumber(props.defaultAssetBuyAmount),
availableAssets: _.isUndefined(props.availableAssetDatas)
? undefined
@@ -86,10 +80,9 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
}
constructor(props: ZeroExInstantProviderProps) {
super(props);
const initialAppState = ZeroExInstantProvider._mergeInitialStateWithProps(this.props, INITIAL_STATE);
const initialAppState = ZeroExInstantProvider._mergeDefaultStateWithProps(this.props);
this._store = store.create(initialAppState);
}
public componentDidMount(): void {
const state = this._store.getState();
// tslint:disable-next-line:no-floating-promises
@@ -108,7 +101,6 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
// tslint:disable-next-line:no-floating-promises
this._flashErrorIfWrongNetwork();
}
public render(): React.ReactNode {
return (
<ReduxProvider store={this._store}>
@@ -116,19 +108,15 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
</ReduxProvider>
);
}
private readonly _flashErrorIfWrongNetwork = async (): Promise<void> => {
const msToShowError = 30000; // 30 seconds
const network = this._store.getState().network;
const assetBuyerIfExists = this._store.getState().assetBuyer;
const providerIfExists = oc(assetBuyerIfExists).provider();
if (!_.isUndefined(providerIfExists)) {
const web3Wrapper = new Web3Wrapper(providerIfExists);
const networkOfProvider = await web3Wrapper.getNetworkIdAsync();
if (network !== networkOfProvider) {
const errorMessage = `Wrong network detected. Try switching to ${Network[network]}.`;
errorFlasher.flashNewErrorMessage(this._store.dispatch, errorMessage, msToShowError);
}
const state = this._store.getState();
const network = state.network;
const web3Wrapper = state.providerState.web3Wrapper;
const networkOfProvider = await web3Wrapper.getNetworkIdAsync();
if (network !== networkOfProvider) {
const errorMessage = `Wrong network detected. Try switching to ${Network[network]}.`;
errorFlasher.flashNewErrorMessage(this._store.dispatch, errorMessage, msToShowError);
}
};
}

View File

@@ -1,7 +1,11 @@
import { BigNumber } from '@0x/utils';
import { Network } from './types';
export const BIG_NUMBER_ZERO = new BigNumber(0);
export const ETH_DECIMALS = 18;
export const DEFAULT_ZERO_EX_CONTAINER_SELECTOR = '#zeroExInstantContainer';
export const INJECTED_DIV_CLASS = 'zeroExInstantResetRoot';
export const INJECTED_DIV_ID = 'zeroExInstant';
export const WEB_3_WRAPPER_TRANSACTION_FAILED_ERROR_MSG_PREFIX = 'Transaction failed';
export const GWEI_IN_WEI = new BigNumber(1000000000);
@@ -13,3 +17,8 @@ export const ETH_GAS_STATION_API_BASE_URL = 'https://ethgasstation.info';
export const COINBASE_API_BASE_URL = 'https://api.coinbase.com/v2';
export const PROGRESS_STALL_AT_WIDTH = '95%';
export const PROGRESS_FINISH_ANIMATION_TIME_MS = 200;
export const ETHEREUM_NODE_URL_BY_NETWORK = {
[Network.Mainnet]: 'https://mainnet.infura.io/',
[Network.Kovan]: 'https://kovan.infura.io/',
};
export const BLOCK_POLLING_INTERVAL_MS = 10000; // 10s

View File

@@ -22,7 +22,7 @@ const mapStateToProps = (state: State, _ownProps: LatestBuyQuoteOrderDetailsProp
// use the worst case quote info
buyQuoteInfo: oc(state).latestBuyQuote.worstCaseQuoteInfo(),
ethUsdPrice: state.ethUsdPrice,
isLoading: state.quoteRequestState === AsyncProcessState.PENDING,
isLoading: state.quoteRequestState === AsyncProcessState.Pending,
});
export const LatestBuyQuoteOrderDetails: React.ComponentClass<LatestBuyQuoteOrderDetailsProps> = connect(

View File

@@ -14,7 +14,7 @@ import { etherscanUtil } from '../util/etherscan';
interface ConnectedState {
buyQuote?: BuyQuote;
buyOrderProcessingState: OrderProcessState;
assetBuyer?: AssetBuyer;
assetBuyer: AssetBuyer;
affiliateInfo?: AffiliateInfo;
onViewTransaction: () => void;
}
@@ -29,29 +29,31 @@ interface ConnectedDispatch {
onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void;
}
export interface SelectedAssetBuyOrderStateButtons {}
const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyOrderStateButtons): ConnectedState => ({
buyOrderProcessingState: state.buyOrderState.processState,
assetBuyer: state.assetBuyer,
buyQuote: state.latestBuyQuote,
affiliateInfo: state.affiliateInfo,
onViewTransaction: () => {
if (
state.assetBuyer &&
(state.buyOrderState.processState === OrderProcessState.PROCESSING ||
state.buyOrderState.processState === OrderProcessState.SUCCESS ||
state.buyOrderState.processState === OrderProcessState.FAILURE)
) {
const etherscanUrl = etherscanUtil.getEtherScanTxnAddressIfExists(
state.buyOrderState.txHash,
state.assetBuyer.networkId,
);
if (etherscanUrl) {
window.open(etherscanUrl, '_blank');
return;
const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyOrderStateButtons): ConnectedState => {
const assetBuyer = state.providerState.assetBuyer;
return {
buyOrderProcessingState: state.buyOrderState.processState,
assetBuyer,
buyQuote: state.latestBuyQuote,
affiliateInfo: state.affiliateInfo,
onViewTransaction: () => {
if (
state.buyOrderState.processState === OrderProcessState.Processing ||
state.buyOrderState.processState === OrderProcessState.Success ||
state.buyOrderState.processState === OrderProcessState.Failure
) {
const etherscanUrl = etherscanUtil.getEtherScanTxnAddressIfExists(
state.buyOrderState.txHash,
assetBuyer.networkId,
);
if (etherscanUrl) {
window.open(etherscanUrl, '_blank');
return;
}
}
}
},
});
},
};
};
const mapDispatchToProps = (
dispatch: Dispatch<Action>,

View File

@@ -23,7 +23,7 @@ export interface SelectedERC20AssetAmountInputProps {
}
interface ConnectedState {
assetBuyer?: AssetBuyer;
assetBuyer: AssetBuyer;
value?: BigNumber;
asset?: ERC20Asset;
isDisabled: boolean;
@@ -33,7 +33,7 @@ interface ConnectedState {
interface ConnectedDispatch {
updateBuyQuote: (
assetBuyer?: AssetBuyer,
assetBuyer: AssetBuyer,
value?: BigNumber,
asset?: ERC20Asset,
affiliateInfo?: AffiliateInfo,
@@ -52,15 +52,16 @@ type FinalProps = ConnectedProps & SelectedERC20AssetAmountInputProps;
const mapStateToProps = (state: State, _ownProps: SelectedERC20AssetAmountInputProps): ConnectedState => {
const processState = state.buyOrderState.processState;
const isEnabled = processState === OrderProcessState.NONE || processState === OrderProcessState.FAILURE;
const isEnabled = processState === OrderProcessState.None || processState === OrderProcessState.Failure;
const isDisabled = !isEnabled;
const selectedAsset =
!_.isUndefined(state.selectedAsset) && state.selectedAsset.metaData.assetProxyId === AssetProxyId.ERC20
? (state.selectedAsset as ERC20Asset)
: undefined;
const numberOfAssetsAvailable = _.isUndefined(state.availableAssets) ? undefined : state.availableAssets.length;
const assetBuyer = state.providerState.assetBuyer;
return {
assetBuyer: state.assetBuyer,
assetBuyer,
value: state.selectedAssetAmount,
asset: selectedAsset,
isDisabled,
@@ -128,7 +129,7 @@ const mapDispatchToProps = (
// reset our buy state
dispatch(actions.setBuyOrderStateNone());
if (!_.isUndefined(value) && value.greaterThan(0) && !_.isUndefined(asset) && !_.isUndefined(assetBuyer)) {
if (!_.isUndefined(value) && value.greaterThan(0) && !_.isUndefined(asset)) {
// even if it's debounced, give them the illusion it's loading
dispatch(actions.setQuoteRequestStatePending());
// tslint:disable-next-line:no-floating-promises

View File

@@ -2,7 +2,7 @@ import * as _ from 'lodash';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { DEFAULT_ZERO_EX_CONTAINER_SELECTOR, INJECTED_DIV_ID } from './constants';
import { DEFAULT_ZERO_EX_CONTAINER_SELECTOR, INJECTED_DIV_CLASS, INJECTED_DIV_ID } from './constants';
import { ZeroExInstantOverlay, ZeroExInstantOverlayProps } from './index';
import { assert } from './util/assert';
@@ -41,6 +41,7 @@ export const render = (props: ZeroExInstantOverlayProps, selector: string = DEFA
const appendTo = appendToIfExists as Element;
const injectedDiv = document.createElement('div');
injectedDiv.setAttribute('id', INJECTED_DIV_ID);
injectedDiv.setAttribute('class', INJECTED_DIV_CLASS);
appendTo.appendChild(injectedDiv);
const instantOverlayProps = {
...props,

View File

@@ -20,18 +20,17 @@ export const asyncData = {
}
},
fetchAvailableAssetDatasAndDispatchToStore: async (store: Store) => {
const { assetBuyer, assetMetaDataMap, network } = store.getState();
if (!_.isUndefined(assetBuyer)) {
try {
const assetDatas = await assetBuyer.getAvailableAssetDatasAsync();
const assets = assetUtils.createAssetsFromAssetDatas(assetDatas, assetMetaDataMap, network);
store.dispatch(actions.setAvailableAssets(assets));
} catch (e) {
const errorMessage = 'Could not find any assets';
errorFlasher.flashNewErrorMessage(store.dispatch, errorMessage);
// On error, just specify that none are available
store.dispatch(actions.setAvailableAssets([]));
}
const { providerState, assetMetaDataMap, network } = store.getState();
const assetBuyer = providerState.assetBuyer;
try {
const assetDatas = await assetBuyer.getAvailableAssetDatasAsync();
const assets = assetUtils.createAssetsFromAssetDatas(assetDatas, assetMetaDataMap, network);
store.dispatch(actions.setAvailableAssets(assets));
} catch (e) {
const errorMessage = 'Could not find any assets';
errorFlasher.flashNewErrorMessage(store.dispatch, errorMessage);
// On error, just specify that none are available
store.dispatch(actions.setAvailableAssets([]));
}
},
};

View File

@@ -1,4 +1,4 @@
import { AssetBuyer, BuyQuote } from '@0x/asset-buyer';
import { BuyQuote } from '@0x/asset-buyer';
import { AssetProxyId, ObjectMap } from '@0x/types';
import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
@@ -14,172 +14,181 @@ import {
Network,
OrderProcessState,
OrderState,
ProviderState,
} from '../types';
import { Action, ActionTypes } from './actions';
export interface State {
// State that is required and we have defaults for, before props are passed in
export interface DefaultState {
network: Network;
assetBuyer?: AssetBuyer;
assetMetaDataMap: ObjectMap<AssetMetaData>;
selectedAsset?: Asset;
availableAssets?: Asset[];
selectedAssetAmount?: BigNumber;
buyOrderState: OrderState;
ethUsdPrice?: BigNumber;
latestBuyQuote?: BuyQuote;
quoteRequestState: AsyncProcessState;
latestErrorMessage?: string;
latestErrorDisplayStatus: DisplayStatus;
affiliateInfo?: AffiliateInfo;
quoteRequestState: AsyncProcessState;
}
export const INITIAL_STATE: State = {
// State that is required but needs to be derived from the props
interface PropsDerivedState {
providerState: ProviderState;
}
// State that is optional
interface OptionalState {
selectedAsset: Asset;
availableAssets: Asset[];
selectedAssetAmount: BigNumber;
ethUsdPrice: BigNumber;
latestBuyQuote: BuyQuote;
latestErrorMessage: string;
affiliateInfo: AffiliateInfo;
}
export type State = DefaultState & PropsDerivedState & Partial<OptionalState>;
export const DEFAULT_STATE: DefaultState = {
network: Network.Mainnet,
selectedAssetAmount: undefined,
availableAssets: undefined,
assetMetaDataMap,
buyOrderState: { processState: OrderProcessState.NONE },
ethUsdPrice: undefined,
latestBuyQuote: undefined,
latestErrorMessage: undefined,
buyOrderState: { processState: OrderProcessState.None },
latestErrorDisplayStatus: DisplayStatus.Hidden,
quoteRequestState: AsyncProcessState.NONE,
affiliateInfo: undefined,
quoteRequestState: AsyncProcessState.None,
};
export const reducer = (state: State = INITIAL_STATE, action: Action): State => {
switch (action.type) {
case ActionTypes.UPDATE_ETH_USD_PRICE:
return {
...state,
ethUsdPrice: action.data,
};
case ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT:
return {
...state,
selectedAssetAmount: action.data,
};
case ActionTypes.UPDATE_LATEST_BUY_QUOTE:
const newBuyQuoteIfExists = action.data;
const shouldUpdate =
_.isUndefined(newBuyQuoteIfExists) || doesBuyQuoteMatchState(newBuyQuoteIfExists, state);
if (shouldUpdate) {
export const createReducer = (initialState: State) => {
const reducer = (state: State = initialState, action: Action): State => {
switch (action.type) {
case ActionTypes.UPDATE_ETH_USD_PRICE:
return {
...state,
latestBuyQuote: newBuyQuoteIfExists,
quoteRequestState: AsyncProcessState.SUCCESS,
ethUsdPrice: action.data,
};
} else {
return state;
}
case ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT:
return {
...state,
selectedAssetAmount: action.data,
};
case ActionTypes.UPDATE_LATEST_BUY_QUOTE:
const newBuyQuoteIfExists = action.data;
const shouldUpdate =
_.isUndefined(newBuyQuoteIfExists) || doesBuyQuoteMatchState(newBuyQuoteIfExists, state);
if (shouldUpdate) {
return {
...state,
latestBuyQuote: newBuyQuoteIfExists,
quoteRequestState: AsyncProcessState.Success,
};
} else {
return state;
}
case ActionTypes.SET_QUOTE_REQUEST_STATE_PENDING:
return {
...state,
latestBuyQuote: undefined,
quoteRequestState: AsyncProcessState.PENDING,
};
case ActionTypes.SET_QUOTE_REQUEST_STATE_FAILURE:
return {
...state,
latestBuyQuote: undefined,
quoteRequestState: AsyncProcessState.FAILURE,
};
case ActionTypes.SET_BUY_ORDER_STATE_NONE:
return {
...state,
buyOrderState: { processState: OrderProcessState.NONE },
};
case ActionTypes.SET_BUY_ORDER_STATE_VALIDATING:
return {
...state,
buyOrderState: { processState: OrderProcessState.VALIDATING },
};
case ActionTypes.SET_BUY_ORDER_STATE_PROCESSING:
const processingData = action.data;
const { startTimeUnix, expectedEndTimeUnix } = processingData;
return {
...state,
buyOrderState: {
processState: OrderProcessState.PROCESSING,
txHash: processingData.txHash,
progress: {
startTimeUnix,
expectedEndTimeUnix,
case ActionTypes.SET_QUOTE_REQUEST_STATE_PENDING:
return {
...state,
latestBuyQuote: undefined,
quoteRequestState: AsyncProcessState.Pending,
};
case ActionTypes.SET_QUOTE_REQUEST_STATE_FAILURE:
return {
...state,
latestBuyQuote: undefined,
quoteRequestState: AsyncProcessState.Failure,
};
case ActionTypes.SET_BUY_ORDER_STATE_NONE:
return {
...state,
buyOrderState: { processState: OrderProcessState.None },
};
case ActionTypes.SET_BUY_ORDER_STATE_VALIDATING:
return {
...state,
buyOrderState: { processState: OrderProcessState.Validating },
};
case ActionTypes.SET_BUY_ORDER_STATE_PROCESSING:
const processingData = action.data;
const { startTimeUnix, expectedEndTimeUnix } = processingData;
return {
...state,
buyOrderState: {
processState: OrderProcessState.Processing,
txHash: processingData.txHash,
progress: {
startTimeUnix,
expectedEndTimeUnix,
},
},
},
};
case ActionTypes.SET_BUY_ORDER_STATE_FAILURE:
const failureTxHash = action.data;
if ('txHash' in state.buyOrderState) {
if (state.buyOrderState.txHash === failureTxHash) {
const { txHash, progress } = state.buyOrderState;
return {
...state,
buyOrderState: {
processState: OrderProcessState.FAILURE,
txHash,
progress,
},
};
};
case ActionTypes.SET_BUY_ORDER_STATE_FAILURE:
const failureTxHash = action.data;
if ('txHash' in state.buyOrderState) {
if (state.buyOrderState.txHash === failureTxHash) {
const { txHash, progress } = state.buyOrderState;
return {
...state,
buyOrderState: {
processState: OrderProcessState.Failure,
txHash,
progress,
},
};
}
}
}
return state;
case ActionTypes.SET_BUY_ORDER_STATE_SUCCESS:
const successTxHash = action.data;
if ('txHash' in state.buyOrderState) {
if (state.buyOrderState.txHash === successTxHash) {
const { txHash, progress } = state.buyOrderState;
return {
...state,
buyOrderState: {
processState: OrderProcessState.SUCCESS,
txHash,
progress,
},
};
return state;
case ActionTypes.SET_BUY_ORDER_STATE_SUCCESS:
const successTxHash = action.data;
if ('txHash' in state.buyOrderState) {
if (state.buyOrderState.txHash === successTxHash) {
const { txHash, progress } = state.buyOrderState;
return {
...state,
buyOrderState: {
processState: OrderProcessState.Success,
txHash,
progress,
},
};
}
}
}
return state;
case ActionTypes.SET_ERROR_MESSAGE:
return {
...state,
latestErrorMessage: action.data,
latestErrorDisplayStatus: DisplayStatus.Present,
};
case ActionTypes.HIDE_ERROR:
return {
...state,
latestErrorDisplayStatus: DisplayStatus.Hidden,
};
case ActionTypes.CLEAR_ERROR:
return {
...state,
latestErrorMessage: undefined,
latestErrorDisplayStatus: DisplayStatus.Hidden,
};
case ActionTypes.UPDATE_SELECTED_ASSET:
return {
...state,
selectedAsset: action.data,
};
case ActionTypes.RESET_AMOUNT:
return {
...state,
latestBuyQuote: undefined,
quoteRequestState: AsyncProcessState.NONE,
buyOrderState: { processState: OrderProcessState.NONE },
selectedAssetAmount: undefined,
};
case ActionTypes.SET_AVAILABLE_ASSETS:
return {
...state,
availableAssets: action.data,
};
default:
return state;
}
return state;
case ActionTypes.SET_ERROR_MESSAGE:
return {
...state,
latestErrorMessage: action.data,
latestErrorDisplayStatus: DisplayStatus.Present,
};
case ActionTypes.HIDE_ERROR:
return {
...state,
latestErrorDisplayStatus: DisplayStatus.Hidden,
};
case ActionTypes.CLEAR_ERROR:
return {
...state,
latestErrorMessage: undefined,
latestErrorDisplayStatus: DisplayStatus.Hidden,
};
case ActionTypes.UPDATE_SELECTED_ASSET:
return {
...state,
selectedAsset: action.data,
};
case ActionTypes.RESET_AMOUNT:
return {
...state,
latestBuyQuote: undefined,
quoteRequestState: AsyncProcessState.None,
buyOrderState: { processState: OrderProcessState.None },
selectedAssetAmount: undefined,
};
case ActionTypes.SET_AVAILABLE_ASSETS:
return {
...state,
availableAssets: action.data,
};
default:
return state;
}
};
return reducer;
};
const doesBuyQuoteMatchState = (buyQuote: BuyQuote, state: State): boolean => {

View File

@@ -2,12 +2,13 @@ import * as _ from 'lodash';
import { createStore, Store as ReduxStore } from 'redux';
import { devToolsEnhancer } from 'redux-devtools-extension/developmentOnly';
import { reducer, State } from './reducer';
import { createReducer, State } from './reducer';
export type Store = ReduxStore<State>;
export const store = {
create: (state: State): Store => {
return createStore(reducer, state, devToolsEnhancer({}));
create: (initialState: State): Store => {
const reducer = createReducer(initialState);
return createStore(reducer, initialState, devToolsEnhancer({}));
},
};

View File

@@ -1,6 +1,6 @@
import * as styledComponents from 'styled-components';
const { default: styled, css, keyframes, withTheme, ThemeProvider } = styledComponents;
const { default: styled, css, keyframes, withTheme, createGlobalStyle, ThemeProvider } = styledComponents;
export type Theme = { [key in ColorOption]: string };
@@ -38,4 +38,4 @@ export const transparentWhite = 'rgba(255,255,255,0.3)';
export const overlayBlack = 'rgba(0, 0, 0, 0.6)';
export const completelyTransparent = 'rga(0, 0, 0, 0)';
export { styled, css, keyframes, withTheme, ThemeProvider };
export { styled, css, keyframes, withTheme, createGlobalStyle, ThemeProvider };

View File

@@ -1,20 +1,23 @@
import { AssetProxyId, ObjectMap } from '@0x/types';
import { AssetBuyer, BigNumber } from '@0x/asset-buyer';
import { AssetProxyId, ObjectMap, SignedOrder } from '@0x/types';
import { Web3Wrapper } from '@0x/web3-wrapper';
import { Provider } from 'ethereum-types';
// Reusable
export type Maybe<T> = T | undefined;
export enum AsyncProcessState {
NONE = 'None',
PENDING = 'Pending',
SUCCESS = 'Success',
FAILURE = 'Failure',
None = 'NONE',
Pending = 'PENDING',
Success = 'SUCCESS',
Failure = 'FAILURE',
}
export enum OrderProcessState {
NONE = 'None',
VALIDATING = 'Validating',
PROCESSING = 'Processing',
SUCCESS = 'Success',
FAILURE = 'Failure',
None = 'NONE',
Validating = 'VALIDATING',
Processing = 'PROCESSING',
Success = 'SUCCESS',
Failure = 'FAILURE',
}
export interface SimulatedProgress {
@@ -23,10 +26,10 @@ export interface SimulatedProgress {
}
interface OrderStatePreTx {
processState: OrderProcessState.NONE | OrderProcessState.VALIDATING;
processState: OrderProcessState.None | OrderProcessState.Validating;
}
interface OrderStatePostTx {
processState: OrderProcessState.PROCESSING | OrderProcessState.SUCCESS | OrderProcessState.FAILURE;
processState: OrderProcessState.Processing | OrderProcessState.Success | OrderProcessState.Failure;
txHash: string;
progress: SimulatedProgress;
}
@@ -89,3 +92,31 @@ export interface AffiliateInfo {
feeRecipient: string;
feePercentage: number;
}
export interface ProviderState {
provider: Provider;
assetBuyer: AssetBuyer;
web3Wrapper: Web3Wrapper;
account: Account;
}
export enum AccountState {
Loading = 'LOADING',
Ready = 'READY',
Locked = 'LOCKED', // TODO(bmillman): break this up into locked / privacy mode enabled
Error = 'ERROR',
None = 'NONE,',
}
export interface AccountReady {
state: AccountState.Ready;
address: string;
ethBalanceInWei?: BigNumber;
}
export interface AccountNotReady {
state: AccountState.None | AccountState.Loading | AccountState.Locked | AccountState.Error;
}
export type Account = AccountReady | AccountNotReady;
export type OrderSource = string | SignedOrder[];

View File

@@ -0,0 +1,17 @@
import { AssetBuyer, AssetBuyerOpts } from '@0x/asset-buyer';
import { Provider } from 'ethereum-types';
import * as _ from 'lodash';
import { Network, OrderSource } from '../types';
export const assetBuyerFactory = {
getAssetBuyer: (provider: Provider, orderSource: OrderSource, network: Network): AssetBuyer => {
const assetBuyerOptions: Partial<AssetBuyerOpts> = {
networkId: network,
};
const assetBuyer = _.isString(orderSource)
? AssetBuyer.getAssetBuyerForStandardRelayerAPIUrl(provider, orderSource, assetBuyerOptions)
: AssetBuyer.getAssetBuyerForProvidedOrders(provider, orderSource, assetBuyerOptions);
return assetBuyer;
},
};

View File

@@ -1,16 +0,0 @@
import { Provider } from 'ethereum-types';
import * as _ from 'lodash';
export const getInjectedProvider = (): Provider => {
const injectedProviderIfExists = (window as any).ethereum;
if (!_.isUndefined(injectedProviderIfExists)) {
// TODO: call enable here when implementing wallet connection flow
return injectedProviderIfExists;
}
const injectedWeb3IfExists = (window as any).web3;
if (!_.isUndefined(injectedWeb3IfExists.currentProvider)) {
return injectedWeb3IfExists.currentProvider;
} else {
throw new Error(`No injected web3 found`);
}
};

View File

@@ -0,0 +1,34 @@
import { EmptyWalletSubprovider, RPCSubprovider, Web3ProviderEngine } from '@0x/subproviders';
import { Provider } from 'ethereum-types';
import * as _ from 'lodash';
import { BLOCK_POLLING_INTERVAL_MS, ETHEREUM_NODE_URL_BY_NETWORK } from '../constants';
import { Maybe, Network } from '../types';
export const providerFactory = {
getInjectedProviderIfExists: (): Maybe<Provider> => {
const injectedProviderIfExists = (window as any).ethereum;
if (!_.isUndefined(injectedProviderIfExists)) {
return injectedProviderIfExists;
}
const injectedWeb3IfExists = (window as any).web3;
if (!_.isUndefined(injectedWeb3IfExists) && !_.isUndefined(injectedWeb3IfExists.currentProvider)) {
return injectedWeb3IfExists.currentProvider;
}
return undefined;
},
getFallbackNoSigningProvider: (network: Network): Provider => {
const providerEngine = new Web3ProviderEngine({
pollingInterval: BLOCK_POLLING_INTERVAL_MS,
});
// Intercept calls to `eth_accounts` and always return empty
providerEngine.addProvider(new EmptyWalletSubprovider());
// Construct an RPC subprovider, all data based requests will be sent via the RPCSubprovider
// TODO(bmillman): make this more resilient to infura failures
const rpcUrl = ETHEREUM_NODE_URL_BY_NETWORK[network];
providerEngine.addProvider(new RPCSubprovider(rpcUrl));
// // Start the Provider Engine
providerEngine.start();
return providerEngine;
},
};

View File

@@ -0,0 +1,69 @@
import { Web3Wrapper } from '@0x/web3-wrapper';
import { Provider } from 'ethereum-types';
import * as _ from 'lodash';
import { AccountNotReady, AccountState, Maybe, Network, OrderSource, ProviderState } from '../types';
import { assetBuyerFactory } from './asset_buyer_factory';
import { providerFactory } from './provider_factory';
const LOADING_ACCOUNT: AccountNotReady = {
state: AccountState.Loading,
};
const NO_ACCOUNT: AccountNotReady = {
state: AccountState.None,
};
export const providerStateFactory = {
getInitialProviderState: (orderSource: OrderSource, network: Network, provider?: Provider): ProviderState => {
if (!_.isUndefined(provider)) {
return providerStateFactory.getInitialProviderStateFromProvider(orderSource, network, provider);
}
const providerStateFromWindowIfExits = providerStateFactory.getInitialProviderStateFromWindowIfExists(
orderSource,
network,
);
if (providerStateFromWindowIfExits) {
return providerStateFromWindowIfExits;
} else {
return providerStateFactory.getInitialProviderStateFallback(orderSource, network);
}
},
getInitialProviderStateFromProvider: (
orderSource: OrderSource,
network: Network,
provider: Provider,
): ProviderState => {
const providerState: ProviderState = {
provider,
web3Wrapper: new Web3Wrapper(provider),
assetBuyer: assetBuyerFactory.getAssetBuyer(provider, orderSource, network),
account: LOADING_ACCOUNT,
};
return providerState;
},
getInitialProviderStateFromWindowIfExists: (orderSource: OrderSource, network: Network): Maybe<ProviderState> => {
const injectedProviderIfExists = providerFactory.getInjectedProviderIfExists();
if (!_.isUndefined(injectedProviderIfExists)) {
const providerState: ProviderState = {
provider: injectedProviderIfExists,
web3Wrapper: new Web3Wrapper(injectedProviderIfExists),
assetBuyer: assetBuyerFactory.getAssetBuyer(injectedProviderIfExists, orderSource, network),
account: LOADING_ACCOUNT,
};
return providerState;
} else {
return undefined;
}
},
getInitialProviderStateFallback: (orderSource: OrderSource, network: Network): ProviderState => {
const provider = providerFactory.getFallbackNoSigningProvider(network);
const providerState: ProviderState = {
provider,
web3Wrapper: new Web3Wrapper(provider),
assetBuyer: assetBuyerFactory.getAssetBuyer(provider, orderSource, network),
account: NO_ACCOUNT,
};
return providerState;
},
};

View File

@@ -7,14 +7,15 @@
"private": true,
"description": "Website and 0x portal dapp",
"scripts": {
"build": "node --max_old_space_size=8192 ../../node_modules/.bin/webpack --mode production",
"build": "yarn build:dev",
"build:prod": "node --max_old_space_size=8192 ../../node_modules/.bin/webpack --mode production",
"build:dev": "../../node_modules/.bin/webpack --mode development",
"clean": "shx rm -f public/bundle*",
"lint": "tslint --format stylish --project . 'ts/**/*.ts' 'ts/**/*.tsx'",
"dev": "webpack-dev-server --mode development --content-base public --https",
"deploy_dogfood": "npm run build; aws s3 sync ./public/. s3://dogfood.0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers",
"deploy_staging": "npm run build; aws s3 sync ./public/. s3://staging-0xproject --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers",
"deploy_live": "DEPLOY_ROLLBAR_SOURCEMAPS=true npm run build; aws s3 sync ./public/. s3://0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --exclude *.map.js"
"deploy_dogfood": "npm run build:prod; aws s3 sync ./public/. s3://dogfood.0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers",
"deploy_staging": "npm run build:prod; aws s3 sync ./public/. s3://staging-0xproject --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers",
"deploy_live": "DEPLOY_ROLLBAR_SOURCEMAPS=true npm run build:prod; aws s3 sync ./public/. s3://0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers --exclude *.map.js"
},
"author": "Fabio Berger",
"license": "Apache-2.0",

View File

@@ -1900,10 +1900,6 @@ aes-js@^0.2.3:
version "0.2.4"
resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-0.2.4.tgz#94b881ab717286d015fa219e08fb66709dda5a3d"
aes-js@^3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/aes-js/-/aes-js-3.1.1.tgz#89fd1f94ae51b4c72d62466adc1a7323ff52f072"
agent-base@4, agent-base@^4.1.0, agent-base@~4.2.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
@@ -3341,7 +3337,7 @@ bs-logger@0.x:
dependencies:
fast-json-stable-stringify "^2.0.0"
bs58@=4.0.1, bs58@^4.0.0:
bs58@=4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a"
dependencies:
@@ -3364,14 +3360,6 @@ bs58check@^1.0.8:
bs58 "^3.1.0"
create-hash "^1.1.0"
bs58check@^2.1.2:
version "2.1.2"
resolved "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc"
dependencies:
bs58 "^4.0.0"
create-hash "^1.1.0"
safe-buffer "^5.1.2"
bser@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719"
@@ -5965,19 +5953,6 @@ ethereumjs-wallet@0.6.0:
utf8 "^2.1.1"
uuid "^2.0.1"
ethereumjs-wallet@~0.6.0:
version "0.6.2"
resolved "https://registry.npmjs.org/ethereumjs-wallet/-/ethereumjs-wallet-0.6.2.tgz#67244b6af3e8113b53d709124b25477b64aeccda"
dependencies:
aes-js "^3.1.1"
bs58check "^2.1.2"
ethereumjs-util "^5.2.0"
hdkey "^1.0.0"
safe-buffer "^5.1.2"
scrypt.js "^0.2.0"
utf8 "^3.0.0"
uuid "^3.3.2"
ethers@~4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/ethers/-/ethers-4.0.4.tgz#d3f85e8b27f4b59537e06526439b0fb15b44dc65"
@@ -6782,7 +6757,7 @@ ganache-core@0xProject/ganache-core#monorepo-dep:
ethereumjs-tx "0xProject/ethereumjs-tx#fake-tx-include-signature-by-default"
ethereumjs-util "^5.2.0"
ethereumjs-vm "2.3.5"
ethereumjs-wallet "~0.6.0"
ethereumjs-wallet "0.6.0"
fake-merkle-patricia-tree "~1.0.1"
heap "~0.2.6"
js-scrypt "^0.2.0"
@@ -7507,14 +7482,6 @@ hdkey@^0.7.0, hdkey@^0.7.1:
coinstring "^2.0.0"
secp256k1 "^3.0.1"
hdkey@^1.0.0:
version "1.1.0"
resolved "https://registry.npmjs.org/hdkey/-/hdkey-1.1.0.tgz#e74e7b01d2c47f797fa65d1d839adb7a44639f29"
dependencies:
coinstring "^2.0.0"
safe-buffer "^5.1.1"
secp256k1 "^3.0.1"
he@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
@@ -15601,10 +15568,6 @@ utf8@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/utf8/-/utf8-2.1.2.tgz#1fa0d9270e9be850d9b05027f63519bf46457d96"
utf8@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1"
util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"