aboutsummaryrefslogtreecommitdiffstats
path: root/packages/website/ts/components/generate_order
diff options
context:
space:
mode:
authorFabio Berger <me@fabioberger.com>2017-11-22 04:03:08 +0800
committerFabio Berger <me@fabioberger.com>2017-11-22 04:03:08 +0800
commit3660ba28d73d70d08bf14c33ef680e5ef3ec7f3b (patch)
treef101656799da807489253e17bea7abfaea90b62d /packages/website/ts/components/generate_order
parent037f466e1f80f635b48f3235258402e2ce75fb7b (diff)
downloaddexon-sol-tools-3660ba28d73d70d08bf14c33ef680e5ef3ec7f3b.tar.gz
dexon-sol-tools-3660ba28d73d70d08bf14c33ef680e5ef3ec7f3b.tar.zst
dexon-sol-tools-3660ba28d73d70d08bf14c33ef680e5ef3ec7f3b.zip
Add website to mono repo, update packages to align with existing sub-packages, use new subscribeAsync 0x.js method
Diffstat (limited to 'packages/website/ts/components/generate_order')
-rw-r--r--packages/website/ts/components/generate_order/asset_picker.tsx291
-rw-r--r--packages/website/ts/components/generate_order/generate_order_form.tsx348
-rw-r--r--packages/website/ts/components/generate_order/new_token_form.tsx237
3 files changed, 876 insertions, 0 deletions
diff --git a/packages/website/ts/components/generate_order/asset_picker.tsx b/packages/website/ts/components/generate_order/asset_picker.tsx
new file mode 100644
index 000000000..59826d06e
--- /dev/null
+++ b/packages/website/ts/components/generate_order/asset_picker.tsx
@@ -0,0 +1,291 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import {colors} from 'material-ui/styles';
+import Dialog from 'material-ui/Dialog';
+import GridList from 'material-ui/GridList/GridList';
+import GridTile from 'material-ui/GridList/GridTile';
+import FlatButton from 'material-ui/FlatButton';
+import {utils} from 'ts/utils/utils';
+import {Blockchain} from 'ts/blockchain';
+import {Dispatcher} from 'ts/redux/dispatcher';
+import {
+ Token,
+ AssetToken,
+ TokenByAddress,
+ Styles,
+ TokenState,
+ DialogConfigs,
+ TokenVisibility,
+} from 'ts/types';
+import {NewTokenForm} from 'ts/components/generate_order/new_token_form';
+import {trackedTokenStorage} from 'ts/local_storage/tracked_token_storage';
+import {TrackTokenConfirmation} from 'ts/components/track_token_confirmation';
+import {TokenIcon} from 'ts/components/ui/token_icon';
+
+const TOKEN_ICON_DIMENSION = 100;
+const TILE_DIMENSION = 146;
+enum AssetViews {
+ ASSET_PICKER = 'ASSET_PICKER',
+ NEW_TOKEN_FORM = 'NEW_TOKEN_FORM',
+ CONFIRM_TRACK_TOKEN = 'CONFIRM_TRACK_TOKEN',
+}
+
+interface AssetPickerProps {
+ userAddress: string;
+ blockchain: Blockchain;
+ dispatcher: Dispatcher;
+ networkId: number;
+ isOpen: boolean;
+ currentTokenAddress: string;
+ onTokenChosen: (tokenAddress: string) => void;
+ tokenByAddress: TokenByAddress;
+ tokenVisibility?: TokenVisibility;
+}
+
+interface AssetPickerState {
+ assetView: AssetViews;
+ hoveredAddress: string | undefined;
+ chosenTrackTokenAddress: string;
+ isAddingTokenToTracked: boolean;
+}
+
+export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerState> {
+ public static defaultProps: Partial<AssetPickerProps> = {
+ tokenVisibility: TokenVisibility.ALL,
+ };
+ private dialogConfigsByAssetView: {[assetView: string]: DialogConfigs};
+ constructor(props: AssetPickerProps) {
+ super(props);
+ this.state = {
+ assetView: AssetViews.ASSET_PICKER,
+ hoveredAddress: undefined,
+ chosenTrackTokenAddress: undefined,
+ isAddingTokenToTracked: false,
+ };
+ this.dialogConfigsByAssetView = {
+ [AssetViews.ASSET_PICKER]: {
+ title: 'Select token',
+ isModal: false,
+ actions: [],
+ },
+ [AssetViews.NEW_TOKEN_FORM]: {
+ title: 'Add an ERC20 token',
+ isModal: false,
+ actions: [],
+ },
+ [AssetViews.CONFIRM_TRACK_TOKEN]: {
+ title: 'Tracking confirmation',
+ isModal: true,
+ actions: [
+ <FlatButton
+ key="noTracking"
+ label="No"
+ onTouchTap={this.onTrackConfirmationRespondedAsync.bind(this, false)}
+ />,
+ <FlatButton
+ key="yesTrack"
+ label="Yes"
+ onTouchTap={this.onTrackConfirmationRespondedAsync.bind(this, true)}
+ />,
+ ],
+ },
+ };
+ }
+ public render() {
+ const dialogConfigs: DialogConfigs = this.dialogConfigsByAssetView[this.state.assetView];
+ return (
+ <Dialog
+ title={dialogConfigs.title}
+ titleStyle={{fontWeight: 100}}
+ modal={dialogConfigs.isModal}
+ open={this.props.isOpen}
+ actions={dialogConfigs.actions}
+ onRequestClose={this.onCloseDialog.bind(this)}
+ >
+ {this.state.assetView === AssetViews.ASSET_PICKER &&
+ this.renderAssetPicker()
+ }
+ {this.state.assetView === AssetViews.NEW_TOKEN_FORM &&
+ <NewTokenForm
+ blockchain={this.props.blockchain}
+ onNewTokenSubmitted={this.onNewTokenSubmitted.bind(this)}
+ tokenByAddress={this.props.tokenByAddress}
+ />
+ }
+ {this.state.assetView === AssetViews.CONFIRM_TRACK_TOKEN &&
+ this.renderConfirmTrackToken()
+ }
+ </Dialog>
+ );
+ }
+ private renderConfirmTrackToken() {
+ const token = this.props.tokenByAddress[this.state.chosenTrackTokenAddress];
+ return (
+ <TrackTokenConfirmation
+ tokens={[token]}
+ tokenByAddress={this.props.tokenByAddress}
+ networkId={this.props.networkId}
+ isAddingTokenToTracked={this.state.isAddingTokenToTracked}
+ />
+ );
+ }
+ private renderAssetPicker() {
+ return (
+ <div
+ className="clearfix flex flex-wrap"
+ style={{overflowY: 'auto', maxWidth: 720, maxHeight: 356, marginBottom: 10}}
+ >
+ {this.renderGridTiles()}
+ </div>
+ );
+ }
+ private renderGridTiles() {
+ let isHovered;
+ let tileStyles;
+ const gridTiles = _.map(this.props.tokenByAddress, (token: Token, address: string) => {
+ if ((this.props.tokenVisibility === TokenVisibility.TRACKED && !token.isTracked) ||
+ (this.props.tokenVisibility === TokenVisibility.UNTRACKED && token.isTracked)) {
+ return null; // Skip
+ }
+ isHovered = this.state.hoveredAddress === address;
+ tileStyles = {
+ cursor: 'pointer',
+ opacity: isHovered ? 0.6 : 1,
+ };
+ return (
+ <div
+ key={address}
+ style={{width: TILE_DIMENSION, height: TILE_DIMENSION, ...tileStyles}}
+ className="p2 mx-auto"
+ onClick={this.onChooseToken.bind(this, address)}
+ onMouseEnter={this.onToggleHover.bind(this, address, true)}
+ onMouseLeave={this.onToggleHover.bind(this, address, false)}
+ >
+ <div className="p1 center">
+ <TokenIcon token={token} diameter={TOKEN_ICON_DIMENSION} />
+ </div>
+ <div className="center">{token.name}</div>
+ </div>
+ );
+ });
+ const otherTokenKey = 'otherToken';
+ isHovered = this.state.hoveredAddress === otherTokenKey;
+ tileStyles = {
+ cursor: 'pointer',
+ opacity: isHovered ? 0.6 : 1,
+ };
+ if (this.props.tokenVisibility !== TokenVisibility.TRACKED) {
+ gridTiles.push((
+ <div
+ key={otherTokenKey}
+ style={{width: TILE_DIMENSION, height: TILE_DIMENSION, ...tileStyles}}
+ className="p2 mx-auto"
+ onClick={this.onCustomAssetChosen.bind(this)}
+ onMouseEnter={this.onToggleHover.bind(this, otherTokenKey, true)}
+ onMouseLeave={this.onToggleHover.bind(this, otherTokenKey, false)}
+ >
+ <div className="p1 center">
+ <i
+ style={{fontSize: 105, paddingLeft: 1, paddingRight: 1}}
+ className="zmdi zmdi-plus-circle"
+ />
+ </div>
+ <div className="center">Other ERC20 Token</div>
+ </div>
+ ));
+ }
+ return gridTiles;
+ }
+ private onToggleHover(address: string, isHovered: boolean) {
+ const hoveredAddress = isHovered ? address : undefined;
+ this.setState({
+ hoveredAddress,
+ });
+ }
+ private onCloseDialog() {
+ this.setState({
+ assetView: AssetViews.ASSET_PICKER,
+ });
+ this.props.onTokenChosen(this.props.currentTokenAddress);
+ }
+ private onChooseToken(tokenAddress: string) {
+ const token = this.props.tokenByAddress[tokenAddress];
+ if (token.isTracked) {
+ this.props.onTokenChosen(tokenAddress);
+ } else {
+ this.setState({
+ assetView: AssetViews.CONFIRM_TRACK_TOKEN,
+ chosenTrackTokenAddress: tokenAddress,
+ });
+ }
+ }
+ private getTitle() {
+ switch (this.state.assetView) {
+ case AssetViews.ASSET_PICKER:
+ return 'Select token';
+
+ case AssetViews.NEW_TOKEN_FORM:
+ return 'Add an ERC20 token';
+
+ case AssetViews.CONFIRM_TRACK_TOKEN:
+ return 'Tracking confirmation';
+
+ default:
+ throw utils.spawnSwitchErr('assetView', this.state.assetView);
+ }
+ }
+ private onCustomAssetChosen() {
+ this.setState({
+ assetView: AssetViews.NEW_TOKEN_FORM,
+ });
+ }
+ private onNewTokenSubmitted(newToken: Token, newTokenState: TokenState) {
+ this.props.dispatcher.updateTokenStateByAddress({
+ [newToken.address]: newTokenState,
+ });
+ trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newToken);
+ this.props.dispatcher.addTokenToTokenByAddress(newToken);
+ this.setState({
+ assetView: AssetViews.ASSET_PICKER,
+ });
+ this.props.onTokenChosen(newToken.address);
+ }
+ private async onTrackConfirmationRespondedAsync(didUserAcceptTracking: boolean) {
+ if (!didUserAcceptTracking) {
+ this.setState({
+ isAddingTokenToTracked: false,
+ assetView: AssetViews.ASSET_PICKER,
+ chosenTrackTokenAddress: undefined,
+ });
+ this.onCloseDialog();
+ return;
+ }
+ this.setState({
+ isAddingTokenToTracked: true,
+ });
+ const tokenAddress = this.state.chosenTrackTokenAddress;
+ const token = this.props.tokenByAddress[tokenAddress];
+ const newTokenEntry = _.assign({}, token);
+
+ newTokenEntry.isTracked = true;
+ trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newTokenEntry);
+ this.props.dispatcher.updateTokenByAddress([newTokenEntry]);
+
+ const [
+ balance,
+ allowance,
+ ] = await this.props.blockchain.getCurrentUserTokenBalanceAndAllowanceAsync(token.address);
+ this.props.dispatcher.updateTokenStateByAddress({
+ [token.address]: {
+ balance,
+ allowance,
+ },
+ });
+ this.setState({
+ isAddingTokenToTracked: false,
+ assetView: AssetViews.ASSET_PICKER,
+ chosenTrackTokenAddress: undefined,
+ });
+ this.props.onTokenChosen(tokenAddress);
+ }
+}
diff --git a/packages/website/ts/components/generate_order/generate_order_form.tsx b/packages/website/ts/components/generate_order/generate_order_form.tsx
new file mode 100644
index 000000000..e9026d9bc
--- /dev/null
+++ b/packages/website/ts/components/generate_order/generate_order_form.tsx
@@ -0,0 +1,348 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import {ZeroEx, Order} from '0x.js';
+import BigNumber from 'bignumber.js';
+import {Blockchain} from 'ts/blockchain';
+import Divider from 'material-ui/Divider';
+import Dialog from 'material-ui/Dialog';
+import {colors} from 'material-ui/styles';
+import {Dispatcher} from 'ts/redux/dispatcher';
+import {utils} from 'ts/utils/utils';
+import {SchemaValidator} from 'ts/schemas/validator';
+import {orderSchema} from 'ts/schemas/order_schema';
+import {Alert} from 'ts/components/ui/alert';
+import {OrderJSON} from 'ts/components/order_json';
+import {IdenticonAddressInput} from 'ts/components/inputs/identicon_address_input';
+import {TokenInput} from 'ts/components/inputs/token_input';
+import {TokenAmountInput} from 'ts/components/inputs/token_amount_input';
+import {HashInput} from 'ts/components/inputs/hash_input';
+import {ExpirationInput} from 'ts/components/inputs/expiration_input';
+import {LifeCycleRaisedButton} from 'ts/components/ui/lifecycle_raised_button';
+import {errorReporter} from 'ts/utils/error_reporter';
+import {HelpTooltip} from 'ts/components/ui/help_tooltip';
+import {SwapIcon} from 'ts/components/ui/swap_icon';
+import {
+ Side,
+ SideToAssetToken,
+ SignatureData,
+ HashData,
+ TokenByAddress,
+ TokenStateByAddress,
+ BlockchainErrs,
+ Token,
+ AlertTypes,
+} from 'ts/types';
+
+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;
+ tokenStateByAddress: TokenStateByAddress;
+}
+
+interface GenerateOrderFormState {
+ globalErrMsg: string;
+ shouldShowIncompleteErrs: boolean;
+ signingState: SigningState;
+}
+
+const style = {
+ paper: {
+ display: 'inline-block',
+ position: 'relative',
+ textAlign: 'center',
+ width: '100%',
+ },
+};
+
+export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, any> {
+ 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 depositTokenState = this.props.tokenStateByAddress[depositTokenAddress];
+ const receiveTokenAddress = this.props.sideToAssetToken[Side.receive].address;
+ const receiveToken = this.props.tokenByAddress[receiveTokenAddress];
+ const receiveTokenState = this.props.tokenStateByAddress[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
+ label="Sell amount"
+ token={depositToken}
+ tokenState={depositTokenState}
+ 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
+ label="Receive amount"
+ token={receiveToken}
+ tokenState={receiveTokenState}
+ 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.grey500}}>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 !== '') {
+ this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
+ return false;
+ }
+
+ // Check if all required inputs were supplied
+ const debitToken = this.props.sideToAssetToken[Side.deposit];
+ const debitBalance = this.props.tokenStateByAddress[debitToken.address].balance;
+ const debitAllowance = this.props.tokenStateByAddress[debitToken.address].allowance;
+ 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) {
+ 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);
+ }
+ 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({
+ isSigning: false,
+ });
+ 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);
+ }
+ }
+}
diff --git a/packages/website/ts/components/generate_order/new_token_form.tsx b/packages/website/ts/components/generate_order/new_token_form.tsx
new file mode 100644
index 000000000..95c05f5bb
--- /dev/null
+++ b/packages/website/ts/components/generate_order/new_token_form.tsx
@@ -0,0 +1,237 @@
+import * as _ from 'lodash';
+import * as React from 'react';
+import {colors} from 'material-ui/styles';
+import TextField from 'material-ui/TextField';
+import {constants} from 'ts/utils/constants';
+import {Blockchain} from 'ts/blockchain';
+import {Token, TokenState, TokenByAddress, AlertTypes} from 'ts/types';
+import {AddressInput} from 'ts/components/inputs/address_input';
+import {Alert} from 'ts/components/ui/alert';
+import {LifeCycleRaisedButton} from 'ts/components/ui/lifecycle_raised_button';
+import {RequiredLabel} from 'ts/components/ui/required_label';
+import BigNumber from 'bignumber.js';
+
+interface NewTokenFormProps {
+ blockchain: Blockchain;
+ tokenByAddress: TokenByAddress;
+ onNewTokenSubmitted: (token: Token, tokenState: TokenState) => void;
+}
+
+interface NewTokenFormState {
+ globalErrMsg: string;
+ name: string;
+ nameErrText: string;
+ symbol: string;
+ symbolErrText: string;
+ address: string;
+ shouldShowAddressIncompleteErr: boolean;
+ decimals: string;
+ decimalsErrText: string;
+}
+
+export class NewTokenForm extends React.Component<NewTokenFormProps, NewTokenFormState> {
+ constructor(props: NewTokenFormProps) {
+ super(props);
+ this.state = {
+ address: '',
+ globalErrMsg: '',
+ name: '',
+ nameErrText: '',
+ shouldShowAddressIncompleteErr: false,
+ symbol: '',
+ symbolErrText: '',
+ decimals: '18',
+ decimalsErrText: '',
+ };
+ }
+ public render() {
+ return (
+ <div className="mx-auto pb2" style={{width: 256}}>
+ <div>
+ <TextField
+ floatingLabelFixed={true}
+ floatingLabelStyle={{color: colors.grey500}}
+ floatingLabelText={<RequiredLabel label="Name" />}
+ value={this.state.name}
+ errorText={this.state.nameErrText}
+ onChange={this.onTokenNameChanged.bind(this)}
+ />
+ </div>
+ <div>
+ <TextField
+ floatingLabelFixed={true}
+ floatingLabelStyle={{color: colors.grey500}}
+ floatingLabelText={<RequiredLabel label="Symbol" />}
+ value={this.state.symbol}
+ errorText={this.state.symbolErrText}
+ onChange={this.onTokenSymbolChanged.bind(this)}
+ />
+ </div>
+ <div>
+ <AddressInput
+ isRequired={true}
+ label="Contract address"
+ initialAddress=""
+ shouldShowIncompleteErrs={this.state.shouldShowAddressIncompleteErr}
+ updateAddress={this.onTokenAddressChanged.bind(this)}
+ />
+ </div>
+ <div>
+ <TextField
+ floatingLabelFixed={true}
+ floatingLabelStyle={{color: colors.grey500}}
+ floatingLabelText={<RequiredLabel label="Decimals" />}
+ value={this.state.decimals}
+ errorText={this.state.decimalsErrText}
+ onChange={this.onTokenDecimalsChanged.bind(this)}
+ />
+ </div>
+ <div className="pt2 mx-auto" style={{width: 120}}>
+ <LifeCycleRaisedButton
+ labelReady="Add"
+ labelLoading="Adding..."
+ labelComplete="Added!"
+ onClickAsyncFn={this.onAddNewTokenClickAsync.bind(this)}
+ />
+ </div>
+ {this.state.globalErrMsg !== '' &&
+ <Alert type={AlertTypes.ERROR} message={this.state.globalErrMsg} />
+ }
+ </div>
+ );
+ }
+ private async onAddNewTokenClickAsync() {
+ // Trigger validation of name and symbol
+ this.onTokenNameChanged(undefined, this.state.name);
+ this.onTokenSymbolChanged(undefined, this.state.symbol);
+ this.onTokenDecimalsChanged(undefined, this.state.decimals);
+
+ const isAddressIncomplete = this.state.address === '';
+ let doesContractExist = false;
+ if (!isAddressIncomplete) {
+ doesContractExist = await this.props.blockchain.doesContractExistAtAddressAsync(this.state.address);
+ }
+
+ let hasBalanceAllowanceErr = false;
+ let balance = new BigNumber(0);
+ let allowance = new BigNumber(0);
+ if (doesContractExist) {
+ try {
+ [
+ balance,
+ allowance,
+ ] = await this.props.blockchain.getCurrentUserTokenBalanceAndAllowanceAsync(this.state.address);
+ } catch (err) {
+ hasBalanceAllowanceErr = true;
+ }
+ }
+
+ let globalErrMsg = '';
+ if (this.state.nameErrText !== '' || this.state.symbolErrText !== '' ||
+ this.state.decimalsErrText !== '' || isAddressIncomplete) {
+ globalErrMsg = 'Please fix the above issues';
+ } else if (!doesContractExist) {
+ globalErrMsg = 'No contract found at supplied address';
+ } else if (hasBalanceAllowanceErr) {
+ globalErrMsg = 'Unsuccessful call to `balanceOf` and/or `allowance` on supplied contract address';
+ } else if (!isAddressIncomplete && !_.isUndefined(this.props.tokenByAddress[this.state.address])) {
+ globalErrMsg = 'A token already exists with this address';
+ }
+
+ if (globalErrMsg !== '') {
+ this.setState({
+ globalErrMsg,
+ shouldShowAddressIncompleteErr: isAddressIncomplete,
+ });
+ return;
+ }
+
+ const newToken: Token = {
+ address: this.state.address,
+ decimals: _.parseInt(this.state.decimals),
+ iconUrl: undefined,
+ name: this.state.name,
+ symbol: this.state.symbol.toUpperCase(),
+ isTracked: true,
+ isRegistered: false,
+ };
+ const newTokenState: TokenState = {
+ balance,
+ allowance,
+ };
+ this.props.onNewTokenSubmitted(newToken, newTokenState);
+ }
+ private onTokenNameChanged(e: any, name: string) {
+ let nameErrText = '';
+ const maxLength = 30;
+ const tokens = _.values(this.props.tokenByAddress);
+ const tokenWithNameIfExists = _.find(tokens, {name});
+ const tokenWithNameExists = !_.isUndefined(tokenWithNameIfExists);
+ if (name === '') {
+ nameErrText = 'Name is required';
+ } else if (!this.isValidName(name)) {
+ nameErrText = 'Name should only contain letters, digits and spaces';
+ } else if (name.length > maxLength) {
+ nameErrText = `Max length is ${maxLength}`;
+ } else if (tokenWithNameExists) {
+ nameErrText = 'Token with this name already exists';
+ }
+
+ this.setState({
+ name,
+ nameErrText,
+ });
+ }
+ private onTokenSymbolChanged(e: any, symbol: string) {
+ let symbolErrText = '';
+ const maxLength = 5;
+ const tokens = _.values(this.props.tokenByAddress);
+ const tokenWithSymbolExists = !_.isUndefined(_.find(tokens, {symbol}));
+ if (symbol === '') {
+ symbolErrText = 'Symbol is required';
+ } else if (!this.isLetters(symbol)) {
+ symbolErrText = 'Can only include letters';
+ } else if (symbol.length > maxLength) {
+ symbolErrText = `Max length is ${maxLength}`;
+ } else if (tokenWithSymbolExists) {
+ symbolErrText = 'Token with symbol already exists';
+ }
+
+ this.setState({
+ symbol,
+ symbolErrText,
+ });
+ }
+ private onTokenDecimalsChanged(e: any, decimals: string) {
+ let decimalsErrText = '';
+ const maxLength = 2;
+ if (decimals === '') {
+ decimalsErrText = 'Decimals is required';
+ } else if (!this.isInteger(decimals)) {
+ decimalsErrText = 'Must be an integer';
+ } else if (decimals.length > maxLength) {
+ decimalsErrText = `Max length is ${maxLength}`;
+ }
+
+ this.setState({
+ decimals,
+ decimalsErrText,
+ });
+ }
+ private onTokenAddressChanged(address?: string) {
+ if (!_.isUndefined(address)) {
+ this.setState({
+ address,
+ });
+ }
+ }
+ private isValidName(input: string) {
+ return /^[a-z0-9 ]+$/i.test(input);
+ }
+ private isInteger(input: string) {
+ return /^[0-9]+$/i.test(input);
+ }
+ private isLetters(input: string) {
+ return /^[a-zA-Z]+$/i.test(input);
+ }
+}