Extreme MVP of buying a ERC721 works

This commit is contained in:
fragosti 2019-04-05 15:55:10 -07:00
parent fdcad84cee
commit 90ad681a9e
10 changed files with 120 additions and 37 deletions

View File

@ -245,7 +245,7 @@ export class DutchAuctionContract extends BaseContract {
return contractInstance; return contractInstance;
} }
constructor(abi: ContractAbi, address: string, supportedProvider: SupportedProvider, txDefaults?: Partial<TxData>) { constructor(abi: ContractAbi, address: string, supportedProvider: SupportedProvider, txDefaults?: Partial<TxData>) {
super('DutchAuction', abi, address, providerUtils.standardizeOrThrow(supportedProvider), txDefaults); super('DutchAuction', abi, address, supportedProvider, txDefaults);
classUtils.bindAll(this, ['_abiEncoderByFunctionSignature', 'address', 'abi', '_web3Wrapper']); classUtils.bindAll(this, ['_abiEncoderByFunctionSignature', 'address', 'abi', '_web3Wrapper']);
} }
} // tslint:disable:max-file-line-count } // tslint:disable:max-file-line-count

View File

@ -74,6 +74,26 @@
EXPONENTIAL_AT: 1000, EXPONENTIAL_AT: 1000,
DECIMAL_PLACES: 78, DECIMAL_PLACES: 78,
}); });
const nftOrders = [
{
makerAddress: '0x34a745008a643eebc58920eaa29fb1165b4a288e',
takerAddress: '0x0000000000000000000000000000000000000000',
makerFee: '0',
takerFee: '0',
senderAddress: '0x0000000000000000000000000000000000000000',
makerAssetAmount: '1',
takerAssetAmount: '25000000000000000',
makerAssetData:
'0x02571792000000000000000000000000f5b0a3efb8e8e4c201e2a935f110eaaf3ffecb8d0000000000000000000000000000000000000000000000000000000000008849',
takerAssetData: '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
exchangeAddress: '0x4f833a24e1f95d70f028921e27040ca56e09ab0b',
feeRecipientAddress: '0x66a836664adc7c525c0cc4527dee8619d4faf669',
salt: '73683692897280689979350475364949461624706477613931387930893215569576520901319',
expirationTimeSeconds: '1557096652',
signature:
'0x1bf37ac01ba863c08f2ddba776b263af4f960326ed8d8a0a651acb4c7bce54592e5bd9826fb117ccd0d40392d26339de6e3377721fabbb3107043e4de444b24ae602',
},
];
const providedOrders = [ const providedOrders = [
// Order selling REP // Order selling REP
{ {
@ -168,7 +188,8 @@
}; };
} }
const renderOptionsOverrides = { const renderOptionsOverrides = {
orderSource: orderSourceOverride === 'provided' ? providedOrders : orderSourceOverride, // orderSource: orderSourceOverride === 'provided' ? providedOrders : orderSourceOverride,
orderSource: nftOrders,
networkId: +queryParams.getQueryParamValue('networkId') || undefined, networkId: +queryParams.getQueryParamValue('networkId') || undefined,
defaultAssetBuyAmount: +queryParams.getQueryParamValue('defaultAssetBuyAmount') || undefined, defaultAssetBuyAmount: +queryParams.getQueryParamValue('defaultAssetBuyAmount') || undefined,
availableAssetDatas: availableAssetDatasString ? JSON.parse(availableAssetDatasString) : undefined, availableAssetDatas: availableAssetDatasString ? JSON.parse(availableAssetDatasString) : undefined,
@ -176,6 +197,15 @@
affiliateInfo: affiliateInfoOverride, affiliateInfo: affiliateInfoOverride,
shouldDisablePushToHistory: !!queryParams.getQueryParamValue('shouldDisablePushToHistory'), shouldDisablePushToHistory: !!queryParams.getQueryParamValue('shouldDisablePushToHistory'),
walletDisplayName: queryParams.getQueryParamValue('walletDisplayName') || undefined, walletDisplayName: queryParams.getQueryParamValue('walletDisplayName') || undefined,
additionalAssetMetaDataMap: {
'0x02571792000000000000000000000000f5b0a3efb8e8e4c201e2a935f110eaaf3ffecb8d0000000000000000000000000000000000000000000000000000000000008849': {
assetProxyId: '0x02571792',
name: 'Axie #34889',
imageUrl:
'https://storage.opensea.io/0xf5b0a3efb8e8e4c201e2a935f110eaaf3ffecb8d/34889-1549990035.png',
primaryColor: '#b5d868',
},
},
}; };
return renderOptionsOverrides; return renderOptionsOverrides;
}; };

View File

@ -1,10 +1,11 @@
import { AssetProxyId } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { SelectedERC20AssetAmountInput } from '../containers/selected_erc20_asset_amount_input'; import { SelectedERC20AssetAmountInput } from '../containers/selected_erc20_asset_amount_input';
import { ColorOption } from '../style/theme'; import { ColorOption } from '../style/theme';
import { AsyncProcessState, ERC20Asset, OrderProcessState, OrderState } from '../types'; import { Asset, AsyncProcessState, ERC20Asset, ERC721Asset, OrderProcessState, OrderState } from '../types';
import { format } from '../util/format'; import { format } from '../util/format';
import { AmountPlaceholder } from './amount_placeholder'; import { AmountPlaceholder } from './amount_placeholder';
@ -15,6 +16,7 @@ import { Spinner } from './ui/spinner';
import { Text } from './ui/text'; import { Text } from './ui/text';
export interface InstantHeadingProps { export interface InstantHeadingProps {
selectedAsset?: Asset;
selectedAssetUnitAmount?: BigNumber; selectedAssetUnitAmount?: BigNumber;
totalEthBaseUnitAmount?: BigNumber; totalEthBaseUnitAmount?: BigNumber;
ethUsdPrice?: BigNumber; ethUsdPrice?: BigNumber;
@ -30,9 +32,36 @@ const ICON_COLOR = ColorOption.white;
export class InstantHeading extends React.PureComponent<InstantHeadingProps, {}> { export class InstantHeading extends React.PureComponent<InstantHeadingProps, {}> {
public render(): React.ReactNode { public render(): React.ReactNode {
const iconOrAmounts = this._renderIcon() || this._renderAmountsSection(); const { selectedAsset } = this.props;
return ( return (
<Container backgroundColor={ColorOption.primaryColor} width="100%" padding="20px"> <Container backgroundColor={ColorOption.primaryColor} width="100%" padding="20px">
{this._renderAssetHeadingContent()}
</Container>
);
}
private _renderAssetHeadingContent(): React.ReactNode {
const { selectedAsset } = this.props;
if (_.isUndefined(selectedAsset)) {
// TODO: Only the ERC20 flow supports selecting assets.
return this._renderERC20AssetHeading();
}
if (selectedAsset.metaData.assetProxyId === AssetProxyId.ERC20) {
return this._renderERC20AssetHeading();
} else if (selectedAsset.metaData.assetProxyId === AssetProxyId.ERC721) {
return this._renderERC721AssetHeading(selectedAsset as ERC721Asset);
}
return null;
}
private _renderERC721AssetHeading(asset: ERC721Asset): React.ReactNode {
return <Container><img src={asset.metaData.imageUrl}/> </Container>;
}
private _renderERC20AssetHeading(): React.ReactNode {
const iconOrAmounts = this._renderIcon() || this._renderAmountsSection();
return (
<React.Fragment>
<Container marginBottom="5px"> <Container marginBottom="5px">
<Text <Text
letterSpacing="1px" letterSpacing="1px"
@ -56,7 +85,7 @@ export class InstantHeading extends React.PureComponent<InstantHeadingProps, {}>
{iconOrAmounts} {iconOrAmounts}
</Flex> </Flex>
</Flex> </Flex>
</Container> </React.Fragment>
); );
} }

View File

@ -1,3 +1,5 @@
import { AssetProxyId } from '@0x/types';
import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import PoweredByLogo from '../assets/powered_by_0x.svg'; import PoweredByLogo from '../assets/powered_by_0x.svg';
@ -11,7 +13,7 @@ import { SelectedAssetBuyOrderStateButtons } from '../containers/selected_asset_
import { SelectedAssetInstantHeading } from '../containers/selected_asset_instant_heading'; import { SelectedAssetInstantHeading } from '../containers/selected_asset_instant_heading';
import { ColorOption } from '../style/theme'; import { ColorOption } from '../style/theme';
import { zIndex } from '../style/z_index'; import { zIndex } from '../style/z_index';
import { SlideAnimationState } from '../types'; import { Asset, SlideAnimationState } from '../types';
import { analytics, TokenSelectorClosedVia } from '../util/analytics'; import { analytics, TokenSelectorClosedVia } from '../util/analytics';
import { CSSReset } from './css_reset'; import { CSSReset } from './css_reset';
@ -84,11 +86,15 @@ export class ZeroExInstantContainer extends React.PureComponent<
</React.Fragment> </React.Fragment>
); );
} }
private readonly _handleSymbolClick = (): void => { private readonly _handleSymbolClick = (asset?: Asset): void => {
if (_.isUndefined(asset) || asset.metaData.assetProxyId === AssetProxyId.ERC20) {
analytics.trackTokenSelectorOpened(); analytics.trackTokenSelectorOpened();
this.setState({ this.setState({
tokenSelectionPanelAnimationState: 'slidIn', tokenSelectionPanelAnimationState: 'slidIn',
}); });
} else if (asset.metaData.assetProxyId === AssetProxyId.ERC721) {
// TODO: Link open sea.
}
}; };
private readonly _handlePanelCloseClickedX = (): void => { private readonly _handlePanelCloseClickedX = (): void => {
this._handlePanelClose(TokenSelectorClosedVia.ClickedX); this._handlePanelClose(TokenSelectorClosedVia.ClickedX);

View File

@ -18,6 +18,7 @@ import { gasPriceEstimator } from '../util/gas_price_estimator';
import { Heartbeater } from '../util/heartbeater'; import { Heartbeater } from '../util/heartbeater';
import { generateAccountHeartbeater, generateBuyQuoteHeartbeater } from '../util/heartbeater_factory'; import { generateAccountHeartbeater, generateBuyQuoteHeartbeater } from '../util/heartbeater_factory';
import { providerStateFactory } from '../util/provider_state_factory'; import { providerStateFactory } from '../util/provider_state_factory';
import { AssetProxyId } from '@0x/types';
export type ZeroExInstantProviderProps = ZeroExInstantBaseConfig; export type ZeroExInstantProviderProps = ZeroExInstantBaseConfig;
@ -46,22 +47,30 @@ export class ZeroExInstantProvider extends React.PureComponent<ZeroExInstantProv
..._.mapKeys(props.additionalAssetMetaDataMap || {}, (value, key) => key.toLowerCase()), ..._.mapKeys(props.additionalAssetMetaDataMap || {}, (value, key) => key.toLowerCase()),
...defaultState.assetMetaDataMap, ...defaultState.assetMetaDataMap,
}; };
const selectedAsset = _.isUndefined(props.defaultSelectedAssetData)
? undefined
: assetUtils.createAssetFromAssetDataOrThrow(
props.defaultSelectedAssetData,
completeAssetMetaDataMap,
networkId,
);
let selectedAssetUnitAmount: BigNumber | undefined = new BigNumber(1);
if (!_.isUndefined(selectedAsset) && selectedAsset.metaData.assetProxyId === AssetProxyId.ERC20) {
selectedAssetUnitAmount = _.isUndefined(props.defaultAssetBuyAmount)
? undefined
: new BigNumber(props.defaultAssetBuyAmount);
}
// construct the final state // construct the final state
const storeStateFromProps: State = { const storeStateFromProps: State = {
...defaultState, ...defaultState,
providerState, providerState,
network: networkId, network: networkId,
walletDisplayName: props.walletDisplayName, walletDisplayName: props.walletDisplayName,
selectedAsset: _.isUndefined(props.defaultSelectedAssetData) selectedAsset,
? undefined selectedAssetUnitAmount,
: assetUtils.createAssetFromAssetDataOrThrow(
props.defaultSelectedAssetData,
completeAssetMetaDataMap,
networkId,
),
selectedAssetUnitAmount: _.isUndefined(props.defaultAssetBuyAmount)
? undefined
: new BigNumber(props.defaultAssetBuyAmount),
availableAssets: _.isUndefined(props.availableAssetDatas) availableAssets: _.isUndefined(props.availableAssetDatas)
? undefined ? undefined
: assetUtils.createAssetsFromAssetDatas(props.availableAssetDatas, completeAssetMetaDataMap, networkId), : assetUtils.createAssetsFromAssetDatas(props.availableAssetDatas, completeAssetMetaDataMap, networkId),

View File

@ -16,7 +16,8 @@ export const ONE_SECOND_MS = 1000;
export const ONE_MINUTE_MS = ONE_SECOND_MS * 60; export const ONE_MINUTE_MS = ONE_SECOND_MS * 60;
export const GIT_SHA = process.env.GIT_SHA; export const GIT_SHA = process.env.GIT_SHA;
export const NODE_ENV = process.env.NODE_ENV; export const NODE_ENV = process.env.NODE_ENV;
export const SLIPPAGE_PERCENTAGE = 0.2; export const ERC20_BUY_QUOTE_SLIPPAGE_PERCENTAGE = 0.2;
export const ERC721_BUY_QUOTE_SLIPPAGE_PERCENTAGE = 0;
export const NPM_PACKAGE_VERSION = process.env.NPM_PACKAGE_VERSION; export const NPM_PACKAGE_VERSION = process.env.NPM_PACKAGE_VERSION;
export const DEFAULT_UNKOWN_ASSET_NAME = '???'; export const DEFAULT_UNKOWN_ASSET_NAME = '???';
export const ACCOUNT_UPDATE_INTERVAL_TIME_MS = ONE_SECOND_MS * 5; export const ACCOUNT_UPDATE_INTERVAL_TIME_MS = ONE_SECOND_MS * 5;

View File

@ -5,15 +5,16 @@ import { connect } from 'react-redux';
import { oc } from 'ts-optchain'; import { oc } from 'ts-optchain';
import { State } from '../redux/reducer'; import { State } from '../redux/reducer';
import { AsyncProcessState, ERC20Asset, OrderState } from '../types'; import { Asset, AsyncProcessState, OrderState } from '../types';
import { InstantHeading } from '../components/instant_heading'; import { InstantHeading } from '../components/instant_heading';
export interface InstantHeadingProps { export interface InstantHeadingProps {
onSelectAssetClick?: (asset?: ERC20Asset) => void; onSelectAssetClick?: (asset?: Asset) => void;
} }
interface ConnectedState { interface ConnectedState {
selectedAsset?: Asset;
selectedAssetUnitAmount?: BigNumber; selectedAssetUnitAmount?: BigNumber;
totalEthBaseUnitAmount?: BigNumber; totalEthBaseUnitAmount?: BigNumber;
ethUsdPrice?: BigNumber; ethUsdPrice?: BigNumber;
@ -22,6 +23,7 @@ interface ConnectedState {
} }
const mapStateToProps = (state: State, _ownProps: InstantHeadingProps): ConnectedState => ({ const mapStateToProps = (state: State, _ownProps: InstantHeadingProps): ConnectedState => ({
selectedAsset: state.selectedAsset,
selectedAssetUnitAmount: state.selectedAssetUnitAmount, selectedAssetUnitAmount: state.selectedAssetUnitAmount,
totalEthBaseUnitAmount: oc(state).latestBuyQuote.worstCaseQuoteInfo.totalEthAmount(), totalEthBaseUnitAmount: oc(state).latestBuyQuote.worstCaseQuoteInfo.totalEthAmount(),
ethUsdPrice: state.ethUsdPrice, ethUsdPrice: state.ethUsdPrice,

View File

@ -100,13 +100,12 @@ export const asyncData = {
!_.isUndefined(selectedAssetUnitAmount) && !_.isUndefined(selectedAssetUnitAmount) &&
!_.isUndefined(selectedAsset) && !_.isUndefined(selectedAsset) &&
selectedAssetUnitAmount.isGreaterThan(BIG_NUMBER_ZERO) && selectedAssetUnitAmount.isGreaterThan(BIG_NUMBER_ZERO) &&
buyOrderState.processState === OrderProcessState.None && buyOrderState.processState === OrderProcessState.None
selectedAsset.metaData.assetProxyId === AssetProxyId.ERC20
) { ) {
await buyQuoteUpdater.updateBuyQuoteAsync( await buyQuoteUpdater.updateBuyQuoteAsync(
assetBuyer, assetBuyer,
dispatch, dispatch,
selectedAsset as ERC20Asset, selectedAsset,
selectedAssetUnitAmount, selectedAssetUnitAmount,
fetchOrigin, fetchOrigin,
{ {

View File

@ -109,17 +109,17 @@ export const assetUtils = {
); );
return _.compact(erc20sOrUndefined); return _.compact(erc20sOrUndefined);
}, },
assetBuyerErrorMessage: (asset: ERC20Asset, error: Error): string | undefined => { assetBuyerErrorMessage: (asset: Asset, error: Error): string | undefined => {
if (error.message === AssetBuyerError.InsufficientAssetLiquidity) { if (error.message === AssetBuyerError.InsufficientAssetLiquidity) {
const assetName = assetUtils.bestNameForAsset(asset, 'of this asset'); const assetName = assetUtils.bestNameForAsset(asset, 'of this asset');
if ( if (
error instanceof InsufficientAssetLiquidityError && error instanceof InsufficientAssetLiquidityError &&
error.amountAvailableToFill.isGreaterThan(BIG_NUMBER_ZERO) error.amountAvailableToFill.isGreaterThan(BIG_NUMBER_ZERO)
) { ) {
const unitAmountAvailableToFill = Web3Wrapper.toUnitAmount( const unitAmountAvailableToFill =
error.amountAvailableToFill, asset.metaData.assetProxyId === AssetProxyId.ERC20
asset.metaData.decimals, ? Web3Wrapper.toUnitAmount(error.amountAvailableToFill, asset.metaData.decimals)
); : error.amountAvailableToFill;
const roundedUnitAmountAvailableToFill = unitAmountAvailableToFill.decimalPlaces( const roundedUnitAmountAvailableToFill = unitAmountAvailableToFill.decimalPlaces(
2, 2,
BigNumber.ROUND_DOWN, BigNumber.ROUND_DOWN,

View File

@ -1,13 +1,14 @@
import { AssetBuyer, BuyQuote } from '@0x/asset-buyer'; import { AssetBuyer, BuyQuote } from '@0x/asset-buyer';
import { AssetProxyId } from '@0x/types';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper'; import { Web3Wrapper } from '@0x/web3-wrapper';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { oc } from 'ts-optchain'; import { oc } from 'ts-optchain';
import { SLIPPAGE_PERCENTAGE } from '../constants'; import { ERC20_BUY_QUOTE_SLIPPAGE_PERCENTAGE, ERC721_BUY_QUOTE_SLIPPAGE_PERCENTAGE } from '../constants';
import { Action, actions } from '../redux/actions'; import { Action, actions } from '../redux/actions';
import { AffiliateInfo, ERC20Asset, QuoteFetchOrigin } from '../types'; import { AffiliateInfo, Asset, QuoteFetchOrigin } from '../types';
import { analytics } from '../util/analytics'; import { analytics } from '../util/analytics';
import { assetUtils } from '../util/asset'; import { assetUtils } from '../util/asset';
import { errorFlasher } from '../util/error_flasher'; import { errorFlasher } from '../util/error_flasher';
@ -17,7 +18,7 @@ export const buyQuoteUpdater = {
updateBuyQuoteAsync: async ( updateBuyQuoteAsync: async (
assetBuyer: AssetBuyer, assetBuyer: AssetBuyer,
dispatch: Dispatch<Action>, dispatch: Dispatch<Action>,
asset: ERC20Asset, asset: Asset,
assetUnitAmount: BigNumber, assetUnitAmount: BigNumber,
fetchOrigin: QuoteFetchOrigin, fetchOrigin: QuoteFetchOrigin,
options: { options: {
@ -27,14 +28,20 @@ export const buyQuoteUpdater = {
}, },
): Promise<void> => { ): Promise<void> => {
// get a new buy quote. // get a new buy quote.
const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetUnitAmount, asset.metaData.decimals); const baseUnitValue =
asset.metaData.assetProxyId === AssetProxyId.ERC20
? Web3Wrapper.toBaseUnitAmount(assetUnitAmount, asset.metaData.decimals)
: assetUnitAmount;
if (options.setPending) { if (options.setPending) {
// mark quote as pending // mark quote as pending
dispatch(actions.setQuoteRequestStatePending()); dispatch(actions.setQuoteRequestStatePending());
} }
const feePercentage = oc(options.affiliateInfo).feePercentage(); const feePercentage = oc(options.affiliateInfo).feePercentage();
let newBuyQuote: BuyQuote | undefined; let newBuyQuote: BuyQuote | undefined;
const slippagePercentage = SLIPPAGE_PERCENTAGE; const slippagePercentage =
asset.metaData.assetProxyId === AssetProxyId.ERC20
? ERC20_BUY_QUOTE_SLIPPAGE_PERCENTAGE
: ERC721_BUY_QUOTE_SLIPPAGE_PERCENTAGE;
try { try {
newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue, { newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue, {
feePercentage, feePercentage,