import { assetDataUtils, orderHashUtils } from '@0x/order-utils'; import { colors, Link } from '@0x/react-shared'; import { BigNumber, logUtils } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import * as accounting from 'accounting'; import * as _ from 'lodash'; import { Card, CardHeader, CardText } from 'material-ui/Card'; import Divider from 'material-ui/Divider'; import RaisedButton from 'material-ui/RaisedButton'; import * as React from 'react'; import { Blockchain } from 'ts/blockchain'; import { TrackTokenConfirmationDialog } from 'ts/components/dialogs/track_token_confirmation_dialog'; import { FillOrderJSON } from 'ts/components/fill_order_json'; import { FillWarningDialog } from 'ts/components/fill_warning_dialog'; import { TokenAmountInput } from 'ts/components/inputs/token_amount_input'; import { Alert } from 'ts/components/ui/alert'; import { EthereumAddress } from 'ts/components/ui/ethereum_address'; import { Identicon } from 'ts/components/ui/identicon'; import { VisualOrder } from 'ts/components/visual_order'; import { Dispatcher } from 'ts/redux/dispatcher'; import { portalOrderSchema } from 'ts/schemas/portal_order_schema'; import { validator } from 'ts/schemas/validator'; import { AlertTypes, BlockchainErrs, PortalOrder, Token, TokenByAddress, WebsitePaths } from 'ts/types'; import { analytics } from 'ts/utils/analytics'; import { errorReporter } from 'ts/utils/error_reporter'; import { orderParser } from 'ts/utils/order_parser'; import { utils } from 'ts/utils/utils'; interface FillOrderProps { blockchain: Blockchain; blockchainErr: BlockchainErrs; orderFillAmount: BigNumber; isOrderInUrl: boolean; networkId: number; userAddress: string; tokenByAddress: TokenByAddress; initialOrder: PortalOrder; dispatcher: Dispatcher; lastForceTokenStateRefetch: number; isFullWidth?: boolean; shouldHideHeader?: boolean; } interface FillOrderState { didOrderValidationRun: boolean; areAllInvolvedTokensTracked: boolean; globalErrMsg: string; orderJSON: string; orderJSONErrMsg: string; parsedOrder: PortalOrder; didFillOrderSucceed: boolean; didCancelOrderSucceed: boolean; unavailableTakerAmount: BigNumber; isMakerTokenAddressInRegistry: boolean; isTakerTokenAddressInRegistry: boolean; isFillWarningDialogOpen: boolean; isFilling: boolean; isCancelling: boolean; isConfirmingTokenTracking: boolean; tokensToTrack: Token[]; } export class FillOrder extends React.Component { public static defaultProps: Partial = { isFullWidth: false, shouldHideHeader: false, }; private _isUnmounted: boolean; constructor(props: FillOrderProps) { super(props); this._isUnmounted = false; this.state = { globalErrMsg: '', didOrderValidationRun: false, areAllInvolvedTokensTracked: false, didFillOrderSucceed: false, didCancelOrderSucceed: false, orderJSON: _.isUndefined(this.props.initialOrder) ? '' : JSON.stringify(this.props.initialOrder), orderJSONErrMsg: '', parsedOrder: this.props.initialOrder, unavailableTakerAmount: new BigNumber(0), isMakerTokenAddressInRegistry: false, isTakerTokenAddressInRegistry: false, isFillWarningDialogOpen: false, isFilling: false, isCancelling: false, isConfirmingTokenTracking: false, tokensToTrack: [], }; } public componentWillMount(): void { if (!_.isEmpty(this.state.orderJSON)) { // tslint:disable-next-line:no-floating-promises this._validateFillOrderFireAndForgetAsync(this.state.orderJSON); } } public componentDidMount(): void { window.scrollTo(0, 0); } public componentWillUnmount(): void { this._isUnmounted = true; } public render(): React.ReactNode { const rootClassName = this.props.isFullWidth ? 'clearfix' : 'lg-px4 md-px4 sm-px2'; return (
{!this.props.shouldHideHeader && (

Fill an order

)}
{!this.props.isOrderInUrl && (
Paste an order JSON snippet below to begin
Order JSON
{this._renderOrderJsonNotices()}
)}
{!_.isUndefined(this.state.parsedOrder) && this.state.didOrderValidationRun && this.state.areAllInvolvedTokensTracked && this._renderVisualOrder()}
{this.props.isOrderInUrl && (
{this._renderOrderJsonNotices()}
)}
); } private _renderOrderJsonNotices(): React.ReactNode { return (
{!_.isUndefined(this.props.initialOrder) && !this.state.didOrderValidationRun && (
Validating order...
)} {!_.isEmpty(this.state.orderJSONErrMsg) && ( )}
); } private _renderVisualOrder(): React.ReactNode { const takerTokenAddress = assetDataUtils.decodeERC20AssetData(this.state.parsedOrder.signedOrder.takerAssetData) .tokenAddress; const takerToken = this.props.tokenByAddress[takerTokenAddress]; const orderTakerAmount = this.state.parsedOrder.signedOrder.takerAssetAmount; const orderMakerAmount = this.state.parsedOrder.signedOrder.makerAssetAmount; const takerAssetToken = { amount: orderTakerAmount.minus(this.state.unavailableTakerAmount), symbol: takerToken.symbol, }; const fillToken = this.props.tokenByAddress[takerTokenAddress]; const makerTokenAddress = assetDataUtils.decodeERC20AssetData(this.state.parsedOrder.signedOrder.makerAssetData) .tokenAddress; const makerToken = this.props.tokenByAddress[makerTokenAddress]; const makerAssetToken = { amount: orderMakerAmount .times(takerAssetToken.amount) .div(orderTakerAmount) .floor(), symbol: makerToken.symbol, }; const fillAssetToken = { amount: this.props.orderFillAmount, symbol: takerToken.symbol, }; const parsedOrderExpiration = this.state.parsedOrder.signedOrder.expirationTimeSeconds; let orderReceiveAmount = 0; if (!_.isUndefined(this.props.orderFillAmount)) { const orderReceiveAmountBigNumber = orderMakerAmount .times(this.props.orderFillAmount) .dividedBy(orderTakerAmount) .floor(); orderReceiveAmount = this._formatCurrencyAmount(orderReceiveAmountBigNumber, makerToken.decimals); } const isUserMaker = !_.isUndefined(this.state.parsedOrder) && this.state.parsedOrder.signedOrder.makerAddress === this.props.userAddress; const expiryDate = utils.convertToReadableDateTimeFromUnixTimestamp(parsedOrderExpiration); return (
Order details
Maker:
Expires: {expiryDate} UTC
{!isUserMaker && (
= {accounting.formatNumber(orderReceiveAmount, 6)} {makerToken.symbol}
)}
{isUserMaker ? (
{this.state.didCancelOrderSucceed && ( )}
) : (
{!_.isEmpty(this.state.globalErrMsg) && ( )} {this.state.didFillOrderSucceed && ( )}
)}
); } private _renderFillSuccessMsg(): React.ReactNode { return (
Order successfully filled. See the trade details in your{' '} trade history
); } private _renderCancelSuccessMsg(): React.ReactNode { return
Order successfully cancelled.
; } private _onFillOrderClick(): void { if (!this.state.isMakerTokenAddressInRegistry || !this.state.isTakerTokenAddressInRegistry) { this.setState({ isFillWarningDialogOpen: true, }); } else { // tslint:disable-next-line:no-floating-promises this._onFillOrderClickFireAndForgetAsync(); } } private _onFillWarningClosed(didUserCancel: boolean): void { this.setState({ isFillWarningDialogOpen: false, }); if (!didUserCancel) { // tslint:disable-next-line:no-floating-promises this._onFillOrderClickFireAndForgetAsync(); } } private _onFillAmountChange(_isValid: boolean, amount?: BigNumber): void { this.props.dispatcher.updateOrderFillAmount(amount); } private _onFillOrderJSONChanged(event: any): void { const orderJSON = event.target.value; this.setState({ didOrderValidationRun: _.isEmpty(orderJSON) && _.isEmpty(this.state.orderJSONErrMsg), didFillOrderSucceed: false, }); // tslint:disable-next-line:no-floating-promises this._validateFillOrderFireAndForgetAsync(orderJSON); } private async _checkForUntrackedTokensAndAskToAddAsync(): Promise { if (!_.isEmpty(this.state.orderJSONErrMsg)) { return; } const makerTokenAddress = assetDataUtils.decodeERC20AssetData(this.state.parsedOrder.signedOrder.makerAssetData) .tokenAddress; const takerTokenAddress = assetDataUtils.decodeERC20AssetData(this.state.parsedOrder.signedOrder.takerAssetData) .tokenAddress; const makerTokenIfExists = this.props.tokenByAddress[makerTokenAddress]; const takerTokenIfExists = this.props.tokenByAddress[takerTokenAddress]; const tokensToTrack: Token[] = []; const isUnseenMakerToken = _.isUndefined(makerTokenIfExists); const isMakerTokenTracked = !_.isUndefined(makerTokenIfExists) && utils.isTokenTracked(makerTokenIfExists); if (isUnseenMakerToken) { tokensToTrack.push({ ...this.state.parsedOrder.metadata.makerToken, address: makerTokenAddress, iconUrl: undefined, trackedTimestamp: undefined, isRegistered: false, }); } else if (!isMakerTokenTracked) { tokensToTrack.push(makerTokenIfExists); } const isUnseenTakerToken = _.isUndefined(takerTokenIfExists); const isTakerTokenTracked = !_.isUndefined(takerTokenIfExists) && utils.isTokenTracked(takerTokenIfExists); if (isUnseenTakerToken) { tokensToTrack.push({ ...this.state.parsedOrder.metadata.takerToken, address: takerTokenAddress, iconUrl: undefined, trackedTimestamp: undefined, isRegistered: false, }); } else if (!isTakerTokenTracked) { tokensToTrack.push(takerTokenIfExists); } if (!_.isEmpty(tokensToTrack)) { this.setState({ isConfirmingTokenTracking: true, tokensToTrack, }); } else { this.setState({ areAllInvolvedTokensTracked: true, }); } } private async _validateFillOrderFireAndForgetAsync(orderJSON: string): Promise { let orderJSONErrMsg = ''; let parsedOrder: PortalOrder; let orderHash: string; try { const order = orderParser.parseJsonString(orderJSON); const validationResult = validator.validate(order, portalOrderSchema); if (validationResult.errors.length > 0) { orderJSONErrMsg = 'Submitted order JSON is not a valid order'; logUtils.log(`Unexpected order JSON validation error: ${validationResult.errors.join(', ')}`); return; } parsedOrder = order; const signedOrder = parsedOrder.signedOrder; orderHash = orderHashUtils.getOrderHashHex(signedOrder); const exchangeContractAddr = this.props.blockchain.getExchangeContractAddressIfExists(); const signature = signedOrder.signature; const isSignatureValid = await this.props.blockchain.isValidSignatureAsync( orderHash, signature, signedOrder.makerAddress, ); if (exchangeContractAddr !== signedOrder.exchangeAddress) { orderJSONErrMsg = 'This order was made on another network or using a deprecated Exchange contract'; parsedOrder = undefined; } else if (!isSignatureValid) { orderJSONErrMsg = 'Order signature is invalid'; parsedOrder = undefined; } else { // Update user supplied order cache so that if they navigate away from fill view // e.g to set a token allowance, when they come back, the fill order persists this.props.dispatcher.updateUserSuppliedOrderCache(parsedOrder); } } catch (err) { logUtils.log(`Validate order err: ${err}`); if (!_.isEmpty(orderJSON)) { orderJSONErrMsg = 'Submitted order JSON is not valid JSON'; } if (!this._isUnmounted) { this.setState({ didOrderValidationRun: true, orderJSON, orderJSONErrMsg, parsedOrder, }); } return; } let unavailableTakerAmount = new BigNumber(0); if (!_.isEmpty(orderJSONErrMsg)) { // Clear cache entry if user updates orderJSON to invalid entry this.props.dispatcher.updateUserSuppliedOrderCache(undefined); } else { unavailableTakerAmount = await this.props.blockchain.getUnavailableTakerAmountAsync(orderHash); const makerTokenAddress = assetDataUtils.decodeERC20AssetData(parsedOrder.signedOrder.makerAssetData) .tokenAddress; const takerTokenAddress = assetDataUtils.decodeERC20AssetData(parsedOrder.signedOrder.takerAssetData) .tokenAddress; const isMakerTokenAddressInRegistry = await this.props.blockchain.isAddressInTokenRegistryAsync( makerTokenAddress, ); const isTakerTokenAddressInRegistry = await this.props.blockchain.isAddressInTokenRegistryAsync( takerTokenAddress, ); this.setState({ isMakerTokenAddressInRegistry, isTakerTokenAddressInRegistry, }); } this.setState({ didOrderValidationRun: true, orderJSON, orderJSONErrMsg, parsedOrder, unavailableTakerAmount, }); await this._checkForUntrackedTokensAndAskToAddAsync(); } private _trackOrderEvent(eventName: string): void { const parsedOrder = this.state.parsedOrder; analytics.trackOrderEvent(eventName, parsedOrder); } private async _onFillOrderClickFireAndForgetAsync(): Promise { if (this.props.blockchainErr !== BlockchainErrs.NoError || _.isEmpty(this.props.userAddress)) { this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); return; } this.setState({ isFilling: true, didFillOrderSucceed: false, }); const parsedOrder = this.state.parsedOrder; const takerFillAmount = this.props.orderFillAmount; if (_.isUndefined(this.props.userAddress)) { this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); this.setState({ isFilling: false, }); return; } let globalErrMsg = ''; if (_.isUndefined(takerFillAmount)) { globalErrMsg = 'You must specify a fill amount'; } const signedOrder = parsedOrder.signedOrder; if (_.isEmpty(globalErrMsg)) { try { await this.props.blockchain.validateFillOrderThrowIfInvalidAsync( signedOrder, takerFillAmount, this.props.userAddress, ); } catch (err) { globalErrMsg = utils.zeroExErrToHumanReadableErrMsg(err.message, parsedOrder.signedOrder.takerAddress); } } if (!_.isEmpty(globalErrMsg)) { this.setState({ isFilling: false, globalErrMsg, }); return; } try { const orderFilledAmount: BigNumber = await this.props.blockchain.fillOrderAsync( signedOrder, this.props.orderFillAmount, ); this._trackOrderEvent('Fill Order Success'); // After fill completes, let's force fetch the token balances this.props.dispatcher.forceTokenStateRefetch(); this.setState({ isFilling: false, didFillOrderSucceed: true, globalErrMsg: '', unavailableTakerAmount: this.state.unavailableTakerAmount.plus(orderFilledAmount), }); return; } catch (err) { this.setState({ isFilling: false, }); this._trackOrderEvent('Fill Order Failure'); const errMsg = `${err}`; if (utils.didUserDenyWeb3Request(errMsg)) { return; } globalErrMsg = 'Failed to fill order, please refresh and try again'; logUtils.log(`${err}`); this.setState({ globalErrMsg, }); errorReporter.report(err); return; } } private async _onCancelOrderClickFireAndForgetAsync(): Promise { if (this.props.blockchainErr !== BlockchainErrs.NoError || _.isEmpty(this.props.userAddress)) { this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); return; } this.setState({ isCancelling: true, didCancelOrderSucceed: false, }); const parsedOrder = this.state.parsedOrder; const takerAddress = this.props.userAddress; if (_.isUndefined(takerAddress)) { this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); this.setState({ isFilling: false, }); return; } let globalErrMsg = ''; const signedOrder = parsedOrder.signedOrder; const takerTokenAmount = signedOrder.takerAssetAmount; if (!_.isEmpty(globalErrMsg)) { this.setState({ isCancelling: false, globalErrMsg, }); return; } try { await this.props.blockchain.cancelOrderAsync(signedOrder); this.setState({ isCancelling: false, didCancelOrderSucceed: true, globalErrMsg: '', unavailableTakerAmount: takerTokenAmount, }); this._trackOrderEvent('Cancel Order Success'); return; } catch (err) { this.setState({ isCancelling: false, }); const errMsg = `${err}`; if (utils.didUserDenyWeb3Request(errMsg)) { return; } this._trackOrderEvent('Cancel Order Failure'); globalErrMsg = 'Failed to cancel order, please refresh and try again'; logUtils.log(`${err}`); this.setState({ globalErrMsg, }); errorReporter.report(err); return; } } private _formatCurrencyAmount(amount: BigNumber, decimals: number): number { const unitAmount = Web3Wrapper.toUnitAmount(amount, decimals); const roundedUnitAmount = Math.round(unitAmount.toNumber() * 100000) / 100000; return roundedUnitAmount; } private _onToggleTrackConfirmDialog(didConfirmTokenTracking: boolean): void { if (!didConfirmTokenTracking) { this.setState({ orderJSON: '', orderJSONErrMsg: '', parsedOrder: undefined, }); } else { this.setState({ areAllInvolvedTokensTracked: true, }); } this.setState({ isConfirmingTokenTracking: !this.state.isConfirmingTokenTracking, tokensToTrack: [], }); } } // tslint:disable:max-file-line-count