From 3660ba28d73d70d08bf14c33ef680e5ef3ec7f3b Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Tue, 21 Nov 2017 14:03:08 -0600 Subject: Add website to mono repo, update packages to align with existing sub-packages, use new subscribeAsync 0x.js method --- packages/website/ts/components/fill_order.tsx | 714 ++++++++++++++++++++++++++ 1 file changed, 714 insertions(+) create mode 100644 packages/website/ts/components/fill_order.tsx (limited to 'packages/website/ts/components/fill_order.tsx') diff --git a/packages/website/ts/components/fill_order.tsx b/packages/website/ts/components/fill_order.tsx new file mode 100644 index 000000000..dc965283e --- /dev/null +++ b/packages/website/ts/components/fill_order.tsx @@ -0,0 +1,714 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import * as accounting from 'accounting'; +import {Link} from 'react-router-dom'; +import {ZeroEx, Order as ZeroExOrder} from '0x.js'; +import * as moment from 'moment'; +import BigNumber from 'bignumber.js'; +import Paper from 'material-ui/Paper'; +import {Card, CardText, CardHeader} from 'material-ui/Card'; +import Divider from 'material-ui/Divider'; +import TextField from 'material-ui/TextField'; +import RaisedButton from 'material-ui/RaisedButton'; +import {utils} from 'ts/utils/utils'; +import {constants} from 'ts/utils/constants'; +import { + Side, + TokenByAddress, + TokenStateByAddress, + Order, + BlockchainErrs, + OrderToken, + Token, + ExchangeContractErrs, + AlertTypes, + ContractResponse, + WebsitePaths, +} from 'ts/types'; +import {Alert} from 'ts/components/ui/alert'; +import {Identicon} from 'ts/components/ui/identicon'; +import {EthereumAddress} from 'ts/components/ui/ethereum_address'; +import {TokenAmountInput} from 'ts/components/inputs/token_amount_input'; +import {FillWarningDialog} from 'ts/components/fill_warning_dialog'; +import {FillOrderJSON} from 'ts/components/fill_order_json'; +import {VisualOrder} from 'ts/components/visual_order'; +import {SchemaValidator} from 'ts/schemas/validator'; +import {orderSchema} from 'ts/schemas/order_schema'; +import {Dispatcher} from 'ts/redux/dispatcher'; +import {Blockchain} from 'ts/blockchain'; +import {errorReporter} from 'ts/utils/error_reporter'; +import {trackedTokenStorage} from 'ts/local_storage/tracked_token_storage'; +import {TrackTokenConfirmationDialog} from 'ts/components/dialogs/track_token_confirmation_dialog'; + +const CUSTOM_LIGHT_GRAY = '#BBBBBB'; + +interface FillOrderProps { + blockchain: Blockchain; + blockchainErr: BlockchainErrs; + orderFillAmount: BigNumber; + isOrderInUrl: boolean; + networkId: number; + userAddress: string; + tokenByAddress: TokenByAddress; + tokenStateByAddress: TokenStateByAddress; + initialOrder: Order; + dispatcher: Dispatcher; +} + +interface FillOrderState { + didOrderValidationRun: boolean; + areAllInvolvedTokensTracked: boolean; + globalErrMsg: string; + orderJSON: string; + orderJSONErrMsg: string; + parsedOrder: Order; + 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 { + private validator: SchemaValidator; + constructor(props: FillOrderProps) { + super(props); + 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: [], + }; + this.validator = new SchemaValidator(); + } + public componentWillMount() { + if (!_.isEmpty(this.state.orderJSON)) { + this.validateFillOrderFireAndForgetAsync(this.state.orderJSON); + } + } + public componentDidMount() { + window.scrollTo(0, 0); + } + public render() { + return ( +
+

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() { + return ( +
+ {!_.isUndefined(this.props.initialOrder) && !this.state.didOrderValidationRun && +
+ + + + Validating order... +
+ } + {!_.isEmpty(this.state.orderJSONErrMsg) && + + } +
+ ); + } + private renderVisualOrder() { + const takerTokenAddress = this.state.parsedOrder.taker.token.address; + const takerToken = this.props.tokenByAddress[takerTokenAddress]; + const orderTakerAmount = new BigNumber(this.state.parsedOrder.taker.amount); + const orderMakerAmount = new BigNumber(this.state.parsedOrder.maker.amount); + const takerAssetToken = { + amount: orderTakerAmount.minus(this.state.unavailableTakerAmount), + symbol: takerToken.symbol, + }; + const fillToken = this.props.tokenByAddress[takerToken.address]; + const fillTokenState = this.props.tokenStateByAddress[takerToken.address]; + const makerTokenAddress = this.state.parsedOrder.maker.token.address; + const makerToken = this.props.tokenByAddress[makerTokenAddress]; + const makerAssetToken = { + amount: orderMakerAmount.times(takerAssetToken.amount).div(orderTakerAmount), + symbol: makerToken.symbol, + }; + const fillAssetToken = { + amount: this.props.orderFillAmount, + symbol: takerToken.symbol, + }; + const orderTaker = !_.isEmpty(this.state.parsedOrder.taker.address) ? this.state.parsedOrder.taker.address : + this.props.userAddress; + const parsedOrderExpiration = new BigNumber(this.state.parsedOrder.expiration); + const exchangeRate = orderMakerAmount.div(orderTakerAmount); + + let orderReceiveAmount = 0; + if (!_.isUndefined(this.props.orderFillAmount)) { + const orderReceiveAmountBigNumber = exchangeRate.mul(this.props.orderFillAmount); + orderReceiveAmount = this.formatCurrencyAmount(orderReceiveAmountBigNumber, makerToken.decimals); + } + const isUserMaker = !_.isUndefined(this.state.parsedOrder) && + this.state.parsedOrder.maker.address === 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() { + return ( +
+ Order successfully filled. See the trade details in your{' '} + + trade history + +
+ ); + } + private renderCancelSuccessMsg() { + return ( +
+ Order successfully cancelled. +
+ ); + } + private onFillOrderClick() { + if (!this.state.isMakerTokenAddressInRegistry || !this.state.isTakerTokenAddressInRegistry) { + this.setState({ + isFillWarningDialogOpen: true, + }); + } else { + this.onFillOrderClickFireAndForgetAsync(); + } + } + private onFillWarningClosed(didUserCancel: boolean) { + this.setState({ + isFillWarningDialogOpen: false, + }); + if (!didUserCancel) { + this.onFillOrderClickFireAndForgetAsync(); + } + } + private onFillAmountChange(isValid: boolean, amount?: BigNumber) { + this.props.dispatcher.updateOrderFillAmount(amount); + } + private onFillOrderJSONChanged(event: any) { + const orderJSON = event.target.value; + this.setState({ + didOrderValidationRun: _.isEmpty(orderJSON) && _.isEmpty(this.state.orderJSONErrMsg), + didFillOrderSucceed: false, + }); + this.validateFillOrderFireAndForgetAsync(orderJSON); + } + private async checkForUntrackedTokensAndAskToAdd() { + if (!_.isEmpty(this.state.orderJSONErrMsg)) { + return; + } + + const makerTokenIfExists = this.props.tokenByAddress[this.state.parsedOrder.maker.token.address]; + const takerTokenIfExists = this.props.tokenByAddress[this.state.parsedOrder.taker.token.address]; + + const tokensToTrack = []; + const isUnseenMakerToken = _.isUndefined(makerTokenIfExists); + const isMakerTokenTracked = !_.isUndefined(makerTokenIfExists) && makerTokenIfExists.isTracked; + if (isUnseenMakerToken) { + tokensToTrack.push(_.assign({}, this.state.parsedOrder.maker.token, { + iconUrl: undefined, + isTracked: false, + isRegistered: false, + })); + } else if (!isMakerTokenTracked) { + tokensToTrack.push(makerTokenIfExists); + } + const isUnseenTakerToken = _.isUndefined(takerTokenIfExists); + const isTakerTokenTracked = !_.isUndefined(takerTokenIfExists) && takerTokenIfExists.isTracked; + if (isUnseenTakerToken) { + tokensToTrack.push(_.assign({}, this.state.parsedOrder.taker.token, { + iconUrl: undefined, + isTracked: false, + 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) { + let orderJSONErrMsg = ''; + let parsedOrder: Order; + try { + const order = JSON.parse(orderJSON); + const validationResult = this.validator.validate(order, orderSchema); + if (validationResult.errors.length > 0) { + orderJSONErrMsg = 'Submitted order JSON is not a valid order'; + utils.consoleLog(`Unexpected order JSON validation error: ${validationResult.errors.join(', ')}`); + return; + } + parsedOrder = order; + + const exchangeContractAddr = this.props.blockchain.getExchangeContractAddressIfExists(); + const makerAmount = new BigNumber(parsedOrder.maker.amount); + const takerAmount = new BigNumber(parsedOrder.taker.amount); + const expiration = new BigNumber(parsedOrder.expiration); + const salt = new BigNumber(parsedOrder.salt); + const parsedMakerFee = new BigNumber(parsedOrder.maker.feeAmount); + const parsedTakerFee = new BigNumber(parsedOrder.taker.feeAmount); + + const zeroExOrder: ZeroExOrder = { + exchangeContractAddress: parsedOrder.exchangeContract, + expirationUnixTimestampSec: expiration, + feeRecipient: parsedOrder.feeRecipient, + maker: parsedOrder.maker.address, + makerFee: parsedMakerFee, + makerTokenAddress: parsedOrder.maker.token.address, + makerTokenAmount: makerAmount, + salt, + taker: _.isEmpty(parsedOrder.taker.address) ? constants.NULL_ADDRESS : parsedOrder.taker.address, + takerFee: parsedTakerFee, + takerTokenAddress: parsedOrder.taker.token.address, + takerTokenAmount: takerAmount, + }; + const orderHash = ZeroEx.getOrderHashHex(zeroExOrder); + + const signature = parsedOrder.signature; + const isValidSignature = ZeroEx.isValidSignature(signature.hash, signature, parsedOrder.maker.address); + if (this.props.networkId !== parsedOrder.networkId) { + orderJSONErrMsg = `This order was made on another Ethereum network + (id: ${parsedOrder.networkId}). Connect to this network to fill.`; + parsedOrder = undefined; + } else if (exchangeContractAddr !== parsedOrder.exchangeContract) { + orderJSONErrMsg = 'This order was made using a deprecated 0x Exchange contract.'; + parsedOrder = undefined; + } else if (orderHash !== signature.hash) { + orderJSONErrMsg = 'Order hash does not match supplied plaintext values'; + parsedOrder = undefined; + } else if (!isValidSignature) { + 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) { + utils.consoleLog(`Validate order err: ${err}`); + if (!_.isEmpty(orderJSON)) { + orderJSONErrMsg = 'Submitted order JSON is not valid JSON'; + } + 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 { + const orderHash = parsedOrder.signature.hash; + unavailableTakerAmount = await this.props.blockchain.getUnavailableTakerAmountAsync(orderHash); + const isMakerTokenAddressInRegistry = await this.props.blockchain.isAddressInTokenRegistryAsync( + parsedOrder.maker.token.address, + ); + const isTakerTokenAddressInRegistry = await this.props.blockchain.isAddressInTokenRegistryAsync( + parsedOrder.taker.token.address, + ); + this.setState({ + isMakerTokenAddressInRegistry, + isTakerTokenAddressInRegistry, + }); + } + + this.setState({ + didOrderValidationRun: true, + orderJSON, + orderJSONErrMsg, + parsedOrder, + unavailableTakerAmount, + }); + + await this.checkForUntrackedTokensAndAskToAdd(); + } + private async onFillOrderClickFireAndForgetAsync(): Promise { + if (!_.isEmpty(this.props.blockchainErr) || _.isEmpty(this.props.userAddress)) { + this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); + return; + } + + this.setState({ + isFilling: true, + didFillOrderSucceed: false, + }); + + const parsedOrder = this.state.parsedOrder; + const orderHash = parsedOrder.signature.hash; + const unavailableTakerAmount = await this.props.blockchain.getUnavailableTakerAmountAsync(orderHash); + 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 = this.props.blockchain.portalOrderToSignedOrder( + parsedOrder.maker.address, + parsedOrder.taker.address, + parsedOrder.maker.token.address, + parsedOrder.taker.token.address, + new BigNumber(parsedOrder.maker.amount), + new BigNumber(parsedOrder.taker.amount), + new BigNumber(parsedOrder.maker.feeAmount), + new BigNumber(parsedOrder.taker.feeAmount), + new BigNumber(this.state.parsedOrder.expiration), + parsedOrder.feeRecipient, + parsedOrder.signature, + new BigNumber(parsedOrder.salt), + ); + if (_.isEmpty(globalErrMsg)) { + try { + await this.props.blockchain.validateFillOrderThrowIfInvalidAsync( + signedOrder, takerFillAmount, this.props.userAddress); + } catch (err) { + globalErrMsg = this.props.blockchain.toHumanReadableErrorMsg(err.message, parsedOrder.taker.address); + } + } + if (!_.isEmpty(globalErrMsg)) { + this.setState({ + isFilling: false, + globalErrMsg, + }); + return; + } + try { + const orderFilledAmount: BigNumber = await this.props.blockchain.fillOrderAsync( + signedOrder, this.props.orderFillAmount, + ); + // After fill completes, let's update the token balances + const makerToken = this.props.tokenByAddress[parsedOrder.maker.token.address]; + const takerToken = this.props.tokenByAddress[parsedOrder.taker.token.address]; + const tokens = [makerToken, takerToken]; + await this.props.blockchain.updateTokenBalancesAndAllowancesAsync(tokens); + this.setState({ + isFilling: false, + didFillOrderSucceed: true, + globalErrMsg: '', + unavailableTakerAmount: this.state.unavailableTakerAmount.plus(orderFilledAmount), + }); + return; + } catch (err) { + this.setState({ + isFilling: false, + }); + const errMsg = `${err}`; + if (_.includes(errMsg, 'User denied transaction signature')) { + return; + } + globalErrMsg = 'Failed to fill order, please refresh and try again'; + utils.consoleLog(`${err}`); + await errorReporter.reportAsync(err); + this.setState({ + globalErrMsg, + }); + return; + } + } + private async onCancelOrderClickFireAndForgetAsync(): Promise { + if (!_.isEmpty(this.props.blockchainErr) || _.isEmpty(this.props.userAddress)) { + this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); + return; + } + + this.setState({ + isCancelling: true, + didCancelOrderSucceed: false, + }); + + const parsedOrder = this.state.parsedOrder; + const orderHash = parsedOrder.signature.hash; + const takerAddress = this.props.userAddress; + + if (_.isUndefined(takerAddress)) { + this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); + this.setState({ + isFilling: false, + }); + return; + } + let globalErrMsg = ''; + + const takerTokenAmount = new BigNumber(parsedOrder.taker.amount); + + const signedOrder = this.props.blockchain.portalOrderToSignedOrder( + parsedOrder.maker.address, + parsedOrder.taker.address, + parsedOrder.maker.token.address, + parsedOrder.taker.token.address, + new BigNumber(parsedOrder.maker.amount), + takerTokenAmount, + new BigNumber(parsedOrder.maker.feeAmount), + new BigNumber(parsedOrder.taker.feeAmount), + new BigNumber(this.state.parsedOrder.expiration), + parsedOrder.feeRecipient, + parsedOrder.signature, + new BigNumber(parsedOrder.salt), + ); + const unavailableTakerAmount = await this.props.blockchain.getUnavailableTakerAmountAsync(orderHash); + const availableTakerTokenAmount = takerTokenAmount.minus(unavailableTakerAmount); + try { + await this.props.blockchain.validateCancelOrderThrowIfInvalidAsync( + signedOrder, availableTakerTokenAmount); + } catch (err) { + globalErrMsg = this.props.blockchain.toHumanReadableErrorMsg(err.message, parsedOrder.taker.address); + } + if (!_.isEmpty(globalErrMsg)) { + this.setState({ + isCancelling: false, + globalErrMsg, + }); + return; + } + try { + await this.props.blockchain.cancelOrderAsync( + signedOrder, availableTakerTokenAmount, + ); + this.setState({ + isCancelling: false, + didCancelOrderSucceed: true, + globalErrMsg: '', + unavailableTakerAmount: takerTokenAmount, + }); + return; + } catch (err) { + this.setState({ + isCancelling: false, + }); + const errMsg = `${err}`; + if (_.includes(errMsg, 'User denied transaction signature')) { + return; + } + globalErrMsg = 'Failed to cancel order, please refresh and try again'; + utils.consoleLog(`${err}`); + await errorReporter.reportAsync(err); + this.setState({ + globalErrMsg, + }); + return; + } + } + private formatCurrencyAmount(amount: BigNumber, decimals: number): number { + const unitAmount = ZeroEx.toUnitAmount(amount, decimals); + const roundedUnitAmount = Math.round(unitAmount.toNumber() * 100000) / 100000; + return roundedUnitAmount; + } + private onToggleTrackConfirmDialog(didConfirmTokenTracking: boolean) { + if (!didConfirmTokenTracking) { + this.setState({ + orderJSON: '', + orderJSONErrMsg: '', + parsedOrder: undefined, + }); + } else { + this.setState({ + areAllInvolvedTokensTracked: true, + }); + } + this.setState({ + isConfirmingTokenTracking: !this.state.isConfirmingTokenTracking, + tokensToTrack: [], + }); + } +} -- cgit v1.2.3