aboutsummaryrefslogblamecommitdiffstats
path: root/packages/website/ts/components/generate_order/generate_order_form.tsx
blob: c07e6173a88045a109a9181e1f86e3a9ac451c74 (plain) (tree)
1
2
3
4
5
6
7
                                      
                                             
                            
                                        
                                          
                               
                                    













                                                                                     
        


                   


                     
          
                   
                  


                                                        




















                                       
                                       







                                      
                                                                                                        
                                        






                                                
                                                





                                                 
                                                                                      
                                                                            
                                                                                      
                                                                            

                                                         






                                                                                                    
                                                                   








                                                                                                    
                                                       
                                                                    
                                                                                          



                                                                                                               



                                                                                                      

                                                        
                                                                                             
                                                                                                               






                                                                                                     
                                                                                                                 








                                                                                                    
                                                       
                                                                    
                                                                                          



                                                                                                               



                                                                                                      

                                                          
                                                                                             
                                                                                                               








                                                                                                  
                                                                                              









                                                                                                 
                                                                                    


                                                 
                                                                              
















                                                                              
                                                                                    

                              
                                                            
                                                                                               
                          



                                      
                                                    

                                                                          
                                                                            


















                                                                                   
                                                                                                  



                                                            
     
                                       







                                                                                                
                                                           
                                                                  




                                                                              
                                                                     



                                                                                                            
                                                                               




                                                
                                            


                                                  
                                                                           
                                      





                                                                                











                                                                                                  




                                             






                                               
                                                             






                                                                                                
                                                    























                                                                                            













                                              
                                                                                  





                                                                                    
                                    














                                                                                             
                                                         




                                                                   
import { Order, ZeroEx } from '0x.js';
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import Dialog from 'material-ui/Dialog';
import Divider from 'material-ui/Divider';
import * as React from 'react';
import * as ReactGA from 'react-ga';
import { Blockchain } from 'ts/blockchain';
import { ExpirationInput } from 'ts/components/inputs/expiration_input';
import { HashInput } from 'ts/components/inputs/hash_input';
import { IdenticonAddressInput } from 'ts/components/inputs/identicon_address_input';
import { TokenAmountInput } from 'ts/components/inputs/token_amount_input';
import { TokenInput } from 'ts/components/inputs/token_input';
import { OrderJSON } from 'ts/components/order_json';
import { Alert } from 'ts/components/ui/alert';
import { HelpTooltip } from 'ts/components/ui/help_tooltip';
import { LifeCycleRaisedButton } from 'ts/components/ui/lifecycle_raised_button';
import { SwapIcon } from 'ts/components/ui/swap_icon';
import { Dispatcher } from 'ts/redux/dispatcher';
import { orderSchema } from 'ts/schemas/order_schema';
import { SchemaValidator } from 'ts/schemas/validator';
import {
    AlertTypes,
    BlockchainErrs,
    HashData,
    Side,
    SideToAssetToken,
    SignatureData,
    Token,
    TokenByAddress,
} from 'ts/types';
import { colors } from 'ts/utils/colors';
import { errorReporter } from 'ts/utils/error_reporter';
import { utils } from 'ts/utils/utils';

enum SigningState {
    UNSIGNED,
    SIGNING,
    SIGNED,
}

interface GenerateOrderFormProps {
    blockchain: Blockchain;
    blockchainErr: BlockchainErrs;
    blockchainIsLoaded: boolean;
    dispatcher: Dispatcher;
    hashData: HashData;
    orderExpiryTimestamp: BigNumber;
    networkId: number;
    userAddress: string;
    orderSignatureData: SignatureData;
    orderTakerAddress: string;
    orderSalt: BigNumber;
    sideToAssetToken: SideToAssetToken;
    tokenByAddress: TokenByAddress;
    lastForceTokenStateRefetch: number;
}

interface GenerateOrderFormState {
    globalErrMsg: string;
    shouldShowIncompleteErrs: boolean;
    signingState: SigningState;
}

export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, GenerateOrderFormState> {
    private _validator: SchemaValidator;
    constructor(props: GenerateOrderFormProps) {
        super(props);
        this.state = {
            globalErrMsg: '',
            shouldShowIncompleteErrs: false,
            signingState: SigningState.UNSIGNED,
        };
        this._validator = new SchemaValidator();
    }
    public componentDidMount() {
        window.scrollTo(0, 0);
    }
    public render() {
        const dispatcher = this.props.dispatcher;
        const depositTokenAddress = this.props.sideToAssetToken[Side.Deposit].address;
        const depositToken = this.props.tokenByAddress[depositTokenAddress];
        const receiveTokenAddress = this.props.sideToAssetToken[Side.Receive].address;
        const receiveToken = this.props.tokenByAddress[receiveTokenAddress];
        const takerExplanation =
            'If a taker is specified, only they are<br> \
                                  allowed to fill this order. If no taker is<br> \
                                  specified, anyone is able to fill it.';
        const exchangeContractIfExists = this.props.blockchain.getExchangeContractAddressIfExists();
        return (
            <div className="clearfix mb2 lg-px4 md-px4 sm-px2">
                <h3>Generate an order</h3>
                <Divider />
                <div className="mx-auto" style={{ maxWidth: 580 }}>
                    <div className="pt3">
                        <div className="mx-auto clearfix">
                            <div className="lg-col md-col lg-col-5 md-col-5 sm-col sm-col-5 sm-pb2">
                                <TokenInput
                                    userAddress={this.props.userAddress}
                                    blockchain={this.props.blockchain}
                                    blockchainErr={this.props.blockchainErr}
                                    dispatcher={this.props.dispatcher}
                                    label="Selling"
                                    side={Side.Deposit}
                                    networkId={this.props.networkId}
                                    assetToken={this.props.sideToAssetToken[Side.Deposit]}
                                    updateChosenAssetToken={dispatcher.updateChosenAssetToken.bind(dispatcher)}
                                    tokenByAddress={this.props.tokenByAddress}
                                />
                                <TokenAmountInput
                                    lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
                                    blockchain={this.props.blockchain}
                                    userAddress={this.props.userAddress}
                                    networkId={this.props.networkId}
                                    label="Sell amount"
                                    token={depositToken}
                                    amount={this.props.sideToAssetToken[Side.Deposit].amount}
                                    onChange={this._onTokenAmountChange.bind(this, depositToken, Side.Deposit)}
                                    shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs}
                                    shouldCheckBalance={true}
                                    shouldCheckAllowance={true}
                                />
                            </div>
                            <div className="lg-col md-col lg-col-2 md-col-2 sm-col sm-col-2 xs-hide">
                                <div className="p1">
                                    <SwapIcon swapTokensFn={dispatcher.swapAssetTokenSymbols.bind(dispatcher)} />
                                </div>
                            </div>
                            <div className="lg-col md-col lg-col-5 md-col-5 sm-col sm-col-5 sm-pb2">
                                <TokenInput
                                    userAddress={this.props.userAddress}
                                    blockchain={this.props.blockchain}
                                    blockchainErr={this.props.blockchainErr}
                                    dispatcher={this.props.dispatcher}
                                    label="Buying"
                                    side={Side.Receive}
                                    networkId={this.props.networkId}
                                    assetToken={this.props.sideToAssetToken[Side.Receive]}
                                    updateChosenAssetToken={dispatcher.updateChosenAssetToken.bind(dispatcher)}
                                    tokenByAddress={this.props.tokenByAddress}
                                />
                                <TokenAmountInput
                                    lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
                                    blockchain={this.props.blockchain}
                                    userAddress={this.props.userAddress}
                                    networkId={this.props.networkId}
                                    label="Receive amount"
                                    token={receiveToken}
                                    amount={this.props.sideToAssetToken[Side.Receive].amount}
                                    onChange={this._onTokenAmountChange.bind(this, receiveToken, Side.Receive)}
                                    shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs}
                                    shouldCheckBalance={false}
                                    shouldCheckAllowance={false}
                                />
                            </div>
                        </div>
                    </div>
                    <div className="pt1 sm-pb2 lg-px4 md-px4">
                        <div className="lg-px3 md-px3">
                            <div style={{ fontSize: 12, color: colors.grey }}>Expiration</div>
                            <ExpirationInput
                                orderExpiryTimestamp={this.props.orderExpiryTimestamp}
                                updateOrderExpiry={dispatcher.updateOrderExpiry.bind(dispatcher)}
                            />
                        </div>
                    </div>
                    <div className="pt1 flex mx-auto">
                        <IdenticonAddressInput
                            label="Taker"
                            initialAddress={this.props.orderTakerAddress}
                            updateOrderAddress={this._updateOrderAddress.bind(this)}
                        />
                        <div className="pt3">
                            <div className="pl1">
                                <HelpTooltip explanation={takerExplanation} />
                            </div>
                        </div>
                    </div>
                    <div>
                        <HashInput
                            blockchain={this.props.blockchain}
                            blockchainIsLoaded={this.props.blockchainIsLoaded}
                            hashData={this.props.hashData}
                            label="Order Hash"
                        />
                    </div>
                    <div className="pt2">
                        <div className="center">
                            <LifeCycleRaisedButton
                                labelReady="Sign hash"
                                labelLoading="Signing..."
                                labelComplete="Hash signed!"
                                onClickAsyncFn={this._onSignClickedAsync.bind(this)}
                            />
                        </div>
                        {this.state.globalErrMsg !== '' && (
                            <Alert type={AlertTypes.ERROR} message={this.state.globalErrMsg} />
                        )}
                    </div>
                </div>
                <Dialog
                    title="Order JSON"
                    titleStyle={{ fontWeight: 100 }}
                    modal={false}
                    open={this.state.signingState === SigningState.SIGNED}
                    onRequestClose={this._onCloseOrderJSONDialog.bind(this)}
                >
                    <OrderJSON
                        exchangeContractIfExists={exchangeContractIfExists}
                        orderExpiryTimestamp={this.props.orderExpiryTimestamp}
                        orderSignatureData={this.props.orderSignatureData}
                        orderTakerAddress={this.props.orderTakerAddress}
                        orderMakerAddress={this.props.userAddress}
                        orderSalt={this.props.orderSalt}
                        orderMakerFee={this.props.hashData.makerFee}
                        orderTakerFee={this.props.hashData.takerFee}
                        orderFeeRecipient={this.props.hashData.feeRecipientAddress}
                        networkId={this.props.networkId}
                        sideToAssetToken={this.props.sideToAssetToken}
                        tokenByAddress={this.props.tokenByAddress}
                    />
                </Dialog>
            </div>
        );
    }
    private _onTokenAmountChange(token: Token, side: Side, isValid: boolean, amount?: BigNumber) {
        this.props.dispatcher.updateChosenAssetToken(side, {
            address: token.address,
            amount,
        });
    }
    private _onCloseOrderJSONDialog() {
        // Upon closing the order JSON dialog, we update the orderSalt stored in the Redux store
        // with a new value so that if a user signs the identical order again, the newly signed
        // orderHash will not collide with the previously generated orderHash.
        this.props.dispatcher.updateOrderSalt(ZeroEx.generatePseudoRandomSalt());
        this.setState({
            signingState: SigningState.UNSIGNED,
        });
    }
    private async _onSignClickedAsync(): Promise<boolean> {
        if (this.props.blockchainErr !== BlockchainErrs.NoError) {
            this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
            return false;
        }

        // Check if all required inputs were supplied
        const debitToken = this.props.sideToAssetToken[Side.Deposit];
        const [debitBalance, debitAllowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
            this.props.userAddress,
            debitToken.address,
        );
        const receiveAmount = this.props.sideToAssetToken[Side.Receive].amount;
        if (
            !_.isUndefined(debitToken.amount) &&
            !_.isUndefined(receiveAmount) &&
            debitToken.amount.gt(0) &&
            receiveAmount.gt(0) &&
            this.props.userAddress !== '' &&
            debitBalance.gte(debitToken.amount) &&
            debitAllowance.gte(debitToken.amount)
        ) {
            const didSignSuccessfully = await this._signTransactionAsync();
            if (didSignSuccessfully) {
                ReactGA.event({
                    category: 'Portal',
                    action: 'Sign Order Success',
                    label: this.props.tokenByAddress[debitToken.address].symbol,
                    value: debitToken.amount,
                });
                this.setState({
                    globalErrMsg: '',
                    shouldShowIncompleteErrs: false,
                });
            }
            return didSignSuccessfully;
        } else {
            let globalErrMsg = 'You must fix the above errors in order to generate a valid order';
            if (this.props.userAddress === '') {
                globalErrMsg = 'You must enable wallet communication';
                this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
            }
            ReactGA.event({
                category: 'Portal',
                action: 'Sign Order Failure',
                label: globalErrMsg,
            });
            this.setState({
                globalErrMsg,
                shouldShowIncompleteErrs: true,
            });
            return false;
        }
    }
    private async _signTransactionAsync(): Promise<boolean> {
        this.setState({
            signingState: SigningState.SIGNING,
        });
        const exchangeContractAddr = this.props.blockchain.getExchangeContractAddressIfExists();
        if (_.isUndefined(exchangeContractAddr)) {
            this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
            this.setState({
                signingState: SigningState.UNSIGNED,
            });
            return false;
        }
        const hashData = this.props.hashData;

        const zeroExOrder: Order = {
            exchangeContractAddress: exchangeContractAddr,
            expirationUnixTimestampSec: hashData.orderExpiryTimestamp,
            feeRecipient: hashData.feeRecipientAddress,
            maker: hashData.orderMakerAddress,
            makerFee: hashData.makerFee,
            makerTokenAddress: hashData.depositTokenContractAddr,
            makerTokenAmount: hashData.depositAmount,
            salt: hashData.orderSalt,
            taker: hashData.orderTakerAddress,
            takerFee: hashData.takerFee,
            takerTokenAddress: hashData.receiveTokenContractAddr,
            takerTokenAmount: hashData.receiveAmount,
        };
        const orderHash = ZeroEx.getOrderHashHex(zeroExOrder);

        let globalErrMsg = '';
        try {
            const signatureData = await this.props.blockchain.signOrderHashAsync(orderHash);
            const order = utils.generateOrder(
                this.props.networkId,
                exchangeContractAddr,
                this.props.sideToAssetToken,
                hashData.orderExpiryTimestamp,
                this.props.orderTakerAddress,
                this.props.userAddress,
                hashData.makerFee,
                hashData.takerFee,
                hashData.feeRecipientAddress,
                signatureData,
                this.props.tokenByAddress,
                hashData.orderSalt,
            );
            const validationResult = this._validator.validate(order, orderSchema);
            if (validationResult.errors.length > 0) {
                globalErrMsg = 'Order signing failed. Please refresh and try again';
                utils.consoleLog(`Unexpected error occured: Order validation failed:
                                  ${validationResult.errors}`);
            }
        } catch (err) {
            const errMsg = `${err}`;
            if (utils.didUserDenyWeb3Request(errMsg)) {
                globalErrMsg = 'User denied sign request';
            } else {
                globalErrMsg = 'An unexpected error occured. Please try refreshing the page';
                utils.consoleLog(`Unexpected error occured: ${err}`);
                utils.consoleLog(err.stack);
                await errorReporter.reportAsync(err);
            }
        }
        this.setState({
            signingState: globalErrMsg === '' ? SigningState.SIGNED : SigningState.UNSIGNED,
            globalErrMsg,
        });
        return globalErrMsg === '';
    }
    private _updateOrderAddress(address?: string): void {
        if (!_.isUndefined(address)) {
            this.props.dispatcher.updateOrderTakerAddress(address);
        }
    }
}