Merge branch 'development' into fixOrderValidation
* development: (51 commits) [instant] Viewport specific errors (#1228) Added more comments Include wholeNumberSchema in sra-spec schemas chore(instant): fix linter Fix isNode fix(instant): update buy quote at start up in the case of default amount chore(instant): increase max bundle size for bundle watch Small code review tweaks fix: apply css reset to overlay as well fix(website): turn off production flag when building locally feat(instant): add Account to the ProviderState feat(instant): fallback to an empty wallet provider when none is injected [order_utils.py] is_signature_valid, via Exchange contract (#1216) chore: linter fix: progress bar fix: use fontSize prop in button feat: make instant resistant to external styles chore(instant): update OrderState enum to follow capitalization conventions feat: add faux externall css file Take out unneeded conditionals ...
This commit is contained in:
@@ -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
|
||||
@@ -162,6 +162,9 @@ jobs:
|
||||
working_directory: ~/repo
|
||||
docker:
|
||||
- image: circleci/python
|
||||
- image: 0xorg/ganache-cli
|
||||
command: |
|
||||
ganache-cli --gasLimit 10000000 --noVMErrorsOnRPCResponse --db /snapshot --noVMErrorsOnRPCResponse -p 8545 --networkId 50 -m "concert load couple harbor equip island argue ramp clarify fence smart topic"
|
||||
steps:
|
||||
- checkout
|
||||
- run: sudo chown -R circleci:circleci /usr/local/bin
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -99,6 +99,7 @@ packages/*/scripts/
|
||||
.mypy_cache
|
||||
.tox
|
||||
python-packages/*/build
|
||||
python-packages/*/dist
|
||||
__pycache__
|
||||
python-packages/*/src/*.egg-info
|
||||
python-packages/*/.coverage
|
||||
|
||||
@@ -4,6 +4,7 @@ lib
|
||||
/packages/contracts/generated-artifacts
|
||||
/packages/abi-gen-wrappers/src/generated-wrappers
|
||||
/packages/contract-artifacts/artifacts
|
||||
/python-packages/order_utils/src/zero_ex/contract_artifacts/artifacts
|
||||
/packages/json-schemas/schemas
|
||||
/packages/metacoin/src/contract_wrappers
|
||||
/packages/metacoin/artifacts
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
},
|
||||
{
|
||||
"path": "packages/instant/public/main.bundle.js",
|
||||
"maxSize": "500kB"
|
||||
"maxSize": "1000kB"
|
||||
}
|
||||
],
|
||||
"ci": {
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
"note":
|
||||
"Fix bug where default values for `AssetBuyer` public facing methods could get overriden by `undefined` values",
|
||||
"pr": 1207
|
||||
},
|
||||
{
|
||||
"note": "Lower default expiry buffer from 5 minutes to 2 minutes",
|
||||
"pr": 1217
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -9,7 +9,7 @@ const MAINNET_NETWORK_ID = 1;
|
||||
const DEFAULT_ASSET_BUYER_OPTS: AssetBuyerOpts = {
|
||||
networkId: MAINNET_NETWORK_ID,
|
||||
orderRefreshIntervalMs: 10000, // 10 seconds
|
||||
expiryBufferSeconds: 300, // 5 minutes
|
||||
expiryBufferSeconds: 120, // 2 minutes
|
||||
};
|
||||
|
||||
const DEFAULT_BUY_QUOTE_REQUEST_OPTS: BuyQuoteRequestOpts = {
|
||||
|
||||
13
packages/instant/.staging.discharge.json
Normal file
13
packages/instant/.staging.discharge.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "0x-instant-staging",
|
||||
"build_command": "yarn build:umd:prod",
|
||||
"upload_directory": "public",
|
||||
"index_key": "index.html",
|
||||
"error_key": "index.html",
|
||||
"trailing_slashes": true,
|
||||
"cache": 3600,
|
||||
"aws_profile": "default",
|
||||
"aws_region": "us-east-1",
|
||||
"cdn": false,
|
||||
"dns_configured": true
|
||||
}
|
||||
@@ -53,7 +53,15 @@ You can deploy a work-in-progress version of 0x Instant at http://0x-instant-dog
|
||||
To build and deploy the site run
|
||||
|
||||
```
|
||||
yarn deploy
|
||||
yarn deploy_dogfood
|
||||
```
|
||||
|
||||
We also have a staging bucket that is to be updated less frequently can be used to share instant externally: http://0x-instant-staging.s3-website-us-east-1.amazonaws.com/
|
||||
|
||||
To build and deploy to this bucket, run
|
||||
|
||||
```
|
||||
yarn deploy_staging
|
||||
```
|
||||
|
||||
**NOTE: On deploying the site, it will say the site is available at a non-existent URL. Please ignore and use the (now updated) URL above.**
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"rebuild_and_test": "run-s clean build test",
|
||||
"test:circleci": "yarn test:coverage",
|
||||
"clean": "shx rm -rf lib coverage scripts",
|
||||
"deploy": "discharge deploy",
|
||||
"deploy_dogfood": "discharge deploy -c .dogfood.discharge.json",
|
||||
"deploy_staging": "discharge deploy -c .staging.discharge.json",
|
||||
"manual:postpublish": "yarn build; node ./scripts/postpublish.js"
|
||||
},
|
||||
"config": {
|
||||
@@ -48,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",
|
||||
@@ -65,7 +67,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@0x/tslint-config": "^1.0.9",
|
||||
"@static/discharge": "^1.2.2",
|
||||
"@static/discharge": "https://github.com/0xProject/discharge.git",
|
||||
"@types/enzyme": "^3.1.14",
|
||||
"@types/enzyme-adapter-react-16": "^1.0.3",
|
||||
"@types/jest": "^23.3.5",
|
||||
|
||||
25
packages/instant/public/external.css
Normal file
25
packages/instant/public/external.css
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -42,14 +43,14 @@
|
||||
takerAddress: '0x0000000000000000000000000000000000000000',
|
||||
makerFee: new BigNumber('0'),
|
||||
takerFee: new BigNumber('0'),
|
||||
makerAssetAmount: new BigNumber('400000000000000000000'),
|
||||
takerAssetAmount: new BigNumber('40000000000000000000'),
|
||||
makerAssetAmount: new BigNumber('200000000000000000000'),
|
||||
takerAssetAmount: new BigNumber('10000000000000000000'),
|
||||
makerAssetData: '0xf47261b00000000000000000000000008cb3971b8eb709c14616bd556ff6683019e90d9c',
|
||||
takerAssetData: '0xf47261b0000000000000000000000000d0a1e359811322d97991e03f863a0c30c2cf029c',
|
||||
expirationTimeSeconds: new BigNumber('1543046400'),
|
||||
expirationTimeSeconds: new BigNumber('1601535600'),
|
||||
feeRecipientAddress: '0x0000000000000000000000000000000000000000',
|
||||
salt: new BigNumber('47929252863126413473766089649682650973189811771354566206928245255479607883031'),
|
||||
signature: '0x1c0bf8ba709ceb5b32e6b0b5a8bb7f07e9d19aba88d8530715f8a298d12188e3862fcc0a30ddfad4062b30459f2859323c064052f12cc687466c457934b9419a1b03',
|
||||
salt: new BigNumber('3101985707338942582579795423923841749956600670712030922928319824580764688653'),
|
||||
signature: '0x1bd4d5686fea801fe33c68c4944356085e7e6cb553eb7073160abd815609f714e85fb47f44b7ffd0a2a1321ac40d72d55163869d0a50fdb5a402132150fe33a08403',
|
||||
exchangeAddress: '0x35dd2932454449b14cee11a94d3674a936d5d7b2'
|
||||
},
|
||||
// Order selling ZRX
|
||||
@@ -68,6 +69,40 @@
|
||||
salt: new BigNumber('64592004666704945574675477805199411288137454783320798602050822322450089238268'),
|
||||
signature: '0x1c13cacddca8d7d8248e91f412377e68f8f1f9891a59a6c1b2eea9f7b33558c30c4fb86a448e08ab7def40a28fb3a3062dcb33bb3c45302447fce5c4288b7c7f5b03',
|
||||
exchangeAddress: '0x35dd2932454449b14cee11a94d3674a936d5d7b2'
|
||||
},
|
||||
// Order selling GNT
|
||||
{
|
||||
senderAddress: '0x0000000000000000000000000000000000000000',
|
||||
makerAddress: '0x34a745008a643eebc58920eaa29fb1165b4a288e',
|
||||
takerAddress: '0x0000000000000000000000000000000000000000',
|
||||
makerFee: new BigNumber('0'),
|
||||
takerFee: new BigNumber('0'),
|
||||
makerAssetAmount: new BigNumber('250000000000000000000'),
|
||||
takerAssetAmount: new BigNumber('10000000000000000000'),
|
||||
makerAssetData: '0xf47261b000000000000000000000000031fb614e223706f15d0d3c5f4b08bdf0d5c78623',
|
||||
takerAssetData: '0xf47261b0000000000000000000000000d0a1e359811322d97991e03f863a0c30c2cf029c',
|
||||
expirationTimeSeconds: new BigNumber('1601535600'),
|
||||
feeRecipientAddress: '0x0000000000000000000000000000000000000000',
|
||||
salt: new BigNumber('40204378562212615907903051460421336779451270522691667164301816101569427926606'),
|
||||
signature: '0x1c788bf4b93769da1e8f195f52f0f59b4a298ac6da30cf6d05a87ed4be5ee974f61352ed1bc6a0844d0962b8c894c9ca08e452431255958a4e98dd93cbe1fbc73803',
|
||||
exchangeAddress: '0x35dd2932454449b14cee11a94d3674a936d5d7b2'
|
||||
},
|
||||
// Order selling MKR
|
||||
{
|
||||
senderAddress: '0x0000000000000000000000000000000000000000',
|
||||
makerAddress: '0x34a745008a643eebc58920eaa29fb1165b4a288e',
|
||||
takerAddress: '0x0000000000000000000000000000000000000000',
|
||||
makerFee: new BigNumber('0'),
|
||||
takerFee: new BigNumber('0'),
|
||||
makerAssetAmount: new BigNumber('200000000000000000000'),
|
||||
takerAssetAmount: new BigNumber('5000000000000000000'),
|
||||
makerAssetData: '0xf47261b00000000000000000000000007b6b10caa9e8e9552ba72638ea5b47c25afea1f3',
|
||||
takerAssetData: '0xf47261b0000000000000000000000000d0a1e359811322d97991e03f863a0c30c2cf029c',
|
||||
expirationTimeSeconds: new BigNumber('1601535600'),
|
||||
feeRecipientAddress: '0x0000000000000000000000000000000000000000',
|
||||
salt: new BigNumber('71338269924068280039932133924198049371838034090153601678083172009862985793828'),
|
||||
signature: '0x1bb3151d57ee1e8fa697767ce83ee4ba77d1ceb8cc1e79c7d77126b3687517704c50c6b3d9cb42c7e7d4478d574b297dfbd1626c5c18a7bc9c2a792c4c07f0797c03',
|
||||
exchangeAddress: '0x35dd2932454449b14cee11a94d3674a936d5d7b2'
|
||||
}
|
||||
];
|
||||
const queryParams = new Uri(window.location.search);
|
||||
@@ -87,7 +122,7 @@
|
||||
};
|
||||
}
|
||||
const renderOptionsOverrides = {
|
||||
orderSource: orderSourceOverride === 'provided' ? [providedOrder] : orderSourceOverride,
|
||||
orderSource: orderSourceOverride === 'provided' ? providedOrders : orderSourceOverride,
|
||||
networkId: +queryParams.getQueryParamValue('networkId') || undefined,
|
||||
defaultAssetBuyAmount: +queryParams.getQueryParamValue('defaultAssetBuyAmount') || undefined,
|
||||
availableAssetDatas: availableAssetDatasString ? JSON.parse(availableAssetDatasString) : undefined,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ColorOption } from '../style/theme';
|
||||
|
||||
import { Pulse } from './animations/pulse';
|
||||
|
||||
import { Text } from './ui';
|
||||
import { Text } from './ui/text';
|
||||
|
||||
interface PlainPlaceholder {
|
||||
color: ColorOption;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Keyframes } from 'styled-components';
|
||||
import { InterpolationValue } from 'styled-components';
|
||||
|
||||
import { media, OptionallyScreenSpecific, stylesForMedia } from '../../style/media';
|
||||
import { css, keyframes, styled } from '../../style/theme';
|
||||
|
||||
export interface TransitionInfo {
|
||||
@@ -51,30 +52,57 @@ export interface PositionAnimationSettings {
|
||||
right?: TransitionInfo;
|
||||
timingFunction: string;
|
||||
duration?: string;
|
||||
position?: string;
|
||||
}
|
||||
|
||||
export interface PositionAnimationProps extends PositionAnimationSettings {
|
||||
position: string;
|
||||
const generatePositionAnimationCss = (positionSettings: PositionAnimationSettings) => {
|
||||
return css`
|
||||
animation-name: ${slideKeyframeGenerator(
|
||||
positionSettings.position || 'relative',
|
||||
positionSettings.top,
|
||||
positionSettings.bottom,
|
||||
positionSettings.left,
|
||||
positionSettings.right,
|
||||
)};
|
||||
animation-duration: ${positionSettings.duration || '0.3s'};
|
||||
animation-timing-function: ${positionSettings.timingFunction};
|
||||
animation-delay: 0s;
|
||||
animation-iteration-count: 1;
|
||||
animation-fill-mode: forwards;
|
||||
position: ${positionSettings.position || 'relative'};
|
||||
width: 100%;
|
||||
`;
|
||||
};
|
||||
|
||||
export interface PositionAnimationProps {
|
||||
positionSettings: OptionallyScreenSpecific<PositionAnimationSettings>;
|
||||
zIndex?: OptionallyScreenSpecific<number>;
|
||||
}
|
||||
|
||||
const defaultAnimation = (positionSettings: OptionallyScreenSpecific<PositionAnimationSettings>) => {
|
||||
const bestDefault = 'default' in positionSettings ? positionSettings.default : positionSettings;
|
||||
return generatePositionAnimationCss(bestDefault);
|
||||
};
|
||||
const animationForSize = (
|
||||
positionSettings: OptionallyScreenSpecific<PositionAnimationSettings>,
|
||||
sizeKey: 'sm' | 'md' | 'lg',
|
||||
mediaFn: (...args: any[]) => InterpolationValue[],
|
||||
) => {
|
||||
// checking default makes sure we have a PositionAnimationSettings object
|
||||
// and then we check to see if we have a setting for the specific `sizeKey`
|
||||
const animationSettingsForSize = 'default' in positionSettings && positionSettings[sizeKey];
|
||||
return animationSettingsForSize && mediaFn`${generatePositionAnimationCss(animationSettingsForSize)}`;
|
||||
};
|
||||
|
||||
export const PositionAnimation =
|
||||
styled.div <
|
||||
PositionAnimationProps >
|
||||
`
|
||||
animation-name: ${props =>
|
||||
css`
|
||||
${slideKeyframeGenerator(props.position, props.top, props.bottom, props.left, props.right)};
|
||||
`};
|
||||
animation-duration: ${props => props.duration || '0.3s'};
|
||||
animation-timing-function: ${props => props.timingFunction};
|
||||
animation-delay: 0s;
|
||||
animation-iteration-count: 1;
|
||||
animation-fill-mode: forwards;
|
||||
position: ${props => props.position};
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
&& {
|
||||
${props => props.zIndex && stylesForMedia<number>('z-index', props.zIndex)}
|
||||
${props => defaultAnimation(props.positionSettings)}
|
||||
${props => animationForSize(props.positionSettings, 'sm', media.small)}
|
||||
${props => animationForSize(props.positionSettings, 'md', media.medium)}
|
||||
${props => animationForSize(props.positionSettings, 'lg', media.large)}
|
||||
}
|
||||
`;
|
||||
|
||||
PositionAnimation.defaultProps = {
|
||||
position: 'relative',
|
||||
};
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { OptionallyScreenSpecific } from '../../style/media';
|
||||
|
||||
import { PositionAnimation, PositionAnimationSettings } from './position_animation';
|
||||
|
||||
export type SlideAnimationState = 'slidIn' | 'slidOut' | 'none';
|
||||
export interface SlideAnimationProps {
|
||||
position: string;
|
||||
animationState: SlideAnimationState;
|
||||
slideInSettings: PositionAnimationSettings;
|
||||
slideOutSettings: PositionAnimationSettings;
|
||||
slideInSettings: OptionallyScreenSpecific<PositionAnimationSettings>;
|
||||
slideOutSettings: OptionallyScreenSpecific<PositionAnimationSettings>;
|
||||
zIndex?: OptionallyScreenSpecific<number>;
|
||||
}
|
||||
|
||||
export const SlideAnimation: React.StatelessComponent<SlideAnimationProps> = props => {
|
||||
if (props.animationState === 'none') {
|
||||
return <React.Fragment>{props.children}</React.Fragment>;
|
||||
}
|
||||
const propsToUse = props.animationState === 'slidIn' ? props.slideInSettings : props.slideOutSettings;
|
||||
const positionSettings = props.animationState === 'slidIn' ? props.slideInSettings : props.slideOutSettings;
|
||||
return (
|
||||
<PositionAnimation position={props.position} {...propsToUse}>
|
||||
<PositionAnimation positionSettings={positionSettings} zIndex={props.zIndex}>
|
||||
{props.children}
|
||||
</PositionAnimation>
|
||||
);
|
||||
|
||||
@@ -12,11 +12,11 @@ import { balanceUtil } from '../util/balance';
|
||||
import { gasPriceEstimator } from '../util/gas_price_estimator';
|
||||
import { util } from '../util/util';
|
||||
|
||||
import { Button, Text } from './ui';
|
||||
import { Button } from './ui/button';
|
||||
|
||||
export interface BuyButtonProps {
|
||||
buyQuote?: BuyQuote;
|
||||
assetBuyer?: AssetBuyer;
|
||||
assetBuyer: AssetBuyer;
|
||||
affiliateInfo?: AffiliateInfo;
|
||||
onValidationPending: (buyQuote: BuyQuote) => void;
|
||||
onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void;
|
||||
@@ -33,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);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
import { TimedProgressBar } from '../components/timed_progress_bar';
|
||||
|
||||
import { TimeCounter } from '../components/time_counter';
|
||||
import { Container } from '../components/ui';
|
||||
import { Container } from '../components/ui/container';
|
||||
import { OrderProcessState, OrderState } from '../types';
|
||||
|
||||
export interface BuyOrderProgressProps {
|
||||
@@ -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%">
|
||||
|
||||
@@ -7,12 +7,14 @@ import { AffiliateInfo, OrderProcessState, ZeroExInstantError } from '../types';
|
||||
import { BuyButton } from './buy_button';
|
||||
import { PlacingOrderButton } from './placing_order_button';
|
||||
import { SecondaryButton } from './secondary_button';
|
||||
import { Button, Flex, Text } from './ui';
|
||||
|
||||
import { Button } from './ui/button';
|
||||
import { Flex } from './ui/flex';
|
||||
|
||||
export interface BuyOrderStateButtonProps {
|
||||
buyQuote?: BuyQuote;
|
||||
buyOrderProcessingState: OrderProcessState;
|
||||
assetBuyer?: AssetBuyer;
|
||||
assetBuyer: AssetBuyer;
|
||||
affiliateInfo?: AffiliateInfo;
|
||||
onViewTransaction: () => void;
|
||||
onValidationPending: (buyQuote: BuyQuote) => void;
|
||||
@@ -25,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
|
||||
@@ -39,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 />;
|
||||
}
|
||||
|
||||
|
||||
33
packages/instant/src/components/css_reset.tsx
Normal file
33
packages/instant/src/components/css_reset.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -8,7 +8,11 @@ import { assetUtils } from '../util/asset';
|
||||
import { util } from '../util/util';
|
||||
|
||||
import { ScalingAmountInput } from './scaling_amount_input';
|
||||
import { Container, Flex, Icon, Text } from './ui';
|
||||
|
||||
import { Container } from './ui/container';
|
||||
import { Flex } from './ui/flex';
|
||||
import { Icon } from './ui/icon';
|
||||
import { Text } from './ui/text';
|
||||
|
||||
// Asset amounts only apply to ERC20 assets
|
||||
export interface ERC20AssetAmountInputProps {
|
||||
|
||||
@@ -6,7 +6,11 @@ import { ERC20Asset } from '../types';
|
||||
import { assetUtils } from '../util/asset';
|
||||
|
||||
import { SearchInput } from './search_input';
|
||||
import { Circle, Container, Flex, Text } from './ui';
|
||||
|
||||
import { Circle } from './ui/circle';
|
||||
import { Container } from './ui/container';
|
||||
import { Flex } from './ui/flex';
|
||||
import { Text } from './ui/text';
|
||||
|
||||
export interface ERC20TokenSelectorProps {
|
||||
tokens: ERC20Asset[];
|
||||
@@ -31,7 +35,7 @@ export class ERC20TokenSelector extends React.Component<ERC20TokenSelectorProps>
|
||||
value={this.state.searchQuery}
|
||||
onChange={this._handleSearchInputChange}
|
||||
/>
|
||||
<Container overflow="scroll" height="275px" marginTop="10px">
|
||||
<Container overflow="scroll" height={{ default: '275px', sm: '75vh' }} marginTop="10px">
|
||||
{_.map(tokens, token => {
|
||||
if (!this._isTokenQueryMatch(token)) {
|
||||
return null;
|
||||
|
||||
@@ -8,7 +8,11 @@ import { AsyncProcessState, ERC20Asset, OrderProcessState, OrderState } from '..
|
||||
import { format } from '../util/format';
|
||||
|
||||
import { AmountPlaceholder } from './amount_placeholder';
|
||||
import { Container, Flex, Icon, Spinner, Text } from './ui';
|
||||
import { Container } from './ui/container';
|
||||
import { Flex } from './ui/flex';
|
||||
import { Icon } from './ui/icon';
|
||||
import { Spinner } from './ui/spinner';
|
||||
import { Text } from './ui/text';
|
||||
|
||||
export interface InstantHeadingProps {
|
||||
selectedAssetAmount?: BigNumber;
|
||||
@@ -73,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;
|
||||
@@ -85,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!';
|
||||
}
|
||||
|
||||
@@ -97,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)) {
|
||||
|
||||
@@ -8,7 +8,10 @@ import { ColorOption } from '../style/theme';
|
||||
import { format } from '../util/format';
|
||||
|
||||
import { AmountPlaceholder } from './amount_placeholder';
|
||||
import { Container, Flex, Text } from './ui';
|
||||
|
||||
import { Container } from './ui/container';
|
||||
import { Flex } from './ui/flex';
|
||||
import { Text } from './ui/text';
|
||||
|
||||
export interface OrderDetailsProps {
|
||||
buyQuoteInfo?: BuyQuoteInfo;
|
||||
@@ -23,7 +26,7 @@ export class OrderDetails extends React.Component<OrderDetailsProps> {
|
||||
const ethTokenFee = buyQuoteAccessor.feeEthAmount();
|
||||
const totalEthAmount = buyQuoteAccessor.totalEthAmount();
|
||||
return (
|
||||
<Container padding="20px" width="100%">
|
||||
<Container padding="20px" width="100%" flexGrow={1}>
|
||||
<Container marginBottom="10px">
|
||||
<Text
|
||||
letterSpacing="1px"
|
||||
|
||||
@@ -2,15 +2,15 @@ import * as React from 'react';
|
||||
|
||||
import { ColorOption } from '../style/theme';
|
||||
|
||||
import { Button, Container, Spinner, Text } from './ui';
|
||||
import { Button } from './ui/button';
|
||||
import { Container } from './ui/container';
|
||||
import { Spinner } from './ui/spinner';
|
||||
|
||||
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…
|
||||
</Text>
|
||||
Placing Order…
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from 'react';
|
||||
import { ColorOption } from '../style/theme';
|
||||
import { util } from '../util/util';
|
||||
|
||||
import { Input } from './ui';
|
||||
import { Input } from './ui/input';
|
||||
|
||||
export enum ScalingInputPhase {
|
||||
FixedFontSize,
|
||||
|
||||
@@ -3,7 +3,10 @@ import * as React from 'react';
|
||||
|
||||
import { ColorOption } from '../style/theme';
|
||||
|
||||
import { Container, Flex, Icon, Input, InputProps } from './ui';
|
||||
import { Container } from './ui/container';
|
||||
import { Flex } from './ui/flex';
|
||||
import { Icon } from './ui/icon';
|
||||
import { Input, InputProps } from './ui/input';
|
||||
|
||||
export interface SearchInputProps extends InputProps {
|
||||
backgroundColor?: ColorOption;
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
|
||||
import { ColorOption } from '../style/theme';
|
||||
|
||||
import { Button, ButtonProps, Text } from './ui';
|
||||
import { Button, ButtonProps } from './ui/button';
|
||||
|
||||
export interface SecondaryButtonProps extends ButtonProps {}
|
||||
|
||||
@@ -15,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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { ScreenSpecification } from '../style/media';
|
||||
import { ColorOption } from '../style/theme';
|
||||
import { zIndex } from '../style/z_index';
|
||||
|
||||
import { PositionAnimationSettings } from './animations/position_animation';
|
||||
import { SlideAnimation, SlideAnimationState } from './animations/slide_animation';
|
||||
|
||||
import { Container, Flex, Text } from './ui';
|
||||
import { Container } from './ui/container';
|
||||
import { Flex } from './ui/flex';
|
||||
import { Text } from './ui/text';
|
||||
|
||||
export interface ErrorProps {
|
||||
icon: string;
|
||||
@@ -19,6 +23,7 @@ export const Error: React.StatelessComponent<ErrorProps> = props => (
|
||||
backgroundColor={ColorOption.lightOrange}
|
||||
width="100%"
|
||||
borderRadius="6px"
|
||||
marginTop="10px"
|
||||
marginBottom="10px"
|
||||
>
|
||||
<Flex justify="flex-start">
|
||||
@@ -37,25 +42,51 @@ export interface SlidingErrorProps extends ErrorProps {
|
||||
}
|
||||
export const SlidingError: React.StatelessComponent<SlidingErrorProps> = props => {
|
||||
const slideAmount = '120px';
|
||||
const slideUpSettings: PositionAnimationSettings = {
|
||||
|
||||
const desktopSlideIn: PositionAnimationSettings = {
|
||||
timingFunction: 'ease-in',
|
||||
top: {
|
||||
from: slideAmount,
|
||||
to: '0px',
|
||||
},
|
||||
position: 'relative',
|
||||
};
|
||||
const slideDownSettings: PositionAnimationSettings = {
|
||||
const desktopSlideOut: PositionAnimationSettings = {
|
||||
timingFunction: 'cubic-bezier(0.25, 0.1, 0.25, 1)',
|
||||
top: {
|
||||
from: '0px',
|
||||
to: slideAmount,
|
||||
},
|
||||
position: 'relative',
|
||||
};
|
||||
|
||||
const mobileSlideIn: PositionAnimationSettings = {
|
||||
duration: '0.5s',
|
||||
timingFunction: 'ease-in',
|
||||
top: { from: '-120px', to: '0px' },
|
||||
position: 'fixed',
|
||||
};
|
||||
const moblieSlideOut: PositionAnimationSettings = {
|
||||
duration: '0.5s',
|
||||
timingFunction: 'ease-in',
|
||||
top: { from: '0px', to: '-120px' },
|
||||
position: 'fixed',
|
||||
};
|
||||
|
||||
const slideUpSettings: ScreenSpecification<PositionAnimationSettings> = {
|
||||
default: desktopSlideIn,
|
||||
sm: mobileSlideIn,
|
||||
};
|
||||
const slideOutSettings: ScreenSpecification<PositionAnimationSettings> = {
|
||||
default: desktopSlideOut,
|
||||
sm: moblieSlideOut,
|
||||
};
|
||||
|
||||
return (
|
||||
<SlideAnimation
|
||||
position="relative"
|
||||
slideInSettings={slideUpSettings}
|
||||
slideOutSettings={slideDownSettings}
|
||||
slideOutSettings={slideOutSettings}
|
||||
zIndex={{ sm: zIndex.errorPopUp, default: zIndex.errorPopBehind }}
|
||||
animationState={props.animationState}
|
||||
>
|
||||
<Error icon={props.icon} message={props.message} />
|
||||
|
||||
@@ -5,7 +5,11 @@ import { zIndex } from '../style/z_index';
|
||||
|
||||
import { PositionAnimationSettings } from './animations/position_animation';
|
||||
import { SlideAnimation, SlideAnimationState } from './animations/slide_animation';
|
||||
import { Container, Flex, Icon, Text } from './ui';
|
||||
|
||||
import { Container } from './ui/container';
|
||||
import { Flex } from './ui/flex';
|
||||
import { Icon } from './ui/icon';
|
||||
import { Text } from './ui/text';
|
||||
|
||||
export interface PanelProps {
|
||||
title?: string;
|
||||
@@ -47,6 +51,7 @@ export const SlidingPanel: React.StatelessComponent<SlidingPanelProps> = props =
|
||||
from: slideAmount,
|
||||
to: '0px',
|
||||
},
|
||||
position: 'absolute',
|
||||
};
|
||||
const slideDownSettings: PositionAnimationSettings = {
|
||||
duration: '0.3s',
|
||||
@@ -55,10 +60,10 @@ export const SlidingPanel: React.StatelessComponent<SlidingPanelProps> = props =
|
||||
from: '0px',
|
||||
to: slideAmount,
|
||||
},
|
||||
position: 'absolute',
|
||||
};
|
||||
return (
|
||||
<SlideAnimation
|
||||
position="absolute"
|
||||
slideInSettings={slideUpSettings}
|
||||
slideOutSettings={slideDownSettings}
|
||||
animationState={animationState}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -9,10 +9,12 @@ export const Circle =
|
||||
styled.div <
|
||||
CircleProps >
|
||||
`
|
||||
width: ${props => props.diameter}px;
|
||||
height: ${props => props.diameter}px;
|
||||
background-color: ${props => props.fillColor};
|
||||
border-radius: 50%;
|
||||
&& {
|
||||
width: ${props => props.diameter}px;
|
||||
height: ${props => props.diameter}px;
|
||||
background-color: ${props => props.fillColor};
|
||||
border-radius: 50%;
|
||||
}
|
||||
`;
|
||||
|
||||
Circle.displayName = 'Circle';
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { darken } from 'polished';
|
||||
|
||||
import { MediaChoice, stylesForMedia } from '../../style/media';
|
||||
import { ColorOption, styled } from '../../style/theme';
|
||||
import { cssRuleIfExists } from '../../style/util';
|
||||
|
||||
export interface ContainerProps {
|
||||
display?: string;
|
||||
display?: MediaChoice;
|
||||
position?: string;
|
||||
top?: string;
|
||||
right?: string;
|
||||
bottom?: string;
|
||||
left?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
width?: MediaChoice;
|
||||
height?: MediaChoice;
|
||||
maxWidth?: string;
|
||||
margin?: string;
|
||||
marginTop?: string;
|
||||
@@ -33,47 +34,52 @@ export interface ContainerProps {
|
||||
cursor?: string;
|
||||
overflow?: string;
|
||||
darkenOnHover?: boolean;
|
||||
flexGrow?: string | number;
|
||||
}
|
||||
|
||||
export const Container =
|
||||
styled.div <
|
||||
ContainerProps >
|
||||
`
|
||||
box-sizing: border-box;
|
||||
${props => cssRuleIfExists(props, 'display')}
|
||||
${props => cssRuleIfExists(props, 'position')}
|
||||
${props => cssRuleIfExists(props, 'top')}
|
||||
${props => cssRuleIfExists(props, 'right')}
|
||||
${props => cssRuleIfExists(props, 'bottom')}
|
||||
${props => cssRuleIfExists(props, 'left')}
|
||||
${props => cssRuleIfExists(props, 'width')}
|
||||
${props => cssRuleIfExists(props, 'height')}
|
||||
${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)` : '')};
|
||||
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<string>('display', props.display)}
|
||||
${props => props.width && stylesForMedia<string>('width', props.width)}
|
||||
${props => props.height && stylesForMedia<string>('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'
|
||||
}`
|
||||
: ''};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MediaChoice, stylesForMedia } from '../../style/media';
|
||||
import { ColorOption, styled } from '../../style/theme';
|
||||
import { cssRuleIfExists } from '../../style/util';
|
||||
|
||||
@@ -6,24 +7,29 @@ export interface FlexProps {
|
||||
flexWrap?: 'wrap' | 'nowrap';
|
||||
justify?: 'flex-start' | 'center' | 'space-around' | 'space-between' | 'space-evenly' | 'flex-end';
|
||||
align?: 'flex-start' | 'center' | 'space-around' | 'space-between' | 'space-evenly' | 'flex-end';
|
||||
width?: string;
|
||||
height?: string;
|
||||
width?: MediaChoice;
|
||||
height?: MediaChoice;
|
||||
backgroundColor?: ColorOption;
|
||||
inline?: boolean;
|
||||
flexGrow?: number | string;
|
||||
}
|
||||
|
||||
export const Flex =
|
||||
styled.div <
|
||||
FlexProps >
|
||||
`
|
||||
display: ${props => (props.inline ? 'inline-flex' : 'flex')};
|
||||
flex-direction: ${props => props.direction};
|
||||
flex-wrap: ${props => props.flexWrap};
|
||||
justify-content: ${props => props.justify};
|
||||
align-items: ${props => props.align};
|
||||
${props => cssRuleIfExists(props, 'width')}
|
||||
${props => cssRuleIfExists(props, 'height')}
|
||||
background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')};
|
||||
&& {
|
||||
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 = {
|
||||
|
||||
@@ -101,15 +101,17 @@ const PlainIcon: React.StatelessComponent<IconProps> = props => {
|
||||
};
|
||||
|
||||
export const Icon = withTheme(styled(PlainIcon)`
|
||||
cursor: ${props => (!_.isUndefined(props.onClick) ? 'pointer' : 'default')};
|
||||
transition: opacity 0.5s ease;
|
||||
padding: ${props => props.padding};
|
||||
opacity: ${props => (!_.isUndefined(props.onClick) ? 0.7 : 1)};
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
&:active {
|
||||
opacity: 1;
|
||||
&& {
|
||||
cursor: ${props => (!_.isUndefined(props.onClick) ? 'pointer' : 'default')};
|
||||
transition: opacity 0.5s ease;
|
||||
padding: ${props => props.padding};
|
||||
opacity: ${props => (!_.isUndefined(props.onClick) ? 0.7 : 1)};
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
&:active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export { Text, TextProps, Title } from './text';
|
||||
export { Circle, CircleProps } from './circle';
|
||||
export { Button, ButtonProps } from './button';
|
||||
export { Flex, FlexProps } from './flex';
|
||||
export { Container, ContainerProps } from './container';
|
||||
export { Input, InputProps } from './input';
|
||||
export { Icon, IconProps } from './icon';
|
||||
export { Spinner, SpinnerProps } from './spinner';
|
||||
export { Overlay, OverlayProps } from './overlay';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -15,20 +15,24 @@ export interface OverlayProps {
|
||||
|
||||
const PlainOverlay: React.StatelessComponent<OverlayProps> = ({ children, className, onClose }) => (
|
||||
<Flex height="100vh" className={className}>
|
||||
<Container position="absolute" top="0px" right="0px">
|
||||
<Container position="absolute" top="0px" right="0px" display={{ default: 'initial', sm: 'none' }}>
|
||||
<Icon height={18} width={18} color={ColorOption.white} icon="closeX" onClick={onClose} padding="2em 2em" />
|
||||
</Container>
|
||||
<div>{children}</div>
|
||||
<Container width={{ default: 'auto', sm: '100%' }} height={{ default: 'auto', sm: '100%' }}>
|
||||
{children}
|
||||
</Container>
|
||||
</Flex>
|
||||
);
|
||||
export const Overlay = styled(PlainOverlay)`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: ${props => props.zIndex}
|
||||
background-color: ${overlayBlack};
|
||||
&& {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: ${props => props.zIndex}
|
||||
background-color: ${overlayBlack};
|
||||
}
|
||||
`;
|
||||
|
||||
Overlay.defaultProps = {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,8 +12,11 @@ import { ColorOption } from '../style/theme';
|
||||
import { zIndex } from '../style/z_index';
|
||||
|
||||
import { SlideAnimationState } from './animations/slide_animation';
|
||||
import { CSSReset } from './css_reset';
|
||||
import { SlidingPanel } from './sliding_panel';
|
||||
import { Container, Flex } from './ui';
|
||||
import { Container } from './ui/container';
|
||||
import { Flex } from './ui/flex';
|
||||
|
||||
export interface ZeroExInstantContainerProps {}
|
||||
export interface ZeroExInstantContainerState {
|
||||
tokenSelectionPanelAnimationState: SlideAnimationState;
|
||||
@@ -25,35 +28,43 @@ export class ZeroExInstantContainer extends React.Component<ZeroExInstantContain
|
||||
};
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<Container width="350px" 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"
|
||||
>
|
||||
<Flex direction="column" justify="flex-start">
|
||||
<SelectedAssetInstantHeading onSelectAssetClick={this._handleSymbolClick} />
|
||||
<SelectedAssetBuyOrderProgress />
|
||||
<LatestBuyQuoteOrderDetails />
|
||||
<Container padding="20px" width="100%">
|
||||
<SelectedAssetBuyOrderStateButtons />
|
||||
</Container>
|
||||
</Flex>
|
||||
<SlidingPanel
|
||||
title="Select Token"
|
||||
animationState={this.state.tokenSelectionPanelAnimationState}
|
||||
onClose={this._handlePanelClose}
|
||||
<Container 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 />
|
||||
<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 => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Overlay } from './ui';
|
||||
import { Overlay } from './ui/overlay';
|
||||
import { ZeroExInstantContainer } from './zero_ex_instant_container';
|
||||
import { ZeroExInstantProvider, ZeroExInstantProviderProps } from './zero_ex_instant_provider';
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -99,16 +92,15 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
asyncData.fetchAvailableAssetDatasAndDispatchToStore(this._store);
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
asyncData.fetchCurrentBuyQuoteAndDispatchToStore(this._store);
|
||||
// warm up the gas price estimator cache just in case we can't
|
||||
// grab the gas price estimate when submitting the transaction
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
gasPriceEstimator.getGasInfoAsync();
|
||||
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer';
|
||||
import { AssetBuyer } from '@0x/asset-buyer';
|
||||
import { AssetProxyId } from '@0x/types';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { Web3Wrapper } from '@0x/web3-wrapper';
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import { oc } from 'ts-optchain';
|
||||
|
||||
import { ERC20AssetAmountInput } from '../components/erc20_asset_amount_input';
|
||||
import { Action, actions } from '../redux/actions';
|
||||
import { State } from '../redux/reducer';
|
||||
import { ColorOption } from '../style/theme';
|
||||
import { AffiliateInfo, ERC20Asset, OrderProcessState } from '../types';
|
||||
import { assetUtils } from '../util/asset';
|
||||
import { errorFlasher } from '../util/error_flasher';
|
||||
import { buyQuoteUpdater } from '../util/buy_quote_updater';
|
||||
|
||||
export interface SelectedERC20AssetAmountInputProps {
|
||||
fontColor?: ColorOption;
|
||||
@@ -23,7 +20,7 @@ export interface SelectedERC20AssetAmountInputProps {
|
||||
}
|
||||
|
||||
interface ConnectedState {
|
||||
assetBuyer?: AssetBuyer;
|
||||
assetBuyer: AssetBuyer;
|
||||
value?: BigNumber;
|
||||
asset?: ERC20Asset;
|
||||
isDisabled: boolean;
|
||||
@@ -33,7 +30,7 @@ interface ConnectedState {
|
||||
|
||||
interface ConnectedDispatch {
|
||||
updateBuyQuote: (
|
||||
assetBuyer?: AssetBuyer,
|
||||
assetBuyer: AssetBuyer,
|
||||
value?: BigNumber,
|
||||
asset?: ERC20Asset,
|
||||
affiliateInfo?: AffiliateInfo,
|
||||
@@ -52,15 +49,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,
|
||||
@@ -69,52 +67,9 @@ const mapStateToProps = (state: State, _ownProps: SelectedERC20AssetAmountInputP
|
||||
};
|
||||
};
|
||||
|
||||
const updateBuyQuoteAsync = async (
|
||||
assetBuyer: AssetBuyer,
|
||||
dispatch: Dispatch<Action>,
|
||||
asset: ERC20Asset,
|
||||
assetAmount: BigNumber,
|
||||
affiliateInfo?: AffiliateInfo,
|
||||
): Promise<void> => {
|
||||
// get a new buy quote.
|
||||
const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, asset.metaData.decimals);
|
||||
|
||||
// mark quote as pending
|
||||
dispatch(actions.setQuoteRequestStatePending());
|
||||
|
||||
const feePercentage = oc(affiliateInfo).feePercentage();
|
||||
let newBuyQuote: BuyQuote | undefined;
|
||||
try {
|
||||
newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue, { feePercentage });
|
||||
} catch (error) {
|
||||
dispatch(actions.setQuoteRequestStateFailure());
|
||||
let errorMessage;
|
||||
if (error.message === AssetBuyerError.InsufficientAssetLiquidity) {
|
||||
const assetName = assetUtils.bestNameForAsset(asset, 'of this asset');
|
||||
errorMessage = `Not enough ${assetName} available`;
|
||||
} else if (error.message === AssetBuyerError.InsufficientZrxLiquidity) {
|
||||
errorMessage = 'Not enough ZRX available';
|
||||
} else if (
|
||||
error.message === AssetBuyerError.StandardRelayerApiError ||
|
||||
error.message.startsWith(AssetBuyerError.AssetUnavailable)
|
||||
) {
|
||||
const assetName = assetUtils.bestNameForAsset(asset, 'This asset');
|
||||
errorMessage = `${assetName} is currently unavailable`;
|
||||
}
|
||||
if (!_.isUndefined(errorMessage)) {
|
||||
errorFlasher.flashNewErrorMessage(dispatch, errorMessage);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// We have a successful new buy quote
|
||||
errorFlasher.clearError(dispatch);
|
||||
// invalidate the last buy quote.
|
||||
dispatch(actions.updateLatestBuyQuote(newBuyQuote));
|
||||
};
|
||||
|
||||
const debouncedUpdateBuyQuoteAsync = _.debounce(updateBuyQuoteAsync, 200, { trailing: true });
|
||||
const debouncedUpdateBuyQuoteAsync = _.debounce(buyQuoteUpdater.updateBuyQuoteAsync.bind(buyQuoteUpdater), 200, {
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: Dispatch<Action>,
|
||||
@@ -128,7 +83,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
|
||||
|
||||
@@ -26,7 +26,8 @@ export const assetDataNetworkMapping: AssetDataByNetwork[] = [
|
||||
// MKR
|
||||
{
|
||||
[Network.Mainnet]: '0xf47261b00000000000000000000000009f8f72aa9304c8b593d555f12ef6589cc3a579a2',
|
||||
[Network.Kovan]: '0xf47261b00000000000000000000000000xbf5e8e38659562fda594fbb3ec5a3576d02a9e0a',
|
||||
// 0x Kovan MKR
|
||||
[Network.Kovan]: '0xf47261b00000000000000000000000007b6b10caa9e8e9552ba72638ea5b47c25afea1f3',
|
||||
},
|
||||
// BAT
|
||||
{
|
||||
@@ -45,8 +46,9 @@ export const assetDataNetworkMapping: AssetDataByNetwork[] = [
|
||||
},
|
||||
// GNT
|
||||
{
|
||||
[Network.Mainnet]: '0xf47261b0000000000000000000000000a74476443119A942dE498590Fe1f2454d7D4aC0d',
|
||||
[Network.Kovan]: '0xf47261b00000000000000000000000006986fa3646f408905ecb1876bfd355d25039ee3a',
|
||||
[Network.Mainnet]: '0xf47261b0000000000000000000000000a74476443119a942de498590fe1f2454d7d4ac0d',
|
||||
// 0x Kovan GNT
|
||||
[Network.Kovan]: '0xf47261b000000000000000000000000031fb614e223706f15d0d3c5f4b08bdf0d5c78623',
|
||||
},
|
||||
// SUB
|
||||
{
|
||||
|
||||
@@ -54,7 +54,7 @@ export const assetMetaDataMap: ObjectMap<AssetMetaData> = {
|
||||
symbol: 'mana',
|
||||
name: 'Decentraland',
|
||||
},
|
||||
'0xf47261b0000000000000000000000000a74476443119A942dE498590Fe1f2454d7D4aC0d': {
|
||||
'0xf47261b0000000000000000000000000a74476443119a942de498590fe1f2454d7d4ac0d': {
|
||||
assetProxyId: AssetProxyId.ERC20,
|
||||
decimals: 18,
|
||||
primaryColor: '#263469',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { AssetProxyId } from '@0x/types';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { BIG_NUMBER_ZERO } from '../constants';
|
||||
import { ERC20Asset } from '../types';
|
||||
import { assetUtils } from '../util/asset';
|
||||
import { buyQuoteUpdater } from '../util/buy_quote_updater';
|
||||
import { coinbaseApi } from '../util/coinbase_api';
|
||||
import { errorFlasher } from '../util/error_flasher';
|
||||
|
||||
@@ -20,18 +23,34 @@ 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([]));
|
||||
}
|
||||
},
|
||||
fetchCurrentBuyQuoteAndDispatchToStore: async (store: Store) => {
|
||||
const { providerState, selectedAsset, selectedAssetAmount, affiliateInfo } = store.getState();
|
||||
const assetBuyer = providerState.assetBuyer;
|
||||
if (
|
||||
!_.isUndefined(selectedAssetAmount) &&
|
||||
!_.isUndefined(selectedAsset) &&
|
||||
selectedAsset.metaData.assetProxyId === AssetProxyId.ERC20
|
||||
) {
|
||||
await buyQuoteUpdater.updateBuyQuoteAsync(
|
||||
assetBuyer,
|
||||
store.dispatch,
|
||||
selectedAsset as ERC20Asset,
|
||||
selectedAssetAmount,
|
||||
affiliateInfo,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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({}));
|
||||
},
|
||||
};
|
||||
|
||||
51
packages/instant/src/style/media.ts
Normal file
51
packages/instant/src/style/media.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { InterpolationValue } from 'styled-components';
|
||||
|
||||
import { css } from './theme';
|
||||
|
||||
export enum ScreenWidths {
|
||||
Sm = 40,
|
||||
Md = 52,
|
||||
Lg = 64,
|
||||
}
|
||||
|
||||
const generateMediaWrapper = (screenWidth: ScreenWidths) => (...args: any[]) => css`
|
||||
@media (max-width: ${screenWidth}em) {
|
||||
${css.apply(css, args)};
|
||||
}
|
||||
`;
|
||||
|
||||
export const media = {
|
||||
small: generateMediaWrapper(ScreenWidths.Sm),
|
||||
medium: generateMediaWrapper(ScreenWidths.Md),
|
||||
large: generateMediaWrapper(ScreenWidths.Lg),
|
||||
};
|
||||
|
||||
export interface ScreenSpecification<T> {
|
||||
default: T;
|
||||
sm?: T;
|
||||
md?: T;
|
||||
lg?: T;
|
||||
}
|
||||
export type OptionallyScreenSpecific<T> = T | ScreenSpecification<T>;
|
||||
export type MediaChoice = OptionallyScreenSpecific<string>;
|
||||
/**
|
||||
* Given a css property name and a OptionallyScreenSpecific value,
|
||||
* generates css properties with screen-specific viewport styling
|
||||
*/
|
||||
export function stylesForMedia<T extends string | number>(
|
||||
cssPropertyName: string,
|
||||
choice: OptionallyScreenSpecific<T>,
|
||||
): InterpolationValue[] {
|
||||
if (typeof choice === 'object') {
|
||||
return css`
|
||||
${cssPropertyName}: ${choice.default};
|
||||
${choice.lg && media.large`${cssPropertyName}: ${choice.lg}`}
|
||||
${choice.md && media.medium`${cssPropertyName}: ${choice.md}`}
|
||||
${choice.sm && media.small`${cssPropertyName}: ${choice.sm}`}
|
||||
`;
|
||||
} else {
|
||||
return css`
|
||||
${cssPropertyName}: ${choice};
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -33,4 +33,4 @@ export const theme: Theme = {
|
||||
export const transparentWhite = 'rgba(255,255,255,0.3)';
|
||||
export const overlayBlack = 'rgba(0, 0, 0, 0.6)';
|
||||
|
||||
export { styled, css, keyframes, withTheme, ThemeProvider };
|
||||
export { styled, css, keyframes, withTheme, createGlobalStyle, ThemeProvider };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const zIndex = {
|
||||
errorPopup: 1,
|
||||
errorPopBehind: 1,
|
||||
mainContainer: 2,
|
||||
panel: 3,
|
||||
errorPopUp: 4,
|
||||
};
|
||||
|
||||
@@ -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[];
|
||||
|
||||
17
packages/instant/src/util/asset_buyer_factory.ts
Normal file
17
packages/instant/src/util/asset_buyer_factory.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
56
packages/instant/src/util/buy_quote_updater.ts
Normal file
56
packages/instant/src/util/buy_quote_updater.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer';
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import { Web3Wrapper } from '@0x/web3-wrapper';
|
||||
import * as _ from 'lodash';
|
||||
import { Dispatch } from 'redux';
|
||||
import { oc } from 'ts-optchain';
|
||||
|
||||
import { Action, actions } from '../redux/actions';
|
||||
import { AffiliateInfo, ERC20Asset } from '../types';
|
||||
import { assetUtils } from '../util/asset';
|
||||
import { errorFlasher } from '../util/error_flasher';
|
||||
|
||||
export const buyQuoteUpdater = {
|
||||
updateBuyQuoteAsync: async (
|
||||
assetBuyer: AssetBuyer,
|
||||
dispatch: Dispatch<Action>,
|
||||
asset: ERC20Asset,
|
||||
assetAmount: BigNumber,
|
||||
affiliateInfo?: AffiliateInfo,
|
||||
): Promise<void> => {
|
||||
// get a new buy quote.
|
||||
const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, asset.metaData.decimals);
|
||||
// mark quote as pending
|
||||
dispatch(actions.setQuoteRequestStatePending());
|
||||
const feePercentage = oc(affiliateInfo).feePercentage();
|
||||
let newBuyQuote: BuyQuote | undefined;
|
||||
try {
|
||||
newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue, { feePercentage });
|
||||
} catch (error) {
|
||||
dispatch(actions.setQuoteRequestStateFailure());
|
||||
let errorMessage;
|
||||
if (error.message === AssetBuyerError.InsufficientAssetLiquidity) {
|
||||
const assetName = assetUtils.bestNameForAsset(asset, 'of this asset');
|
||||
errorMessage = `Not enough ${assetName} available`;
|
||||
} else if (error.message === AssetBuyerError.InsufficientZrxLiquidity) {
|
||||
errorMessage = 'Not enough ZRX available';
|
||||
} else if (
|
||||
error.message === AssetBuyerError.StandardRelayerApiError ||
|
||||
error.message.startsWith(AssetBuyerError.AssetUnavailable)
|
||||
) {
|
||||
const assetName = assetUtils.bestNameForAsset(asset, 'This asset');
|
||||
errorMessage = `${assetName} is currently unavailable`;
|
||||
}
|
||||
if (!_.isUndefined(errorMessage)) {
|
||||
errorFlasher.flashNewErrorMessage(dispatch, errorMessage);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// We have a successful new buy quote
|
||||
errorFlasher.clearError(dispatch);
|
||||
// invalidate the last buy quote.
|
||||
dispatch(actions.updateLatestBuyQuote(newBuyQuote));
|
||||
},
|
||||
};
|
||||
@@ -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`);
|
||||
}
|
||||
};
|
||||
34
packages/instant/src/util/provider_factory.ts
Normal file
34
packages/instant/src/util/provider_factory.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
69
packages/instant/src/util/provider_state_factory.ts
Normal file
69
packages/instant/src/util/provider_state_factory.ts
Normal 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;
|
||||
},
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import { schemas as jsonSchemas } from '@0x/json-schemas';
|
||||
|
||||
// Only include schemas we actually need
|
||||
const {
|
||||
wholeNumberSchema,
|
||||
numberSchema,
|
||||
addressSchema,
|
||||
hexSchema,
|
||||
@@ -28,6 +29,7 @@ const {
|
||||
} = jsonSchemas;
|
||||
|
||||
const usedSchemas = {
|
||||
wholeNumberSchema,
|
||||
numberSchema,
|
||||
addressSchema,
|
||||
hexSchema,
|
||||
|
||||
@@ -11,4 +11,27 @@ BigNumber.config({
|
||||
DECIMAL_PLACES: 78,
|
||||
});
|
||||
|
||||
// Set a debug print function for NodeJS
|
||||
// Upstream issue: https://github.com/MikeMcl/bignumber.js/issues/188
|
||||
import isNode = require('detect-node');
|
||||
if (isNode) {
|
||||
// Dynamically load a NodeJS specific module.
|
||||
// Typescript requires all imports to be global, so we need to use
|
||||
// `const` here and disable the tslint warning.
|
||||
// tslint:disable-next-line: no-var-requires
|
||||
const util = require('util');
|
||||
|
||||
// Set a custom util.inspect function
|
||||
// HACK: We add a function to the BigNumber class by assigning to the
|
||||
// prototype. The function name is a symbol provided by Node.
|
||||
(BigNumber.prototype as any)[util.inspect.custom] = function(): string {
|
||||
// HACK: When executed, `this` will refer to the BigNumber instance.
|
||||
// This is also why we need a function expression instead of an
|
||||
// arrow function, as the latter does not have a `this`.
|
||||
// Return the readable string representation
|
||||
// tslint:disable-next-line: no-invalid-this
|
||||
return this.toString();
|
||||
};
|
||||
}
|
||||
|
||||
export { BigNumber };
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -18,7 +18,7 @@ Please read our [contribution guidelines](../../CONTRIBUTING.md) before getting
|
||||
|
||||
### Install Code and Dependencies
|
||||
|
||||
Ensure that you have Python >=3.6 installed, then:
|
||||
Ensure that you have installed Python >=3.6 and Docker. Then:
|
||||
|
||||
```bash
|
||||
pip install -e .[dev]
|
||||
@@ -26,7 +26,7 @@ pip install -e .[dev]
|
||||
|
||||
### Test
|
||||
|
||||
`./setup.py test`
|
||||
Tests depend on a running ganache instance with the 0x contracts deployed in it. For convenience, a docker container is provided that has ganache-cli and a snapshot containing the necessary contracts. A shortcut is provided to run that docker container: `./setup.py ganache`. With that running, the tests can be run with `./setup.py test`.
|
||||
|
||||
### Clean
|
||||
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
import subprocess # nosec
|
||||
from shutil import rmtree
|
||||
from os import environ, path
|
||||
from pathlib import Path
|
||||
from sys import argv
|
||||
|
||||
from distutils.command.clean import clean
|
||||
import distutils.command.build_py
|
||||
from setuptools import setup
|
||||
from setuptools import find_packages, setup
|
||||
from setuptools.command.test import test as TestCommand
|
||||
|
||||
|
||||
@@ -59,8 +60,15 @@ class LintCommand(distutils.command.build_py.build_py):
|
||||
import eth_abi
|
||||
|
||||
eth_abi_dir = path.dirname(path.realpath(eth_abi.__file__))
|
||||
with open(path.join(eth_abi_dir, "py.typed"), "a"):
|
||||
pass
|
||||
Path(path.join(eth_abi_dir, "py.typed")).touch()
|
||||
|
||||
# HACK(gene): until eth_utils fixes
|
||||
# https://github.com/ethereum/eth-utils/issues/140 , we need to simply
|
||||
# create an empty file `py.typed` in the eth_abi package directory.
|
||||
import eth_utils
|
||||
|
||||
eth_utils_dir = path.dirname(path.realpath(eth_utils.__file__))
|
||||
Path(path.join(eth_utils_dir, "py.typed")).touch()
|
||||
|
||||
for lint_command in lint_commands:
|
||||
print(
|
||||
@@ -79,7 +87,7 @@ class CleanCommandExtension(clean):
|
||||
rmtree(".mypy_cache", ignore_errors=True)
|
||||
rmtree(".tox", ignore_errors=True)
|
||||
rmtree(".pytest_cache", ignore_errors=True)
|
||||
rmtree("src/order_utils.egg-info", ignore_errors=True)
|
||||
rmtree("src/0x_order_utils.egg-info", ignore_errors=True)
|
||||
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
@@ -111,6 +119,26 @@ class PublishCommand(distutils.command.build_py.build_py):
|
||||
subprocess.check_call("twine upload dist/*".split()) # nosec
|
||||
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
class GanacheCommand(distutils.command.build_py.build_py):
|
||||
"""Custom command to publish to pypi.org."""
|
||||
|
||||
description = "Run ganache daemon to support tests."
|
||||
|
||||
def run(self):
|
||||
"""Run ganache."""
|
||||
cmd_line = (
|
||||
"docker run -d -p 8545:8545 0xorg/ganache-cli --gasLimit"
|
||||
+ " 10000000 --db /snapshot --noVMErrorsOnRPCResponse -p 8545"
|
||||
+ " --networkId 50 -m"
|
||||
).split()
|
||||
cmd_line.append(
|
||||
"concert load couple harbor equip island argue ramp clarify fence"
|
||||
+ " smart topic"
|
||||
)
|
||||
subprocess.call(cmd_line) # nosec
|
||||
|
||||
|
||||
with open("README.md", "r") as file_handle:
|
||||
README_MD = file_handle.read()
|
||||
|
||||
@@ -130,9 +158,9 @@ setup(
|
||||
"test": TestCommandExtension,
|
||||
"test_publish": TestPublishCommand,
|
||||
"publish": PublishCommand,
|
||||
"ganache": GanacheCommand,
|
||||
},
|
||||
include_package_data=True,
|
||||
install_requires=["eth-abi", "mypy_extensions", "web3"],
|
||||
install_requires=["eth-abi", "eth_utils", "mypy_extensions", "web3"],
|
||||
extras_require={
|
||||
"dev": [
|
||||
"bandit",
|
||||
@@ -151,14 +179,17 @@ setup(
|
||||
]
|
||||
},
|
||||
python_requires=">=3.6, <4",
|
||||
package_data={"zero_ex.order_utils": ["py.typed"]},
|
||||
package_data={
|
||||
"zero_ex.order_utils": ["py.typed"],
|
||||
"zero_ex.contract_artifacts": ["artifacts/*"],
|
||||
},
|
||||
package_dir={"": "src"},
|
||||
license="Apache 2.0",
|
||||
keywords=(
|
||||
"ethereum cryptocurrency 0x decentralized blockchain dex exchange"
|
||||
),
|
||||
namespace_packages=["zero_ex"],
|
||||
packages=["zero_ex.order_utils", "zero_ex.dev_utils"],
|
||||
packages=find_packages("src"),
|
||||
classifiers=[
|
||||
"Development Status :: 2 - Pre-Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# Reference: http://www.sphinx-doc.org/en/master/config
|
||||
|
||||
from typing import List
|
||||
import pkg_resources
|
||||
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@@ -12,7 +13,7 @@ project = "0x-order-utils"
|
||||
# pylint: disable=redefined-builtin
|
||||
copyright = "2018, ZeroEx, Intl."
|
||||
author = "F. Eugene Aumson"
|
||||
version = "0.1.0" # The short X.Y version
|
||||
version = pkg_resources.get_distribution("0x-order-utils").version
|
||||
release = "" # The full version, including alpha/beta/rc tags
|
||||
|
||||
extensions = [
|
||||
|
||||
@@ -19,6 +19,9 @@ Python zero_ex.order_utils
|
||||
|
||||
See source for class properties. Sphinx does not easily generate class property docs; pull requests welcome.
|
||||
|
||||
.. automodule:: zero_ex.order_utils.signature_utils
|
||||
:members:
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Solc-generated artifacts for 0x smart contracts."""
|
||||
@@ -0,0 +1 @@
|
||||
../../../../../packages/contract-artifacts/artifacts
|
||||
@@ -46,3 +46,13 @@ def assert_is_int(value: Any, name: str) -> None:
|
||||
f"expected variable '{name}', with value {str(value)}, to have"
|
||||
+ f" type 'int', not '{type(value).__name__}'"
|
||||
)
|
||||
|
||||
|
||||
def assert_is_hex_string(value: Any, name: str) -> None:
|
||||
"""Assert that :param value: is a string of hex chars.
|
||||
|
||||
If :param value: isn't a str, raise a TypeError. If it is a string but
|
||||
contains non-hex characters ("0x" prefix permitted), raise a ValueError.
|
||||
"""
|
||||
assert_is_string(value, name)
|
||||
int(value, 16) # raises a ValueError if value isn't a base-16 str
|
||||
|
||||
@@ -1 +1,11 @@
|
||||
"""Order utilities for 0x applications."""
|
||||
"""Order utilities for 0x applications.
|
||||
|
||||
Some methods require the caller to pass in a `Web3.HTTPProvider` object. For
|
||||
local testing one may construct such a provider pointing at an instance of
|
||||
`ganache-cli <https://www.npmjs.com/package/ganache-cli>`_ which has the 0x
|
||||
contracts deployed on it. For convenience, a docker container is provided for
|
||||
just this purpose. To start it: ``docker run -d -p 8545:8545 0xorg/ganache-cli
|
||||
--gasLimit 10000000 --db /snapshot --noVMErrorsOnRPCResponse -p 8545
|
||||
--networkId 50 -m "concert load couple harbor equip island argue ramp clarify
|
||||
fence smart topic"``.
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Signature utilities."""
|
||||
|
||||
from typing import Dict, Tuple
|
||||
import json
|
||||
from pkg_resources import resource_string
|
||||
|
||||
from eth_utils import is_address, to_checksum_address
|
||||
from web3 import Web3
|
||||
import web3.exceptions
|
||||
from web3.utils import datatypes
|
||||
|
||||
from zero_ex.dev_utils.type_assertions import assert_is_hex_string
|
||||
|
||||
|
||||
# prefer `black` formatting. pylint: disable=C0330
|
||||
EXCHANGE_ABI = json.loads(
|
||||
resource_string("zero_ex.contract_artifacts", "artifacts/Exchange.json")
|
||||
)["compilerOutput"]["abi"]
|
||||
|
||||
network_to_exchange_addr: Dict[str, str] = {
|
||||
"1": "0x4f833a24e1f95d70f028921e27040ca56e09ab0b",
|
||||
"3": "0x4530c0483a1633c7a1c97d2c53721caff2caaaaf",
|
||||
"42": "0x35dd2932454449b14cee11a94d3674a936d5d7b2",
|
||||
"50": "0x48bacb9266a570d521063ef5dd96e61686dbe788",
|
||||
}
|
||||
|
||||
|
||||
# prefer `black` formatting. pylint: disable=C0330
|
||||
def is_valid_signature(
|
||||
provider: Web3.HTTPProvider, data: str, signature: str, signer_address: str
|
||||
) -> Tuple[bool, str]:
|
||||
# docstring considered all one line by pylint: disable=line-too-long
|
||||
"""Check the validity of the supplied signature.
|
||||
|
||||
Check if the supplied ``signature`` corresponds to signing ``data`` with
|
||||
the private key corresponding to ``signer_address``.
|
||||
|
||||
:param provider: A Web3 provider able to access the 0x Exchange contract.
|
||||
:param data: The hex encoded data signed by the supplied signature.
|
||||
:param signature: The hex encoded signature.
|
||||
:param signer_address: The hex encoded address that signed the data to
|
||||
produce the supplied signature.
|
||||
:rtype: Boolean indicating whether the given signature is valid.
|
||||
|
||||
>>> is_valid_signature(
|
||||
... Web3.HTTPProvider("http://127.0.0.1:8545"),
|
||||
... '0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0',
|
||||
... '0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace225403',
|
||||
... '0x5409ed021d9299bf6814279a6a1411a7e866a631',
|
||||
... )
|
||||
(True, '')
|
||||
""" # noqa: E501 (line too long)
|
||||
# TODO: make this provider check more flexible. pylint: disable=fixme
|
||||
# https://app.asana.com/0/684263176955174/901300863045491/f
|
||||
if not isinstance(provider, Web3.HTTPProvider):
|
||||
raise TypeError("provider is not a Web3.HTTPProvider")
|
||||
assert_is_hex_string(data, "data")
|
||||
assert_is_hex_string(signature, "signature")
|
||||
assert_is_hex_string(signer_address, "signer_address")
|
||||
if not is_address(signer_address):
|
||||
raise ValueError("signer_address is not a valid address")
|
||||
|
||||
web3_instance = Web3(provider)
|
||||
# false positive from pylint: disable=no-member
|
||||
network_id = web3_instance.net.version
|
||||
contract_address = network_to_exchange_addr[network_id]
|
||||
# false positive from pylint: disable=no-member
|
||||
contract: datatypes.Contract = web3_instance.eth.contract(
|
||||
address=to_checksum_address(contract_address), abi=EXCHANGE_ABI
|
||||
)
|
||||
try:
|
||||
return (
|
||||
contract.call().isValidSignature(
|
||||
data, to_checksum_address(signer_address), signature
|
||||
),
|
||||
"",
|
||||
)
|
||||
except web3.exceptions.BadFunctionCallOutput as exception:
|
||||
known_revert_reasons = [
|
||||
"LENGTH_GREATER_THAN_0_REQUIRED",
|
||||
"SIGNATURE_UNSUPPORTED",
|
||||
"LENGTH_0_REQUIRED",
|
||||
"LENGTH_65_REQUIRED",
|
||||
]
|
||||
for known_revert_reason in known_revert_reasons:
|
||||
if known_revert_reason in str(exception):
|
||||
return (False, known_revert_reason)
|
||||
return (False, f"Unknown: {exception}")
|
||||
@@ -1,6 +1,8 @@
|
||||
from distutils.dist import Distribution
|
||||
from typing import Any
|
||||
from typing import Any, List
|
||||
|
||||
def setup(**attrs: Any) -> Distribution: ...
|
||||
|
||||
class Command: ...
|
||||
|
||||
def find_packages(where: str) -> List[str]: ...
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
from typing import Optional, Union
|
||||
from typing import Dict, Optional, Union
|
||||
|
||||
from web3.utils import datatypes
|
||||
|
||||
|
||||
class Web3:
|
||||
class HTTPProvider: ...
|
||||
|
||||
def __init__(self, provider: HTTPProvider) -> None: ...
|
||||
|
||||
@staticmethod
|
||||
def sha3(
|
||||
primitive: Optional[Union[bytes, int, None]] = None,
|
||||
text: Optional[str] = None,
|
||||
hexstr: Optional[str] = None
|
||||
) -> bytes: ...
|
||||
|
||||
class net:
|
||||
version: str
|
||||
...
|
||||
|
||||
class eth:
|
||||
@staticmethod
|
||||
def contract(address: str, abi: Dict) -> datatypes.Contract: ...
|
||||
...
|
||||
...
|
||||
|
||||
2
python-packages/order_utils/stubs/web3/exceptions.pyi
Normal file
2
python-packages/order_utils/stubs/web3/exceptions.pyi
Normal file
@@ -0,0 +1,2 @@
|
||||
class BadFunctionCallOutput(Exception):
|
||||
...
|
||||
@@ -0,0 +1,3 @@
|
||||
class Contract:
|
||||
def call(self): ...
|
||||
...
|
||||
@@ -1,24 +1,18 @@
|
||||
"""Exercise doctests for order_utils module."""
|
||||
"""Exercise doctests for all of our modules."""
|
||||
|
||||
from doctest import testmod
|
||||
import pkgutil
|
||||
|
||||
from zero_ex.dev_utils import abi_utils, type_assertions
|
||||
from zero_ex.order_utils import asset_data_utils
|
||||
import zero_ex
|
||||
|
||||
|
||||
def test_doctest_asset_data_utils():
|
||||
"""Invoke doctest on the asset_data_utils module."""
|
||||
(failure_count, _) = testmod(asset_data_utils)
|
||||
assert failure_count == 0
|
||||
|
||||
|
||||
def test_doctest_abi_utils():
|
||||
"""Invoke doctest on the abi_utils module."""
|
||||
(failure_count, _) = testmod(abi_utils)
|
||||
assert failure_count == 0
|
||||
|
||||
|
||||
def test_doctest_type_assertions():
|
||||
"""Invoke doctest on the type_assertions module."""
|
||||
(failure_count, _) = testmod(type_assertions)
|
||||
assert failure_count == 0
|
||||
def test_all_doctests():
|
||||
"""Gather zero_ex.* modules and doctest them."""
|
||||
# prefer `black` formatting. pylint: disable=bad-continuation
|
||||
for (importer, modname, _) in pkgutil.walk_packages(
|
||||
path=zero_ex.__path__, prefix="zero_ex."
|
||||
):
|
||||
module = importer.find_module(modname).load_module(modname)
|
||||
print(module)
|
||||
(failure_count, _) = testmod(module)
|
||||
assert failure_count == 0
|
||||
|
||||
128
python-packages/order_utils/test/test_signature_utils.py
Normal file
128
python-packages/order_utils/test/test_signature_utils.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Tests of zero_ex.order_utils.signature_utils."""
|
||||
|
||||
import pytest
|
||||
from web3 import Web3
|
||||
|
||||
from zero_ex.order_utils.signature_utils import is_valid_signature
|
||||
|
||||
|
||||
def test_is_valid_signature__provider_wrong_type():
|
||||
"""Test that giving a non-HTTPProvider raises a TypeError."""
|
||||
with pytest.raises(TypeError):
|
||||
is_valid_signature(
|
||||
123,
|
||||
"0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b"
|
||||
+ "0",
|
||||
"0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351b"
|
||||
+ "c3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace"
|
||||
+ "225403",
|
||||
"0x5409ed021d9299bf6814279a6a1411a7e866a631",
|
||||
)
|
||||
|
||||
|
||||
def test_is_valid_signature__data_not_string():
|
||||
"""Test that giving non-string `data` raises a TypeError."""
|
||||
with pytest.raises(TypeError):
|
||||
is_valid_signature(
|
||||
Web3.HTTPProvider("http://127.0.0.1:8545"),
|
||||
123,
|
||||
"0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351b"
|
||||
+ "c3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace"
|
||||
+ "225403",
|
||||
"0x5409ed021d9299bf6814279a6a1411a7e866a631",
|
||||
)
|
||||
|
||||
|
||||
def test_is_valid_signature__data_not_hex_string():
|
||||
"""Test that giving non-hex-string `data` raises a ValueError."""
|
||||
with pytest.raises(ValueError):
|
||||
is_valid_signature(
|
||||
Web3.HTTPProvider("http://127.0.0.1:8545"),
|
||||
"jjj",
|
||||
"0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351b"
|
||||
+ "c3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace"
|
||||
+ "225403",
|
||||
"0x5409ed021d9299bf6814279a6a1411a7e866a631",
|
||||
)
|
||||
|
||||
|
||||
def test_is_valid_signature__signature_not_string():
|
||||
"""Test that passng a non-string signature raises a TypeError."""
|
||||
with pytest.raises(TypeError):
|
||||
is_valid_signature(
|
||||
Web3.HTTPProvider("http://127.0.0.1:8545"),
|
||||
"0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b"
|
||||
+ "0",
|
||||
123,
|
||||
"0x5409ed021d9299bf6814279a6a1411a7e866a631",
|
||||
)
|
||||
|
||||
|
||||
def test_is_valid_signature__signature_not_hex_string():
|
||||
"""Test that passing a non-hex-string signature raises a ValueError."""
|
||||
with pytest.raises(ValueError):
|
||||
is_valid_signature(
|
||||
Web3.HTTPProvider("http://127.0.0.1:8545"),
|
||||
"0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b"
|
||||
+ "0",
|
||||
"jjj",
|
||||
"0x5409ed021d9299bf6814279a6a1411a7e866a631",
|
||||
)
|
||||
|
||||
|
||||
def test_is_valid_signature__signer_address_not_string():
|
||||
"""Test that giving a non-address `signer_address` raises a ValueError."""
|
||||
with pytest.raises(TypeError):
|
||||
is_valid_signature(
|
||||
Web3.HTTPProvider("http://127.0.0.1:8545"),
|
||||
"0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b"
|
||||
+ "0",
|
||||
"0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351b"
|
||||
+ "c3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace"
|
||||
+ "225403",
|
||||
123,
|
||||
)
|
||||
|
||||
|
||||
def test_is_valid_signature__signer_address_not_hex_string():
|
||||
"""Test that giving a non-hex-str `signer_address` raises a ValueError."""
|
||||
with pytest.raises(ValueError):
|
||||
is_valid_signature(
|
||||
Web3.HTTPProvider("http://127.0.0.1:8545"),
|
||||
"0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b"
|
||||
+ "0",
|
||||
"0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351b"
|
||||
+ "c3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace"
|
||||
+ "225403",
|
||||
"jjj",
|
||||
)
|
||||
|
||||
|
||||
def test_is_valid_signature__signer_address_not_valid_address():
|
||||
"""Test that giving a non-address for `signer_address` raises an error."""
|
||||
with pytest.raises(ValueError):
|
||||
is_valid_signature(
|
||||
Web3.HTTPProvider("http://127.0.0.1:8545"),
|
||||
"0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b"
|
||||
+ "0",
|
||||
"0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351b"
|
||||
+ "c3340349190569279751135161d22529dc25add4f6069af05be04cacbda2ace"
|
||||
+ "225403",
|
||||
"0xff",
|
||||
)
|
||||
|
||||
|
||||
def test_is_valid_signature__unsupported_sig_types():
|
||||
"""Test that passing in a sig w/invalid type raises error.
|
||||
|
||||
To induce this error, the last byte of the signature is tweaked from 03 to
|
||||
ff."""
|
||||
(is_valid, reason) = is_valid_signature(
|
||||
Web3.HTTPProvider("http://127.0.0.1:8545"),
|
||||
"0x6927e990021d23b1eb7b8789f6a6feaf98fe104bb0cf8259421b79f9a34222b0",
|
||||
"0x1B61a3ed31b43c8780e905a260a35faefcc527be7516aa11c0256729b5b351bc334"
|
||||
+ "0349190569279751135161d22529dc25add4f6069af05be04cacbda2ace2254ff",
|
||||
"0x5409ed021d9299bf6814279a6a1411a7e866a631",
|
||||
)
|
||||
assert is_valid is False
|
||||
assert reason == "SIGNATURE_UNSUPPORTED"
|
||||
16
yarn.lock
16
yarn.lock
@@ -1148,15 +1148,15 @@
|
||||
dependencies:
|
||||
samsam "1.3.0"
|
||||
|
||||
"@static/discharge@^1.2.2":
|
||||
"@static/discharge@https://github.com/0xProject/discharge.git":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.npmjs.org/@static/discharge/-/discharge-1.2.2.tgz#dc941e0c02c421b338b83ac2e59e140d7214c971"
|
||||
resolved "https://github.com/0xProject/discharge.git#3aed990822cabbb79b71b52700fdef08cd9eb400"
|
||||
dependencies:
|
||||
aws-sdk "^2.304.0"
|
||||
aws-sdk "^2.347.0"
|
||||
execa "^1.0.0"
|
||||
glob "^7.1.3"
|
||||
inquirer "^6.2.0"
|
||||
listr "^0.14.1"
|
||||
listr "^0.14.2"
|
||||
lodash.differenceby "^4.8.0"
|
||||
lodash.intersectionby "^4.7.0"
|
||||
lodash.intersectionwith "^4.4.0"
|
||||
@@ -2324,9 +2324,9 @@ aws-sdk@^2.127.0:
|
||||
uuid "3.1.0"
|
||||
xml2js "0.4.19"
|
||||
|
||||
aws-sdk@^2.304.0:
|
||||
version "2.338.0"
|
||||
resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.338.0.tgz#f1b1347f78defa27b92b030b5787fad0f89ac83b"
|
||||
aws-sdk@^2.347.0:
|
||||
version "2.348.0"
|
||||
resolved "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.348.0.tgz#69c5b6dd77a5eac85b54bf7cc1640c19d762d3ee"
|
||||
dependencies:
|
||||
buffer "4.9.1"
|
||||
events "1.1.1"
|
||||
@@ -9449,7 +9449,7 @@ listr@^0.12.0:
|
||||
stream-to-observable "^0.1.0"
|
||||
strip-ansi "^3.0.1"
|
||||
|
||||
listr@^0.14.1:
|
||||
listr@^0.14.2:
|
||||
version "0.14.2"
|
||||
resolved "https://registry.npmjs.org/listr/-/listr-0.14.2.tgz#cbe44b021100a15376addfc2d79349ee430bfe14"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user