Add website to mono repo, update packages to align with existing sub-packages, use new subscribeAsync 0x.js method
This commit is contained in:
770
packages/website/ts/blockchain.ts
Normal file
770
packages/website/ts/blockchain.ts
Normal file
@@ -0,0 +1,770 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ZeroEx,
|
||||
ZeroExError,
|
||||
ExchangeContractErrs,
|
||||
ExchangeContractEventArgs,
|
||||
ExchangeEvents,
|
||||
SubscriptionOpts,
|
||||
IndexedFilterValues,
|
||||
DecodedLogEvent,
|
||||
BlockParam,
|
||||
LogFillContractEventArgs,
|
||||
LogCancelContractEventArgs,
|
||||
Token as ZeroExToken,
|
||||
LogWithDecodedArgs,
|
||||
TransactionReceiptWithDecodedLogs,
|
||||
SignedOrder,
|
||||
Order,
|
||||
} from '0x.js';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import Web3 = require('web3');
|
||||
import promisify = require('es6-promisify');
|
||||
import findVersions = require('find-versions');
|
||||
import compareVersions = require('compare-versions');
|
||||
import contract = require('truffle-contract');
|
||||
import ethUtil = require('ethereumjs-util');
|
||||
import ProviderEngine = require('web3-provider-engine');
|
||||
import FilterSubprovider = require('web3-provider-engine/subproviders/filters');
|
||||
import {TransactionSubmitted} from 'ts/components/flash_messages/transaction_submitted';
|
||||
import {TokenSendCompleted} from 'ts/components/flash_messages/token_send_completed';
|
||||
import {RedundantRPCSubprovider} from 'ts/subproviders/redundant_rpc_subprovider';
|
||||
import {InjectedWeb3SubProvider} from 'ts/subproviders/injected_web3_subprovider';
|
||||
import {ledgerWalletSubproviderFactory} from 'ts/subproviders/ledger_wallet_subprovider_factory';
|
||||
import {Dispatcher} from 'ts/redux/dispatcher';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {configs} from 'ts/utils/configs';
|
||||
import {
|
||||
BlockchainErrs,
|
||||
Token,
|
||||
SignatureData,
|
||||
Side,
|
||||
ContractResponse,
|
||||
BlockchainCallErrs,
|
||||
ContractInstance,
|
||||
ProviderType,
|
||||
LedgerWalletSubprovider,
|
||||
EtherscanLinkSuffixes,
|
||||
TokenByAddress,
|
||||
TokenStateByAddress,
|
||||
} from 'ts/types';
|
||||
import {Web3Wrapper} from 'ts/web3_wrapper';
|
||||
import {errorReporter} from 'ts/utils/error_reporter';
|
||||
import {tradeHistoryStorage} from 'ts/local_storage/trade_history_storage';
|
||||
import {trackedTokenStorage} from 'ts/local_storage/tracked_token_storage';
|
||||
import * as MintableArtifacts from '../contracts/Mintable.json';
|
||||
|
||||
const ALLOWANCE_TO_ZERO_GAS_AMOUNT = 45730;
|
||||
const BLOCK_NUMBER_BACK_TRACK = 50;
|
||||
|
||||
export class Blockchain {
|
||||
public networkId: number;
|
||||
public nodeVersion: string;
|
||||
private zeroEx: ZeroEx;
|
||||
private dispatcher: Dispatcher;
|
||||
private web3Wrapper: Web3Wrapper;
|
||||
private exchangeAddress: string;
|
||||
private tokenTransferProxy: ContractInstance;
|
||||
private tokenRegistry: ContractInstance;
|
||||
private userAddress: string;
|
||||
private cachedProvider: Web3.Provider;
|
||||
private ledgerSubProvider: LedgerWalletSubprovider;
|
||||
private zrxPollIntervalId: number;
|
||||
constructor(dispatcher: Dispatcher, isSalePage: boolean = false) {
|
||||
this.dispatcher = dispatcher;
|
||||
this.userAddress = '';
|
||||
this.onPageLoadInitFireAndForgetAsync();
|
||||
}
|
||||
public async networkIdUpdatedFireAndForgetAsync(newNetworkId: number) {
|
||||
const isConnected = !_.isUndefined(newNetworkId);
|
||||
if (!isConnected) {
|
||||
this.networkId = newNetworkId;
|
||||
this.dispatcher.encounteredBlockchainError(BlockchainErrs.DISCONNECTED_FROM_ETHEREUM_NODE);
|
||||
this.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
|
||||
} else if (this.networkId !== newNetworkId) {
|
||||
this.networkId = newNetworkId;
|
||||
this.dispatcher.encounteredBlockchainError('');
|
||||
await this.fetchTokenInformationAsync();
|
||||
await this.rehydrateStoreWithContractEvents();
|
||||
}
|
||||
}
|
||||
public async userAddressUpdatedFireAndForgetAsync(newUserAddress: string) {
|
||||
if (this.userAddress !== newUserAddress) {
|
||||
this.userAddress = newUserAddress;
|
||||
await this.fetchTokenInformationAsync();
|
||||
await this.rehydrateStoreWithContractEvents();
|
||||
}
|
||||
}
|
||||
public async nodeVersionUpdatedFireAndForgetAsync(nodeVersion: string) {
|
||||
if (this.nodeVersion !== nodeVersion) {
|
||||
this.nodeVersion = nodeVersion;
|
||||
}
|
||||
}
|
||||
public async isAddressInTokenRegistryAsync(tokenAddress: string): Promise<boolean> {
|
||||
utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.');
|
||||
const tokenIfExists = await this.zeroEx.tokenRegistry.getTokenIfExistsAsync(tokenAddress);
|
||||
return !_.isUndefined(tokenIfExists);
|
||||
}
|
||||
public getLedgerDerivationPathIfExists(): string {
|
||||
if (_.isUndefined(this.ledgerSubProvider)) {
|
||||
return undefined;
|
||||
}
|
||||
const path = this.ledgerSubProvider.getPath();
|
||||
return path;
|
||||
}
|
||||
public updateLedgerDerivationPathIfExists(path: string) {
|
||||
if (_.isUndefined(this.ledgerSubProvider)) {
|
||||
return; // noop
|
||||
}
|
||||
this.ledgerSubProvider.setPath(path);
|
||||
}
|
||||
public updateLedgerDerivationIndex(pathIndex: number) {
|
||||
if (_.isUndefined(this.ledgerSubProvider)) {
|
||||
return; // noop
|
||||
}
|
||||
this.ledgerSubProvider.setPathIndex(pathIndex);
|
||||
}
|
||||
public async providerTypeUpdatedFireAndForgetAsync(providerType: ProviderType) {
|
||||
utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.');
|
||||
// Should actually be Web3.Provider|ProviderEngine union type but it causes issues
|
||||
// later on in the logic.
|
||||
let provider;
|
||||
switch (providerType) {
|
||||
case ProviderType.LEDGER: {
|
||||
const isU2FSupported = await utils.isU2FSupportedAsync();
|
||||
if (!isU2FSupported) {
|
||||
throw new Error('Cannot update providerType to LEDGER without U2F support');
|
||||
}
|
||||
|
||||
// Cache injected provider so that we can switch the user back to it easily
|
||||
this.cachedProvider = this.web3Wrapper.getProviderObj();
|
||||
|
||||
this.dispatcher.updateUserAddress(''); // Clear old userAddress
|
||||
|
||||
provider = new ProviderEngine();
|
||||
this.ledgerSubProvider = ledgerWalletSubproviderFactory(this.getBlockchainNetworkId.bind(this));
|
||||
provider.addProvider(this.ledgerSubProvider);
|
||||
provider.addProvider(new FilterSubprovider());
|
||||
const networkId = configs.isMainnetEnabled ?
|
||||
constants.MAINNET_NETWORK_ID :
|
||||
constants.TESTNET_NETWORK_ID;
|
||||
provider.addProvider(new RedundantRPCSubprovider(
|
||||
constants.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId],
|
||||
));
|
||||
provider.start();
|
||||
this.web3Wrapper.destroy();
|
||||
const shouldPollUserAddress = false;
|
||||
this.web3Wrapper = new Web3Wrapper(this.dispatcher, provider, this.networkId, shouldPollUserAddress);
|
||||
await this.zeroEx.setProviderAsync(provider);
|
||||
await this.postInstantiationOrUpdatingProviderZeroExAsync();
|
||||
break;
|
||||
}
|
||||
|
||||
case ProviderType.INJECTED: {
|
||||
if (_.isUndefined(this.cachedProvider)) {
|
||||
return; // Going from injected to injected, so we noop
|
||||
}
|
||||
provider = this.cachedProvider;
|
||||
const shouldPollUserAddress = true;
|
||||
this.web3Wrapper = new Web3Wrapper(this.dispatcher, provider, this.networkId, shouldPollUserAddress);
|
||||
await this.zeroEx.setProviderAsync(provider);
|
||||
await this.postInstantiationOrUpdatingProviderZeroExAsync();
|
||||
delete this.ledgerSubProvider;
|
||||
delete this.cachedProvider;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw utils.spawnSwitchErr('providerType', providerType);
|
||||
}
|
||||
|
||||
await this.fetchTokenInformationAsync();
|
||||
}
|
||||
public async setProxyAllowanceAsync(token: Token, amountInBaseUnits: BigNumber): Promise<void> {
|
||||
utils.assert(this.isValidAddress(token.address), BlockchainCallErrs.TOKEN_ADDRESS_IS_INVALID);
|
||||
utils.assert(this.doesUserAddressExist(), BlockchainCallErrs.USER_HAS_NO_ASSOCIATED_ADDRESSES);
|
||||
utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.');
|
||||
|
||||
const txHash = await this.zeroEx.token.setProxyAllowanceAsync(
|
||||
token.address, this.userAddress, amountInBaseUnits,
|
||||
);
|
||||
await this.showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
|
||||
const allowance = amountInBaseUnits;
|
||||
this.dispatcher.replaceTokenAllowanceByAddress(token.address, allowance);
|
||||
}
|
||||
public async transferAsync(token: Token, toAddress: string,
|
||||
amountInBaseUnits: BigNumber): Promise<void> {
|
||||
const txHash = await this.zeroEx.token.transferAsync(
|
||||
token.address, this.userAddress, toAddress, amountInBaseUnits,
|
||||
);
|
||||
await this.showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
|
||||
const etherScanLinkIfExists = utils.getEtherScanLinkIfExists(txHash, this.networkId, EtherscanLinkSuffixes.tx);
|
||||
this.dispatcher.showFlashMessage(React.createElement(TokenSendCompleted, {
|
||||
etherScanLinkIfExists,
|
||||
token,
|
||||
toAddress,
|
||||
amountInBaseUnits,
|
||||
}));
|
||||
}
|
||||
public portalOrderToSignedOrder(maker: string, taker: string, makerTokenAddress: string,
|
||||
takerTokenAddress: string, makerTokenAmount: BigNumber,
|
||||
takerTokenAmount: BigNumber, makerFee: BigNumber,
|
||||
takerFee: BigNumber, expirationUnixTimestampSec: BigNumber,
|
||||
feeRecipient: string,
|
||||
signatureData: SignatureData, salt: BigNumber): SignedOrder {
|
||||
const ecSignature = signatureData;
|
||||
const exchangeContractAddress = this.getExchangeContractAddressIfExists();
|
||||
taker = _.isEmpty(taker) ? constants.NULL_ADDRESS : taker;
|
||||
const signedOrder = {
|
||||
ecSignature,
|
||||
exchangeContractAddress,
|
||||
expirationUnixTimestampSec,
|
||||
feeRecipient,
|
||||
maker,
|
||||
makerFee,
|
||||
makerTokenAddress,
|
||||
makerTokenAmount,
|
||||
salt,
|
||||
taker,
|
||||
takerFee,
|
||||
takerTokenAddress,
|
||||
takerTokenAmount,
|
||||
};
|
||||
return signedOrder;
|
||||
}
|
||||
public async fillOrderAsync(signedOrder: SignedOrder,
|
||||
fillTakerTokenAmount: BigNumber): Promise<BigNumber> {
|
||||
utils.assert(this.doesUserAddressExist(), BlockchainCallErrs.USER_HAS_NO_ASSOCIATED_ADDRESSES);
|
||||
|
||||
const shouldThrowOnInsufficientBalanceOrAllowance = true;
|
||||
|
||||
const txHash = await this.zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder, fillTakerTokenAmount, shouldThrowOnInsufficientBalanceOrAllowance, this.userAddress,
|
||||
);
|
||||
const receipt = await this.showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
|
||||
const logs: Array<LogWithDecodedArgs<ExchangeContractEventArgs>> = receipt.logs as any;
|
||||
this.zeroEx.exchange.throwLogErrorsAsErrors(logs);
|
||||
const logFill = _.find(logs, {event: 'LogFill'});
|
||||
const args = logFill.args as any as LogFillContractEventArgs;
|
||||
const filledTakerTokenAmount = args.filledTakerTokenAmount;
|
||||
return filledTakerTokenAmount;
|
||||
}
|
||||
public async cancelOrderAsync(signedOrder: SignedOrder,
|
||||
cancelTakerTokenAmount: BigNumber): Promise<BigNumber> {
|
||||
const txHash = await this.zeroEx.exchange.cancelOrderAsync(
|
||||
signedOrder, cancelTakerTokenAmount,
|
||||
);
|
||||
const receipt = await this.showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
|
||||
const logs: Array<LogWithDecodedArgs<ExchangeContractEventArgs>> = receipt.logs as any;
|
||||
this.zeroEx.exchange.throwLogErrorsAsErrors(logs);
|
||||
const logCancel = _.find(logs, {event: ExchangeEvents.LogCancel});
|
||||
const args = logCancel.args as any as LogCancelContractEventArgs;
|
||||
const cancelledTakerTokenAmount = args.cancelledTakerTokenAmount;
|
||||
return cancelledTakerTokenAmount;
|
||||
}
|
||||
public async getUnavailableTakerAmountAsync(orderHash: string): Promise<BigNumber> {
|
||||
utils.assert(ZeroEx.isValidOrderHash(orderHash), 'Must be valid orderHash');
|
||||
utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.');
|
||||
const unavailableTakerAmount = await this.zeroEx.exchange.getUnavailableTakerAmountAsync(orderHash);
|
||||
return unavailableTakerAmount;
|
||||
}
|
||||
public getExchangeContractAddressIfExists() {
|
||||
return this.exchangeAddress;
|
||||
}
|
||||
public toHumanReadableErrorMsg(error: ZeroExError|ExchangeContractErrs, takerAddress: string): string {
|
||||
const ZeroExErrorToHumanReadableError: {[error: string]: string} = {
|
||||
[ZeroExError.ContractDoesNotExist]: 'Contract does not exist',
|
||||
[ZeroExError.ExchangeContractDoesNotExist]: 'Exchange contract does not exist',
|
||||
[ZeroExError.UnhandledError]: ' Unhandled error occured',
|
||||
[ZeroExError.UserHasNoAssociatedAddress]: 'User has no addresses available',
|
||||
[ZeroExError.InvalidSignature]: 'Order signature is not valid',
|
||||
[ZeroExError.ContractNotDeployedOnNetwork]: 'Contract is not deployed on the detected network',
|
||||
[ZeroExError.InvalidJump]: 'Invalid jump occured while executing the transaction',
|
||||
[ZeroExError.OutOfGas]: 'Transaction ran out of gas',
|
||||
[ZeroExError.NoNetworkId]: 'No network id detected',
|
||||
};
|
||||
const exchangeContractErrorToHumanReadableError: {[error: string]: string} = {
|
||||
[ExchangeContractErrs.OrderFillExpired]: 'This order has expired',
|
||||
[ExchangeContractErrs.OrderCancelExpired]: 'This order has expired',
|
||||
[ExchangeContractErrs.OrderCancelAmountZero]: 'Order cancel amount can\'t be 0',
|
||||
[ExchangeContractErrs.OrderAlreadyCancelledOrFilled]:
|
||||
'This order has already been completely filled or cancelled',
|
||||
[ExchangeContractErrs.OrderFillAmountZero]: 'Order fill amount can\'t be 0',
|
||||
[ExchangeContractErrs.OrderRemainingFillAmountZero]:
|
||||
'This order has already been completely filled or cancelled',
|
||||
[ExchangeContractErrs.OrderFillRoundingError]: 'Rounding error will occur when filling this order',
|
||||
[ExchangeContractErrs.InsufficientTakerBalance]:
|
||||
'Taker no longer has a sufficient balance to complete this order',
|
||||
[ExchangeContractErrs.InsufficientTakerAllowance]:
|
||||
'Taker no longer has a sufficient allowance to complete this order',
|
||||
[ExchangeContractErrs.InsufficientMakerBalance]:
|
||||
'Maker no longer has a sufficient balance to complete this order',
|
||||
[ExchangeContractErrs.InsufficientMakerAllowance]:
|
||||
'Maker no longer has a sufficient allowance to complete this order',
|
||||
[ExchangeContractErrs.InsufficientTakerFeeBalance]: 'Taker no longer has a sufficient balance to pay fees',
|
||||
[ExchangeContractErrs.InsufficientTakerFeeAllowance]:
|
||||
'Taker no longer has a sufficient allowance to pay fees',
|
||||
[ExchangeContractErrs.InsufficientMakerFeeBalance]: 'Maker no longer has a sufficient balance to pay fees',
|
||||
[ExchangeContractErrs.InsufficientMakerFeeAllowance]:
|
||||
'Maker no longer has a sufficient allowance to pay fees',
|
||||
[ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker]:
|
||||
`This order can only be filled by ${takerAddress}`,
|
||||
[ExchangeContractErrs.InsufficientRemainingFillAmount]:
|
||||
'Insufficient remaining fill amount',
|
||||
};
|
||||
const humanReadableErrorMsg = exchangeContractErrorToHumanReadableError[error] ||
|
||||
ZeroExErrorToHumanReadableError[error];
|
||||
return humanReadableErrorMsg;
|
||||
}
|
||||
public async validateFillOrderThrowIfInvalidAsync(signedOrder: SignedOrder,
|
||||
fillTakerTokenAmount: BigNumber,
|
||||
takerAddress: string): Promise<void> {
|
||||
await this.zeroEx.exchange.validateFillOrderThrowIfInvalidAsync(
|
||||
signedOrder, fillTakerTokenAmount, takerAddress);
|
||||
}
|
||||
public async validateCancelOrderThrowIfInvalidAsync(order: Order,
|
||||
cancelTakerTokenAmount: BigNumber): Promise<void> {
|
||||
await this.zeroEx.exchange.validateCancelOrderThrowIfInvalidAsync(order, cancelTakerTokenAmount);
|
||||
}
|
||||
public isValidAddress(address: string): boolean {
|
||||
const lowercaseAddress = address.toLowerCase();
|
||||
return this.web3Wrapper.isAddress(lowercaseAddress);
|
||||
}
|
||||
public async pollTokenBalanceAsync(token: Token) {
|
||||
utils.assert(this.doesUserAddressExist(), BlockchainCallErrs.USER_HAS_NO_ASSOCIATED_ADDRESSES);
|
||||
|
||||
const [currBalance] = await this.getTokenBalanceAndAllowanceAsync(this.userAddress, token.address);
|
||||
|
||||
this.zrxPollIntervalId = window.setInterval(async () => {
|
||||
const [balance] = await this.getTokenBalanceAndAllowanceAsync(this.userAddress, token.address);
|
||||
if (!balance.eq(currBalance)) {
|
||||
this.dispatcher.replaceTokenBalanceByAddress(token.address, balance);
|
||||
clearInterval(this.zrxPollIntervalId);
|
||||
delete this.zrxPollIntervalId;
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
public async signOrderHashAsync(orderHash: string): Promise<SignatureData> {
|
||||
utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.');
|
||||
const makerAddress = this.userAddress;
|
||||
// If makerAddress is undefined, this means they have a web3 instance injected into their browser
|
||||
// but no account addresses associated with it.
|
||||
if (_.isUndefined(makerAddress)) {
|
||||
throw new Error('Tried to send a sign request but user has no associated addresses');
|
||||
}
|
||||
const ecSignature = await this.zeroEx.signOrderHashAsync(orderHash, makerAddress);
|
||||
const signatureData = _.extend({}, ecSignature, {
|
||||
hash: orderHash,
|
||||
});
|
||||
this.dispatcher.updateSignatureData(signatureData);
|
||||
return signatureData;
|
||||
}
|
||||
public async mintTestTokensAsync(token: Token) {
|
||||
utils.assert(this.doesUserAddressExist(), BlockchainCallErrs.USER_HAS_NO_ASSOCIATED_ADDRESSES);
|
||||
|
||||
const mintableContract = await this.instantiateContractIfExistsAsync(MintableArtifacts, token.address);
|
||||
await mintableContract.mint(constants.MINT_AMOUNT, {
|
||||
from: this.userAddress,
|
||||
});
|
||||
const balanceDelta = constants.MINT_AMOUNT;
|
||||
this.dispatcher.updateTokenBalanceByAddress(token.address, balanceDelta);
|
||||
}
|
||||
public async getBalanceInEthAsync(owner: string): Promise<BigNumber> {
|
||||
const balance = await this.web3Wrapper.getBalanceInEthAsync(owner);
|
||||
return balance;
|
||||
}
|
||||
public async convertEthToWrappedEthTokensAsync(amount: BigNumber): Promise<void> {
|
||||
utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.');
|
||||
utils.assert(this.doesUserAddressExist(), BlockchainCallErrs.USER_HAS_NO_ASSOCIATED_ADDRESSES);
|
||||
|
||||
const txHash = await this.zeroEx.etherToken.depositAsync(amount, this.userAddress);
|
||||
await this.showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
|
||||
}
|
||||
public async convertWrappedEthTokensToEthAsync(amount: BigNumber): Promise<void> {
|
||||
utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.');
|
||||
utils.assert(this.doesUserAddressExist(), BlockchainCallErrs.USER_HAS_NO_ASSOCIATED_ADDRESSES);
|
||||
|
||||
const txHash = await this.zeroEx.etherToken.withdrawAsync(amount, this.userAddress);
|
||||
await this.showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
|
||||
}
|
||||
public async doesContractExistAtAddressAsync(address: string) {
|
||||
const doesContractExist = await this.web3Wrapper.doesContractExistAtAddressAsync(address);
|
||||
return doesContractExist;
|
||||
}
|
||||
public async getCurrentUserTokenBalanceAndAllowanceAsync(tokenAddress: string): Promise<BigNumber[]> {
|
||||
const tokenBalanceAndAllowance = await this.getTokenBalanceAndAllowanceAsync(this.userAddress, tokenAddress);
|
||||
return tokenBalanceAndAllowance;
|
||||
}
|
||||
public async getTokenBalanceAndAllowanceAsync(ownerAddress: string, tokenAddress: string):
|
||||
Promise<BigNumber[]> {
|
||||
utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.');
|
||||
|
||||
if (_.isEmpty(ownerAddress)) {
|
||||
const zero = new BigNumber(0);
|
||||
return [zero, zero];
|
||||
}
|
||||
let balance = new BigNumber(0);
|
||||
let allowance = new BigNumber(0);
|
||||
if (this.doesUserAddressExist()) {
|
||||
balance = await this.zeroEx.token.getBalanceAsync(tokenAddress, ownerAddress);
|
||||
allowance = await this.zeroEx.token.getProxyAllowanceAsync(tokenAddress, ownerAddress);
|
||||
}
|
||||
return [balance, allowance];
|
||||
}
|
||||
public async updateTokenBalancesAndAllowancesAsync(tokens: Token[]) {
|
||||
const tokenStateByAddress: TokenStateByAddress = {};
|
||||
for (const token of tokens) {
|
||||
let balance = new BigNumber(0);
|
||||
let allowance = new BigNumber(0);
|
||||
if (this.doesUserAddressExist()) {
|
||||
[
|
||||
balance,
|
||||
allowance,
|
||||
] = await this.getTokenBalanceAndAllowanceAsync(this.userAddress, token.address);
|
||||
}
|
||||
const tokenState = {
|
||||
balance,
|
||||
allowance,
|
||||
};
|
||||
tokenStateByAddress[token.address] = tokenState;
|
||||
}
|
||||
this.dispatcher.updateTokenStateByAddress(tokenStateByAddress);
|
||||
}
|
||||
public async getUserAccountsAsync() {
|
||||
utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.');
|
||||
const userAccountsIfExists = await this.zeroEx.getAvailableAddressesAsync();
|
||||
return userAccountsIfExists;
|
||||
}
|
||||
// HACK: When a user is using a Ledger, we simply dispatch the selected userAddress, which
|
||||
// by-passes the web3Wrapper logic for updating the prevUserAddress. We therefore need to
|
||||
// manually update it. This should only be called by the LedgerConfigDialog.
|
||||
public updateWeb3WrapperPrevUserAddress(newUserAddress: string) {
|
||||
this.web3Wrapper.updatePrevUserAddress(newUserAddress);
|
||||
}
|
||||
public destroy() {
|
||||
clearInterval(this.zrxPollIntervalId);
|
||||
this.web3Wrapper.destroy();
|
||||
this.stopWatchingExchangeLogFillEventsAsync(); // fire and forget
|
||||
}
|
||||
private async showEtherScanLinkAndAwaitTransactionMinedAsync(
|
||||
txHash: string): Promise<TransactionReceiptWithDecodedLogs> {
|
||||
const etherScanLinkIfExists = utils.getEtherScanLinkIfExists(txHash, this.networkId, EtherscanLinkSuffixes.tx);
|
||||
this.dispatcher.showFlashMessage(React.createElement(TransactionSubmitted, {
|
||||
etherScanLinkIfExists,
|
||||
}));
|
||||
const receipt = await this.zeroEx.awaitTransactionMinedAsync(txHash);
|
||||
return receipt;
|
||||
}
|
||||
private doesUserAddressExist(): boolean {
|
||||
return this.userAddress !== '';
|
||||
}
|
||||
private async rehydrateStoreWithContractEvents() {
|
||||
// Ensure we are only ever listening to one set of events
|
||||
await this.stopWatchingExchangeLogFillEventsAsync();
|
||||
|
||||
if (!this.doesUserAddressExist()) {
|
||||
return; // short-circuit
|
||||
}
|
||||
|
||||
if (!_.isUndefined(this.zeroEx)) {
|
||||
// Since we do not have an index on the `taker` address and want to show
|
||||
// transactions where an account is either the `maker` or `taker`, we loop
|
||||
// through all fill events, and filter/cache them client-side.
|
||||
const filterIndexObj = {};
|
||||
await this.startListeningForExchangeLogFillEventsAsync(filterIndexObj);
|
||||
}
|
||||
}
|
||||
private async startListeningForExchangeLogFillEventsAsync(indexFilterValues: IndexedFilterValues): Promise<void> {
|
||||
utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.');
|
||||
utils.assert(this.doesUserAddressExist(), BlockchainCallErrs.USER_HAS_NO_ASSOCIATED_ADDRESSES);
|
||||
|
||||
// Fetch historical logs
|
||||
await this.fetchHistoricalExchangeLogFillEventsAsync(indexFilterValues);
|
||||
|
||||
// Start a subscription for new logs
|
||||
const exchangeAddress = this.getExchangeContractAddressIfExists();
|
||||
const subscriptionId = await this.zeroEx.exchange.subscribeAsync(
|
||||
ExchangeEvents.LogFill, indexFilterValues,
|
||||
async (err: Error, decodedLogEvent: DecodedLogEvent<LogFillContractEventArgs>) => {
|
||||
if (err) {
|
||||
// Note: it's not entirely clear from the documentation which
|
||||
// errors will be thrown by `watch`. For now, let's log the error
|
||||
// to rollbar and stop watching when one occurs
|
||||
errorReporter.reportAsync(err); // fire and forget
|
||||
this.stopWatchingExchangeLogFillEventsAsync(); // fire and forget
|
||||
return;
|
||||
} else {
|
||||
await this.addFillEventToTradeHistoryAsync(decodedLogEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
private async fetchHistoricalExchangeLogFillEventsAsync(indexFilterValues: IndexedFilterValues) {
|
||||
const fromBlock = tradeHistoryStorage.getFillsLatestBlock(this.userAddress, this.networkId);
|
||||
const subscriptionOpts: SubscriptionOpts = {
|
||||
fromBlock,
|
||||
toBlock: 'latest' as BlockParam,
|
||||
};
|
||||
const decodedLogs = await this.zeroEx.exchange.getLogsAsync<LogFillContractEventArgs>(
|
||||
ExchangeEvents.LogFill, subscriptionOpts, indexFilterValues,
|
||||
);
|
||||
for (const decodedLog of decodedLogs) {
|
||||
console.log('decodedLog', decodedLog);
|
||||
await this.addFillEventToTradeHistoryAsync(decodedLog);
|
||||
}
|
||||
}
|
||||
private async addFillEventToTradeHistoryAsync(decodedLog: LogWithDecodedArgs<LogFillContractEventArgs>) {
|
||||
const args = decodedLog.args as LogFillContractEventArgs;
|
||||
const isUserMakerOrTaker = args.maker === this.userAddress ||
|
||||
args.taker === this.userAddress;
|
||||
if (!isUserMakerOrTaker) {
|
||||
return; // We aren't interested in the fill event
|
||||
}
|
||||
const isBlockPending = _.isNull(decodedLog.blockNumber);
|
||||
if (!isBlockPending) {
|
||||
// Hack: I've observed the behavior where a client won't register certain fill events
|
||||
// and lowering the cache blockNumber fixes the issue. As a quick fix for now, simply
|
||||
// set the cached blockNumber 50 below the one returned. This way, upon refreshing, a user
|
||||
// would still attempt to re-fetch events from the previous 50 blocks, but won't need to
|
||||
// re-fetch all events in all blocks.
|
||||
// TODO: Debug if this is a race condition, and apply a more precise fix
|
||||
const blockNumberToSet = decodedLog.blockNumber - BLOCK_NUMBER_BACK_TRACK < 0 ?
|
||||
0 :
|
||||
decodedLog.blockNumber - BLOCK_NUMBER_BACK_TRACK;
|
||||
tradeHistoryStorage.setFillsLatestBlock(this.userAddress, this.networkId, blockNumberToSet);
|
||||
}
|
||||
const blockTimestamp = await this.web3Wrapper.getBlockTimestampAsync(decodedLog.blockHash);
|
||||
const fill = {
|
||||
filledTakerTokenAmount: args.filledTakerTokenAmount,
|
||||
filledMakerTokenAmount: args.filledMakerTokenAmount,
|
||||
logIndex: decodedLog.logIndex,
|
||||
maker: args.maker,
|
||||
orderHash: args.orderHash,
|
||||
taker: args.taker,
|
||||
makerToken: args.makerToken,
|
||||
takerToken: args.takerToken,
|
||||
paidMakerFee: args.paidMakerFee,
|
||||
paidTakerFee: args.paidTakerFee,
|
||||
transactionHash: decodedLog.transactionHash,
|
||||
blockTimestamp,
|
||||
};
|
||||
tradeHistoryStorage.addFillToUser(this.userAddress, this.networkId, fill);
|
||||
}
|
||||
private async stopWatchingExchangeLogFillEventsAsync() {
|
||||
this.zeroEx.exchange.unsubscribeAll();
|
||||
}
|
||||
private async getTokenRegistryTokensByAddressAsync(): Promise<TokenByAddress> {
|
||||
utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.');
|
||||
const tokenRegistryTokens = await this.zeroEx.tokenRegistry.getTokensAsync();
|
||||
|
||||
const tokenByAddress: TokenByAddress = {};
|
||||
_.each(tokenRegistryTokens, (t: ZeroExToken, i: number) => {
|
||||
// HACK: For now we have a hard-coded list of iconUrls for the dummyTokens
|
||||
// TODO: Refactor this out and pull the iconUrl directly from the TokenRegistry
|
||||
const iconUrl = constants.iconUrlBySymbol[t.symbol];
|
||||
const token: Token = {
|
||||
iconUrl,
|
||||
address: t.address,
|
||||
name: t.name,
|
||||
symbol: t.symbol,
|
||||
decimals: t.decimals,
|
||||
isTracked: false,
|
||||
isRegistered: true,
|
||||
};
|
||||
tokenByAddress[token.address] = token;
|
||||
});
|
||||
return tokenByAddress;
|
||||
}
|
||||
private async onPageLoadInitFireAndForgetAsync() {
|
||||
await this.onPageLoadAsync(); // wait for page to load
|
||||
|
||||
// Hack: We need to know the networkId the injectedWeb3 is connected to (if it is defined) in
|
||||
// order to properly instantiate the web3Wrapper. Since we must use the async call, we cannot
|
||||
// retrieve it from within the web3Wrapper constructor. This is and should remain the only
|
||||
// call to a web3 instance outside of web3Wrapper in the entire dapp.
|
||||
// In addition, if the user has an injectedWeb3 instance that is disconnected from a backing
|
||||
// Ethereum node, this call will throw. We need to handle this case gracefully
|
||||
const injectedWeb3 = (window as any).web3;
|
||||
let networkId: number;
|
||||
if (!_.isUndefined(injectedWeb3)) {
|
||||
try {
|
||||
networkId = _.parseInt(await promisify(injectedWeb3.version.getNetwork)());
|
||||
} catch (err) {
|
||||
// Ignore error and proceed with networkId undefined
|
||||
}
|
||||
}
|
||||
|
||||
const provider = await this.getProviderAsync(injectedWeb3, networkId);
|
||||
this.zeroEx = new ZeroEx(provider);
|
||||
await this.updateProviderName(injectedWeb3);
|
||||
const shouldPollUserAddress = true;
|
||||
this.web3Wrapper = new Web3Wrapper(this.dispatcher, provider, networkId, shouldPollUserAddress);
|
||||
await this.postInstantiationOrUpdatingProviderZeroExAsync();
|
||||
}
|
||||
// This method should always be run after instantiating or updating the provider
|
||||
// of the ZeroEx instance.
|
||||
private async postInstantiationOrUpdatingProviderZeroExAsync() {
|
||||
utils.assert(!_.isUndefined(this.zeroEx), 'ZeroEx must be instantiated.');
|
||||
this.exchangeAddress = await this.zeroEx.exchange.getContractAddressAsync();
|
||||
}
|
||||
private updateProviderName(injectedWeb3: Web3) {
|
||||
const doesInjectedWeb3Exist = !_.isUndefined(injectedWeb3);
|
||||
const providerName = doesInjectedWeb3Exist ?
|
||||
this.getNameGivenProvider(injectedWeb3.currentProvider) :
|
||||
constants.PUBLIC_PROVIDER_NAME;
|
||||
this.dispatcher.updateInjectedProviderName(providerName);
|
||||
}
|
||||
// This is only ever called by the LedgerWallet subprovider in order to retrieve
|
||||
// the current networkId without this value going stale.
|
||||
private getBlockchainNetworkId() {
|
||||
return this.networkId;
|
||||
}
|
||||
private async getProviderAsync(injectedWeb3: Web3, networkIdIfExists: number) {
|
||||
const doesInjectedWeb3Exist = !_.isUndefined(injectedWeb3);
|
||||
const publicNodeUrlsIfExistsForNetworkId = constants.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkIdIfExists];
|
||||
const isPublicNodeAvailableForNetworkId = !_.isUndefined(publicNodeUrlsIfExistsForNetworkId);
|
||||
|
||||
let provider;
|
||||
if (doesInjectedWeb3Exist && isPublicNodeAvailableForNetworkId) {
|
||||
// We catch all requests involving a users account and send it to the injectedWeb3
|
||||
// instance. All other requests go to the public hosted node.
|
||||
provider = new ProviderEngine();
|
||||
provider.addProvider(new InjectedWeb3SubProvider(injectedWeb3));
|
||||
provider.addProvider(new FilterSubprovider());
|
||||
provider.addProvider(new RedundantRPCSubprovider(
|
||||
publicNodeUrlsIfExistsForNetworkId,
|
||||
));
|
||||
provider.start();
|
||||
} else if (doesInjectedWeb3Exist) {
|
||||
// Since no public node for this network, all requests go to injectedWeb3 instance
|
||||
provider = injectedWeb3.currentProvider;
|
||||
} else {
|
||||
// If no injectedWeb3 instance, all requests fallback to our public hosted mainnet/testnet node
|
||||
// We do this so that users can still browse the 0x Portal DApp even if they do not have web3
|
||||
// injected into their browser.
|
||||
provider = new ProviderEngine();
|
||||
provider.addProvider(new FilterSubprovider());
|
||||
const networkId = configs.isMainnetEnabled ?
|
||||
constants.MAINNET_NETWORK_ID :
|
||||
constants.TESTNET_NETWORK_ID;
|
||||
provider.addProvider(new RedundantRPCSubprovider(
|
||||
constants.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId],
|
||||
));
|
||||
provider.start();
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
private getNameGivenProvider(provider: Web3.Provider): string {
|
||||
if (!_.isUndefined((provider as any).isMetaMask)) {
|
||||
return constants.METAMASK_PROVIDER_NAME;
|
||||
}
|
||||
|
||||
// HACK: We use the fact that Parity Signer's provider is an instance of their
|
||||
// internal `Web3FrameProvider` class.
|
||||
const isParitySigner = _.startsWith(provider.constructor.toString(), 'function Web3FrameProvider');
|
||||
if (isParitySigner) {
|
||||
return constants.PARITY_SIGNER_PROVIDER_NAME;
|
||||
}
|
||||
|
||||
return constants.GENERIC_PROVIDER_NAME;
|
||||
}
|
||||
private async fetchTokenInformationAsync() {
|
||||
utils.assert(!_.isUndefined(this.networkId),
|
||||
'Cannot call fetchTokenInformationAsync if disconnected from Ethereum node');
|
||||
|
||||
this.dispatcher.updateBlockchainIsLoaded(false);
|
||||
this.dispatcher.clearTokenByAddress();
|
||||
|
||||
const tokenRegistryTokensByAddress = await this.getTokenRegistryTokensByAddressAsync();
|
||||
|
||||
// HACK: We need to fetch the userAddress here because otherwise we cannot save the
|
||||
// tracked tokens in localStorage under the users address nor fetch the token
|
||||
// balances and allowances and we need to do this in order not to trigger the blockchain
|
||||
// loading dialog to show up twice. First to load the contracts, and second to load the
|
||||
// balances and allowances.
|
||||
this.userAddress = await this.web3Wrapper.getFirstAccountIfExistsAsync();
|
||||
if (!_.isEmpty(this.userAddress)) {
|
||||
this.dispatcher.updateUserAddress(this.userAddress);
|
||||
}
|
||||
|
||||
let trackedTokensIfExists = trackedTokenStorage.getTrackedTokensIfExists(this.userAddress, this.networkId);
|
||||
const tokenRegistryTokens = _.values(tokenRegistryTokensByAddress);
|
||||
if (_.isUndefined(trackedTokensIfExists)) {
|
||||
trackedTokensIfExists = _.map(configs.defaultTrackedTokenSymbols, symbol => {
|
||||
const token = _.find(tokenRegistryTokens, t => t.symbol === symbol);
|
||||
token.isTracked = true;
|
||||
return token;
|
||||
});
|
||||
_.each(trackedTokensIfExists, token => {
|
||||
trackedTokenStorage.addTrackedTokenToUser(this.userAddress, this.networkId, token);
|
||||
});
|
||||
} else {
|
||||
// Properly set all tokenRegistry tokens `isTracked` to true if they are in the existing trackedTokens array
|
||||
_.each(trackedTokensIfExists, trackedToken => {
|
||||
if (!_.isUndefined(tokenRegistryTokensByAddress[trackedToken.address])) {
|
||||
tokenRegistryTokensByAddress[trackedToken.address].isTracked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
const allTokens = _.uniq([...tokenRegistryTokens, ...trackedTokensIfExists]);
|
||||
this.dispatcher.updateTokenByAddress(allTokens);
|
||||
|
||||
// Get balance/allowance for tracked tokens
|
||||
await this.updateTokenBalancesAndAllowancesAsync(trackedTokensIfExists);
|
||||
|
||||
const mostPopularTradingPairTokens: Token[] = [
|
||||
_.find(allTokens, {symbol: configs.defaultTrackedTokenSymbols[0]}),
|
||||
_.find(allTokens, {symbol: configs.defaultTrackedTokenSymbols[1]}),
|
||||
];
|
||||
this.dispatcher.updateChosenAssetTokenAddress(Side.deposit, mostPopularTradingPairTokens[0].address);
|
||||
this.dispatcher.updateChosenAssetTokenAddress(Side.receive, mostPopularTradingPairTokens[1].address);
|
||||
this.dispatcher.updateBlockchainIsLoaded(true);
|
||||
}
|
||||
private async instantiateContractIfExistsAsync(artifact: any, address?: string): Promise<ContractInstance> {
|
||||
const c = await contract(artifact);
|
||||
const providerObj = this.web3Wrapper.getProviderObj();
|
||||
c.setProvider(providerObj);
|
||||
|
||||
const artifactNetworkConfigs = artifact.networks[this.networkId];
|
||||
let contractAddress;
|
||||
if (!_.isUndefined(address)) {
|
||||
contractAddress = address;
|
||||
} else if (!_.isUndefined(artifactNetworkConfigs)) {
|
||||
contractAddress = artifactNetworkConfigs.address;
|
||||
}
|
||||
|
||||
if (!_.isUndefined(contractAddress)) {
|
||||
const doesContractExist = await this.doesContractExistAtAddressAsync(contractAddress);
|
||||
if (!doesContractExist) {
|
||||
utils.consoleLog(`Contract does not exist: ${artifact.contract_name} at ${contractAddress}`);
|
||||
throw new Error(BlockchainCallErrs.CONTRACT_DOES_NOT_EXIST);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const contractInstance = _.isUndefined(address) ?
|
||||
await c.deployed() :
|
||||
await c.at(address);
|
||||
return contractInstance;
|
||||
} catch (err) {
|
||||
const errMsg = `${err}`;
|
||||
utils.consoleLog(`Notice: Error encountered: ${err} ${err.stack}`);
|
||||
if (_.includes(errMsg, 'not been deployed to detected network')) {
|
||||
throw new Error(BlockchainCallErrs.CONTRACT_DOES_NOT_EXIST);
|
||||
} else {
|
||||
await errorReporter.reportAsync(err);
|
||||
throw new Error(BlockchainCallErrs.UNHANDLED_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
private async onPageLoadAsync() {
|
||||
if (document.readyState === 'complete') {
|
||||
return; // Already loaded
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
window.onload = resolve;
|
||||
});
|
||||
}
|
||||
}
|
||||
158
packages/website/ts/components/dialogs/blockchain_err_dialog.tsx
Normal file
158
packages/website/ts/components/dialogs/blockchain_err_dialog.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import Dialog from 'material-ui/Dialog';
|
||||
import FlatButton from 'material-ui/FlatButton';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {configs} from 'ts/utils/configs';
|
||||
import {Blockchain} from 'ts/blockchain';
|
||||
import {BlockchainErrs} from 'ts/types';
|
||||
|
||||
interface BlockchainErrDialogProps {
|
||||
blockchain: Blockchain;
|
||||
blockchainErr: BlockchainErrs;
|
||||
isOpen: boolean;
|
||||
userAddress: string;
|
||||
toggleDialogFn: (isOpen: boolean) => void;
|
||||
networkId: number;
|
||||
}
|
||||
|
||||
export class BlockchainErrDialog extends React.Component<BlockchainErrDialogProps, undefined> {
|
||||
public render() {
|
||||
const dialogActions = [
|
||||
<FlatButton
|
||||
label="Ok"
|
||||
primary={true}
|
||||
onTouchTap={this.props.toggleDialogFn.bind(this.props.toggleDialogFn, false)}
|
||||
/>,
|
||||
];
|
||||
|
||||
const hasWalletAddress = this.props.userAddress !== '';
|
||||
return (
|
||||
<Dialog
|
||||
title={this.getTitle(hasWalletAddress)}
|
||||
titleStyle={{fontWeight: 100}}
|
||||
actions={dialogActions}
|
||||
open={this.props.isOpen}
|
||||
contentStyle={{width: 400}}
|
||||
onRequestClose={this.props.toggleDialogFn.bind(this.props.toggleDialogFn, false)}
|
||||
autoScrollBodyContent={true}
|
||||
>
|
||||
<div className="pt2" style={{color: colors.grey700}}>
|
||||
{this.renderExplanation(hasWalletAddress)}
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
private getTitle(hasWalletAddress: boolean) {
|
||||
if (this.props.blockchainErr === BlockchainErrs.A_CONTRACT_NOT_DEPLOYED_ON_NETWORK) {
|
||||
return '0x smart contracts not found';
|
||||
} else if (!hasWalletAddress) {
|
||||
return 'Enable wallet communication';
|
||||
} else if (this.props.blockchainErr === BlockchainErrs.DISCONNECTED_FROM_ETHEREUM_NODE) {
|
||||
return 'Disconnected from Ethereum network';
|
||||
} else {
|
||||
return 'Unexpected error';
|
||||
}
|
||||
}
|
||||
private renderExplanation(hasWalletAddress: boolean) {
|
||||
if (this.props.blockchainErr === BlockchainErrs.A_CONTRACT_NOT_DEPLOYED_ON_NETWORK) {
|
||||
return this.renderContractsNotDeployedExplanation();
|
||||
} else if (!hasWalletAddress) {
|
||||
return this.renderNoWalletFoundExplanation();
|
||||
} else if (this.props.blockchainErr === BlockchainErrs.DISCONNECTED_FROM_ETHEREUM_NODE) {
|
||||
return this.renderDisconnectedFromNode();
|
||||
} else {
|
||||
return this.renderUnexpectedErrorExplanation();
|
||||
}
|
||||
}
|
||||
private renderDisconnectedFromNode() {
|
||||
return (
|
||||
<div>
|
||||
You were disconnected from the backing Ethereum node.
|
||||
{' '}If using <a href={constants.METAMASK_CHROME_STORE_URL} target="_blank">
|
||||
Metamask
|
||||
</a> or <a href={constants.MIST_DOWNLOAD_URL} target="_blank">Mist</a> try refreshing
|
||||
{' '}the page. If using a locally hosted Ethereum node, make sure it's still running.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderUnexpectedErrorExplanation() {
|
||||
return (
|
||||
<div>
|
||||
We encountered an unexpected error. Please try refreshing the page.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderNoWalletFoundExplanation() {
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
We were unable to access an Ethereum wallet you control. In order to interact
|
||||
{' '}with the 0x portal dApp,
|
||||
we need a way to interact with one of your Ethereum wallets.
|
||||
{' '}There are two easy ways you can enable us to do that:
|
||||
</div>
|
||||
<h4>1. Metamask chrome extension</h4>
|
||||
<div>
|
||||
You can install the{' '}
|
||||
<a href={constants.METAMASK_CHROME_STORE_URL} target="_blank">
|
||||
Metamask
|
||||
</a> Chrome extension Ethereum wallet. Once installed and set up, refresh this page.
|
||||
<div className="pt1">
|
||||
<span className="bold">Note:</span>
|
||||
{' '}If you already have Metamask installed, make sure it is unlocked.
|
||||
</div>
|
||||
</div>
|
||||
<h4>Parity Signer</h4>
|
||||
<div>
|
||||
The <a href={constants.PARITY_CHROME_STORE_URL} target="_blank">Parity Signer
|
||||
Chrome extension</a>{' '}lets you connect to a locally running Parity node.
|
||||
Make sure you have started your local Parity node with{' '}
|
||||
{configs.isMainnetEnabled && '`parity ui` or'} `parity --chain kovan ui`{' '}
|
||||
in order to connect to {configs.isMainnetEnabled ? 'mainnet or Kovan respectively.' : 'Kovan.'}
|
||||
</div>
|
||||
<div className="pt2">
|
||||
<span className="bold">Note:</span>
|
||||
{' '}If you have done one of the above steps and are still seeing this message,
|
||||
{' '}we might still be unable to retrieve an Ethereum address by calling `web3.eth.accounts`.
|
||||
{' '}Make sure you have created at least one Ethereum address.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderContractsNotDeployedExplanation() {
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
The 0x smart contracts are not deployed on the Ethereum network you are
|
||||
{' '}currently connected to (network Id: {this.props.networkId}).
|
||||
{' '}In order to use the 0x portal dApp,
|
||||
{' '}please connect to the
|
||||
{' '}{constants.TESTNET_NAME} testnet (network Id: {constants.TESTNET_NETWORK_ID})
|
||||
{configs.isMainnetEnabled ?
|
||||
` or ${constants.MAINNET_NAME} (network Id: ${constants.MAINNET_NETWORK_ID}).` :
|
||||
`.`
|
||||
}
|
||||
</div>
|
||||
<h4>Metamask</h4>
|
||||
<div>
|
||||
If you are using{' '}
|
||||
<a href={constants.METAMASK_CHROME_STORE_URL} target="_blank">
|
||||
Metamask
|
||||
</a>, you can switch networks in the top left corner of the extension popover.
|
||||
</div>
|
||||
<h4>Parity Signer</h4>
|
||||
<div>
|
||||
If using the <a href={constants.PARITY_CHROME_STORE_URL} target="_blank">Parity Signer
|
||||
Chrome extension</a>, make sure to start your local Parity node with{' '}
|
||||
{configs.isMainnetEnabled ?
|
||||
'`parity ui` or `parity --chain Kovan ui` in order to connect to mainnet \
|
||||
or Kovan respectively.' :
|
||||
'`parity --chain kovan ui` in order to connect to Kovan.'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import * as React from 'react';
|
||||
import Dialog from 'material-ui/Dialog';
|
||||
import FlatButton from 'material-ui/FlatButton';
|
||||
import RadioButtonGroup from 'material-ui/RadioButton/RadioButtonGroup';
|
||||
import RadioButton from 'material-ui/RadioButton';
|
||||
import {Side, Token, TokenState} from 'ts/types';
|
||||
import {TokenAmountInput} from 'ts/components/inputs/token_amount_input';
|
||||
import {EthAmountInput} from 'ts/components/inputs/eth_amount_input';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
interface EthWethConversionDialogProps {
|
||||
onComplete: (direction: Side, value: BigNumber) => void;
|
||||
onCancelled: () => void;
|
||||
isOpen: boolean;
|
||||
token: Token;
|
||||
tokenState: TokenState;
|
||||
etherBalance: BigNumber;
|
||||
}
|
||||
|
||||
interface EthWethConversionDialogState {
|
||||
value?: BigNumber;
|
||||
direction: Side;
|
||||
shouldShowIncompleteErrs: boolean;
|
||||
hasErrors: boolean;
|
||||
}
|
||||
|
||||
export class EthWethConversionDialog extends
|
||||
React.Component<EthWethConversionDialogProps, EthWethConversionDialogState> {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
direction: Side.deposit,
|
||||
shouldShowIncompleteErrs: false,
|
||||
hasErrors: true,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const convertDialogActions = [
|
||||
<FlatButton
|
||||
key="cancel"
|
||||
label="Cancel"
|
||||
onTouchTap={this.onCancel.bind(this)}
|
||||
/>,
|
||||
<FlatButton
|
||||
key="convert"
|
||||
label="Convert"
|
||||
primary={true}
|
||||
onTouchTap={this.onConvertClick.bind(this)}
|
||||
/>,
|
||||
];
|
||||
return (
|
||||
<Dialog
|
||||
title="I want to convert"
|
||||
titleStyle={{fontWeight: 100}}
|
||||
actions={convertDialogActions}
|
||||
open={this.props.isOpen}
|
||||
>
|
||||
{this.renderConversionDialogBody()}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
private renderConversionDialogBody() {
|
||||
return (
|
||||
<div className="mx-auto" style={{maxWidth: 300}}>
|
||||
<RadioButtonGroup
|
||||
className="pb1"
|
||||
defaultSelected={this.state.direction}
|
||||
name="conversionDirection"
|
||||
onChange={this.onConversionDirectionChange.bind(this)}
|
||||
>
|
||||
<RadioButton
|
||||
className="pb1"
|
||||
value={Side.deposit}
|
||||
label="Ether -> Ether Tokens"
|
||||
/>
|
||||
<RadioButton
|
||||
value={Side.receive}
|
||||
label="Ether Tokens -> Ether"
|
||||
/>
|
||||
</RadioButtonGroup>
|
||||
{this.state.direction === Side.receive ?
|
||||
<TokenAmountInput
|
||||
label="Amount to convert"
|
||||
token={this.props.token}
|
||||
tokenState={this.props.tokenState}
|
||||
shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs}
|
||||
shouldCheckBalance={true}
|
||||
shouldCheckAllowance={false}
|
||||
onChange={this.onValueChange.bind(this)}
|
||||
amount={this.state.value}
|
||||
onVisitBalancesPageClick={this.props.onCancelled}
|
||||
/> :
|
||||
<EthAmountInput
|
||||
label="Amount to convert"
|
||||
balance={this.props.etherBalance}
|
||||
amount={this.state.value}
|
||||
onChange={this.onValueChange.bind(this)}
|
||||
shouldCheckBalance={true}
|
||||
shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs}
|
||||
onVisitBalancesPageClick={this.props.onCancelled}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private onConversionDirectionChange(e: any, direction: Side) {
|
||||
this.setState({
|
||||
value: undefined,
|
||||
shouldShowIncompleteErrs: false,
|
||||
direction,
|
||||
hasErrors: true,
|
||||
});
|
||||
}
|
||||
private onValueChange(isValid: boolean, amount?: BigNumber) {
|
||||
this.setState({
|
||||
value: amount,
|
||||
hasErrors: !isValid,
|
||||
});
|
||||
}
|
||||
private onConvertClick() {
|
||||
if (this.state.hasErrors) {
|
||||
this.setState({
|
||||
shouldShowIncompleteErrs: true,
|
||||
});
|
||||
} else {
|
||||
const value = this.state.value;
|
||||
this.setState({
|
||||
value: undefined,
|
||||
});
|
||||
this.props.onComplete(this.state.direction, value);
|
||||
}
|
||||
}
|
||||
private onCancel() {
|
||||
this.setState({
|
||||
value: undefined,
|
||||
});
|
||||
this.props.onCancelled();
|
||||
}
|
||||
}
|
||||
288
packages/website/ts/components/dialogs/ledger_config_dialog.tsx
Normal file
288
packages/website/ts/components/dialogs/ledger_config_dialog.tsx
Normal file
@@ -0,0 +1,288 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import Dialog from 'material-ui/Dialog';
|
||||
import FlatButton from 'material-ui/FlatButton';
|
||||
import TextField from 'material-ui/TextField';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableHeaderColumn,
|
||||
TableRowColumn,
|
||||
} from 'material-ui/Table';
|
||||
import ReactTooltip = require('react-tooltip');
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {Blockchain} from 'ts/blockchain';
|
||||
import {Dispatcher} from 'ts/redux/dispatcher';
|
||||
import {LifeCycleRaisedButton} from 'ts/components/ui/lifecycle_raised_button';
|
||||
|
||||
const VALID_ETHEREUM_DERIVATION_PATH_PREFIX = `44'/60'`;
|
||||
|
||||
enum LedgerSteps {
|
||||
CONNECT,
|
||||
SELECT_ADDRESS,
|
||||
}
|
||||
|
||||
interface LedgerConfigDialogProps {
|
||||
isOpen: boolean;
|
||||
toggleDialogFn: (isOpen: boolean) => void;
|
||||
dispatcher: Dispatcher;
|
||||
blockchain: Blockchain;
|
||||
networkId: number;
|
||||
}
|
||||
|
||||
interface LedgerConfigDialogState {
|
||||
didConnectFail: boolean;
|
||||
stepIndex: LedgerSteps;
|
||||
userAddresses: string[];
|
||||
addressBalances: BigNumber[];
|
||||
derivationPath: string;
|
||||
derivationErrMsg: string;
|
||||
}
|
||||
|
||||
export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, LedgerConfigDialogState> {
|
||||
constructor(props: LedgerConfigDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
didConnectFail: false,
|
||||
stepIndex: LedgerSteps.CONNECT,
|
||||
userAddresses: [],
|
||||
addressBalances: [],
|
||||
derivationPath: constants.DEFAULT_DERIVATION_PATH,
|
||||
derivationErrMsg: '',
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const dialogActions = [
|
||||
<FlatButton
|
||||
label="Cancel"
|
||||
onTouchTap={this.onClose.bind(this)}
|
||||
/>,
|
||||
];
|
||||
const dialogTitle = this.state.stepIndex === LedgerSteps.CONNECT ?
|
||||
'Connect to your Ledger' :
|
||||
'Select desired address';
|
||||
return (
|
||||
<Dialog
|
||||
title={dialogTitle}
|
||||
titleStyle={{fontWeight: 100}}
|
||||
actions={dialogActions}
|
||||
open={this.props.isOpen}
|
||||
onRequestClose={this.onClose.bind(this)}
|
||||
autoScrollBodyContent={true}
|
||||
bodyStyle={{paddingBottom: 0}}
|
||||
>
|
||||
<div style={{color: colors.grey700, paddingTop: 1}}>
|
||||
{this.state.stepIndex === LedgerSteps.CONNECT &&
|
||||
this.renderConnectStep()
|
||||
}
|
||||
{this.state.stepIndex === LedgerSteps.SELECT_ADDRESS &&
|
||||
this.renderSelectAddressStep()
|
||||
}
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
private renderConnectStep() {
|
||||
return (
|
||||
<div>
|
||||
<div className="h4 pt3">
|
||||
Follow these instructions before proceeding:
|
||||
</div>
|
||||
<ol>
|
||||
<li className="pb1">
|
||||
Connect your Ledger Nano S & Open the Ethereum application
|
||||
</li>
|
||||
<li className="pb1">
|
||||
Verify that Browser Support is enabled in Settings
|
||||
</li>
|
||||
<li className="pb1">
|
||||
If no Browser Support is found in settings, verify that you have{' '}
|
||||
<a href="https://www.ledgerwallet.com/apps/manager" target="_blank">Firmware >1.2</a>
|
||||
</li>
|
||||
</ol>
|
||||
<div className="center pb3">
|
||||
<LifeCycleRaisedButton
|
||||
isPrimary={true}
|
||||
labelReady="Connect to Ledger"
|
||||
labelLoading="Connecting..."
|
||||
labelComplete="Connected!"
|
||||
onClickAsyncFn={this.onConnectLedgerClickAsync.bind(this, true)}
|
||||
/>
|
||||
{this.state.didConnectFail &&
|
||||
<div className="pt2 left-align" style={{color: colors.red200}}>
|
||||
Failed to connect. Follow the instructions and try again.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderSelectAddressStep() {
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Table
|
||||
bodyStyle={{height: 300}}
|
||||
onRowSelection={this.onAddressSelected.bind(this)}
|
||||
>
|
||||
<TableHeader displaySelectAll={false}>
|
||||
<TableRow>
|
||||
<TableHeaderColumn colSpan={2}>Address</TableHeaderColumn>
|
||||
<TableHeaderColumn>Balance</TableHeaderColumn>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{this.renderAddressTableRows()}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex pt2" style={{height: 100}}>
|
||||
<div className="overflow-hidden" style={{width: 180}}>
|
||||
<TextField
|
||||
floatingLabelFixed={true}
|
||||
floatingLabelStyle={{color: colors.grey500}}
|
||||
floatingLabelText="Update path derivation (advanced)"
|
||||
value={this.state.derivationPath}
|
||||
errorText={this.state.derivationErrMsg}
|
||||
onChange={this.onDerivationPathChanged.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
<div className="pl2" style={{paddingTop: 28}}>
|
||||
<LifeCycleRaisedButton
|
||||
labelReady="Update"
|
||||
labelLoading="Updating..."
|
||||
labelComplete="Updated!"
|
||||
onClickAsyncFn={this.onFetchAddressesForDerivationPathAsync.bind(this, true)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderAddressTableRows() {
|
||||
const rows = _.map(this.state.userAddresses, (userAddress: string, i: number) => {
|
||||
const balance = this.state.addressBalances[i];
|
||||
const addressTooltipId = `address-${userAddress}`;
|
||||
const balanceTooltipId = `balance-${userAddress}`;
|
||||
const networkName = constants.networkNameById[this.props.networkId];
|
||||
// We specifically prefix kovan ETH.
|
||||
// TODO: We should probably add prefixes for all networks
|
||||
const isKovanNetwork = networkName === 'Kovan';
|
||||
const balanceString = `${balance.toString()} ${isKovanNetwork ? 'Kovan ' : ''}ETH`;
|
||||
return (
|
||||
<TableRow key={userAddress} style={{height: 40}}>
|
||||
<TableRowColumn colSpan={2}>
|
||||
<div
|
||||
data-tip={true}
|
||||
data-for={addressTooltipId}
|
||||
>
|
||||
{userAddress}
|
||||
</div>
|
||||
<ReactTooltip id={addressTooltipId}>{userAddress}</ReactTooltip>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn>
|
||||
<div
|
||||
data-tip={true}
|
||||
data-for={balanceTooltipId}
|
||||
>
|
||||
{balanceString}
|
||||
</div>
|
||||
<ReactTooltip id={balanceTooltipId}>{balanceString}</ReactTooltip>
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
);
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
private onClose() {
|
||||
this.setState({
|
||||
didConnectFail: false,
|
||||
});
|
||||
const isOpen = false;
|
||||
this.props.toggleDialogFn(isOpen);
|
||||
}
|
||||
private onAddressSelected(selectedRowIndexes: number[]) {
|
||||
const selectedRowIndex = selectedRowIndexes[0];
|
||||
this.props.blockchain.updateLedgerDerivationIndex(selectedRowIndex);
|
||||
const selectedAddress = this.state.userAddresses[selectedRowIndex];
|
||||
const selectAddressBalance = this.state.addressBalances[selectedRowIndex];
|
||||
this.props.dispatcher.updateUserAddress(selectedAddress);
|
||||
this.props.blockchain.updateWeb3WrapperPrevUserAddress(selectedAddress);
|
||||
this.props.dispatcher.updateUserEtherBalance(selectAddressBalance);
|
||||
this.setState({
|
||||
stepIndex: LedgerSteps.CONNECT,
|
||||
});
|
||||
const isOpen = false;
|
||||
this.props.toggleDialogFn(isOpen);
|
||||
}
|
||||
private async onFetchAddressesForDerivationPathAsync() {
|
||||
const currentlySetPath = this.props.blockchain.getLedgerDerivationPathIfExists();
|
||||
if (currentlySetPath === this.state.derivationPath) {
|
||||
return;
|
||||
}
|
||||
this.props.blockchain.updateLedgerDerivationPathIfExists(this.state.derivationPath);
|
||||
const didSucceed = await this.fetchAddressesAndBalancesAsync();
|
||||
if (!didSucceed) {
|
||||
this.setState({
|
||||
derivationErrMsg: 'Failed to connect to Ledger.',
|
||||
});
|
||||
}
|
||||
return didSucceed;
|
||||
}
|
||||
private async fetchAddressesAndBalancesAsync() {
|
||||
let userAddresses: string[];
|
||||
const addressBalances: BigNumber[] = [];
|
||||
try {
|
||||
userAddresses = await this.getUserAddressesAsync();
|
||||
for (const address of userAddresses) {
|
||||
const balance = await this.props.blockchain.getBalanceInEthAsync(address);
|
||||
addressBalances.push(balance);
|
||||
}
|
||||
} catch (err) {
|
||||
utils.consoleLog(`Ledger error: ${JSON.stringify(err)}`);
|
||||
this.setState({
|
||||
didConnectFail: true,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
this.setState({
|
||||
userAddresses,
|
||||
addressBalances,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
private onDerivationPathChanged(e: any, derivationPath: string) {
|
||||
let derivationErrMsg = '';
|
||||
if (!_.startsWith(derivationPath, VALID_ETHEREUM_DERIVATION_PATH_PREFIX)) {
|
||||
derivationErrMsg = 'Must be valid Ethereum path.';
|
||||
}
|
||||
|
||||
this.setState({
|
||||
derivationPath,
|
||||
derivationErrMsg,
|
||||
});
|
||||
}
|
||||
private async onConnectLedgerClickAsync() {
|
||||
const didSucceed = await this.fetchAddressesAndBalancesAsync();
|
||||
if (didSucceed) {
|
||||
this.setState({
|
||||
stepIndex: LedgerSteps.SELECT_ADDRESS,
|
||||
});
|
||||
}
|
||||
return didSucceed;
|
||||
}
|
||||
private async getUserAddressesAsync(): Promise<string[]> {
|
||||
let userAddresses: string[];
|
||||
userAddresses = await this.props.blockchain.getUserAccountsAsync();
|
||||
|
||||
if (_.isEmpty(userAddresses)) {
|
||||
throw new Error('No addresses retrieved.');
|
||||
}
|
||||
return userAddresses;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import * as React from 'react';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import FlatButton from 'material-ui/FlatButton';
|
||||
import Dialog from 'material-ui/Dialog';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
|
||||
interface PortalDisclaimerDialogProps {
|
||||
isOpen: boolean;
|
||||
onToggleDialog: () => void;
|
||||
}
|
||||
|
||||
export function PortalDisclaimerDialog(props: PortalDisclaimerDialogProps) {
|
||||
return (
|
||||
<Dialog
|
||||
title="0x Portal Disclaimer"
|
||||
titleStyle={{fontWeight: 100}}
|
||||
actions={[
|
||||
<FlatButton
|
||||
label="I Agree"
|
||||
onTouchTap={props.onToggleDialog.bind(this)}
|
||||
/>,
|
||||
]}
|
||||
open={props.isOpen}
|
||||
onRequestClose={props.onToggleDialog.bind(this)}
|
||||
autoScrollBodyContent={true}
|
||||
modal={true}
|
||||
>
|
||||
<div className="pt2" style={{color: colors.grey700}}>
|
||||
<div>
|
||||
0x Portal is a free software-based tool intended to help users to
|
||||
buy and sell ERC20-compatible blockchain tokens through the 0x protocol
|
||||
on a purely peer-to-peer basis. 0x portal is not a regulated marketplace,
|
||||
exchange or intermediary of any kind, and therefore, you should only use
|
||||
0x portal to exchange tokens that are not securities, commodity interests,
|
||||
or any other form of regulated instrument. 0x has not attempted to screen
|
||||
or otherwise limit the tokens that you may enter in 0x Portal. By clicking
|
||||
“I Agree” below, you understand that you are solely responsible for using 0x
|
||||
Portal and buying and selling tokens using 0x Portal in compliance with all
|
||||
applicable laws and regulations.
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
126
packages/website/ts/components/dialogs/send_dialog.tsx
Normal file
126
packages/website/ts/components/dialogs/send_dialog.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import * as React from 'react';
|
||||
import * as _ from 'lodash';
|
||||
import Dialog from 'material-ui/Dialog';
|
||||
import FlatButton from 'material-ui/FlatButton';
|
||||
import RadioButtonGroup from 'material-ui/RadioButton/RadioButtonGroup';
|
||||
import RadioButton from 'material-ui/RadioButton';
|
||||
import {Side, Token, TokenState} from 'ts/types';
|
||||
import {TokenAmountInput} from 'ts/components/inputs/token_amount_input';
|
||||
import {EthAmountInput} from 'ts/components/inputs/eth_amount_input';
|
||||
import {AddressInput} from 'ts/components/inputs/address_input';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
interface SendDialogProps {
|
||||
onComplete: (recipient: string, value: BigNumber) => void;
|
||||
onCancelled: () => void;
|
||||
isOpen: boolean;
|
||||
token: Token;
|
||||
tokenState: TokenState;
|
||||
}
|
||||
|
||||
interface SendDialogState {
|
||||
value?: BigNumber;
|
||||
recipient: string;
|
||||
shouldShowIncompleteErrs: boolean;
|
||||
isAmountValid: boolean;
|
||||
}
|
||||
|
||||
export class SendDialog extends React.Component<SendDialogProps, SendDialogState> {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
recipient: '',
|
||||
shouldShowIncompleteErrs: false,
|
||||
isAmountValid: false,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const transferDialogActions = [
|
||||
<FlatButton
|
||||
key="cancelTransfer"
|
||||
label="Cancel"
|
||||
onTouchTap={this.onCancel.bind(this)}
|
||||
/>,
|
||||
<FlatButton
|
||||
key="sendTransfer"
|
||||
disabled={this.hasErrors()}
|
||||
label="Send"
|
||||
primary={true}
|
||||
onTouchTap={this.onSendClick.bind(this)}
|
||||
/>,
|
||||
];
|
||||
return (
|
||||
<Dialog
|
||||
title="I want to send"
|
||||
titleStyle={{fontWeight: 100}}
|
||||
actions={transferDialogActions}
|
||||
open={this.props.isOpen}
|
||||
>
|
||||
{this.renderSendDialogBody()}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
private renderSendDialogBody() {
|
||||
return (
|
||||
<div className="mx-auto" style={{maxWidth: 300}}>
|
||||
<div style={{height: 80}}>
|
||||
<AddressInput
|
||||
initialAddress={this.state.recipient}
|
||||
updateAddress={this.onRecipientChange.bind(this)}
|
||||
isRequired={true}
|
||||
label={'Recipient address'}
|
||||
hintText={'Address'}
|
||||
/>
|
||||
</div>
|
||||
<TokenAmountInput
|
||||
label="Amount to send"
|
||||
token={this.props.token}
|
||||
tokenState={this.props.tokenState}
|
||||
shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs}
|
||||
shouldCheckBalance={true}
|
||||
shouldCheckAllowance={false}
|
||||
onChange={this.onValueChange.bind(this)}
|
||||
amount={this.state.value}
|
||||
onVisitBalancesPageClick={this.props.onCancelled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private onRecipientChange(recipient?: string) {
|
||||
this.setState({
|
||||
shouldShowIncompleteErrs: false,
|
||||
recipient,
|
||||
});
|
||||
}
|
||||
private onValueChange(isValid: boolean, amount?: BigNumber) {
|
||||
this.setState({
|
||||
isAmountValid: isValid,
|
||||
value: amount,
|
||||
});
|
||||
}
|
||||
private onSendClick() {
|
||||
if (this.hasErrors()) {
|
||||
this.setState({
|
||||
shouldShowIncompleteErrs: true,
|
||||
});
|
||||
} else {
|
||||
const value = this.state.value;
|
||||
this.setState({
|
||||
recipient: undefined,
|
||||
value: undefined,
|
||||
});
|
||||
this.props.onComplete(this.state.recipient, value);
|
||||
}
|
||||
}
|
||||
private onCancel() {
|
||||
this.setState({
|
||||
value: undefined,
|
||||
});
|
||||
this.props.onCancelled();
|
||||
}
|
||||
private hasErrors() {
|
||||
return _.isUndefined(this.state.recipient) ||
|
||||
_.isUndefined(this.state.value) ||
|
||||
!this.state.isAmountValid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import FlatButton from 'material-ui/FlatButton';
|
||||
import Dialog from 'material-ui/Dialog';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {Blockchain} from 'ts/blockchain';
|
||||
import {Dispatcher} from 'ts/redux/dispatcher';
|
||||
import {TrackTokenConfirmation} from 'ts/components/track_token_confirmation';
|
||||
import {trackedTokenStorage} from 'ts/local_storage/tracked_token_storage';
|
||||
import {Token, TokenByAddress} from 'ts/types';
|
||||
|
||||
interface TrackTokenConfirmationDialogProps {
|
||||
tokens: Token[];
|
||||
tokenByAddress: TokenByAddress;
|
||||
isOpen: boolean;
|
||||
onToggleDialog: (didConfirmTokenTracking: boolean) => void;
|
||||
dispatcher: Dispatcher;
|
||||
networkId: number;
|
||||
blockchain: Blockchain;
|
||||
userAddress: string;
|
||||
}
|
||||
|
||||
interface TrackTokenConfirmationDialogState {
|
||||
isAddingTokenToTracked: boolean;
|
||||
}
|
||||
|
||||
export class TrackTokenConfirmationDialog extends
|
||||
React.Component<TrackTokenConfirmationDialogProps, TrackTokenConfirmationDialogState> {
|
||||
constructor(props: TrackTokenConfirmationDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isAddingTokenToTracked: false,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const tokens = this.props.tokens;
|
||||
return (
|
||||
<Dialog
|
||||
title="Tracking confirmation"
|
||||
titleStyle={{fontWeight: 100}}
|
||||
actions={[
|
||||
<FlatButton
|
||||
label="No"
|
||||
onTouchTap={this.onTrackConfirmationRespondedAsync.bind(this, false)}
|
||||
/>,
|
||||
<FlatButton
|
||||
label="Yes"
|
||||
onTouchTap={this.onTrackConfirmationRespondedAsync.bind(this, true)}
|
||||
/>,
|
||||
]}
|
||||
open={this.props.isOpen}
|
||||
onRequestClose={this.props.onToggleDialog.bind(this, false)}
|
||||
autoScrollBodyContent={true}
|
||||
>
|
||||
<div className="pt2">
|
||||
<TrackTokenConfirmation
|
||||
tokens={tokens}
|
||||
networkId={this.props.networkId}
|
||||
tokenByAddress={this.props.tokenByAddress}
|
||||
isAddingTokenToTracked={this.state.isAddingTokenToTracked}
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
private async onTrackConfirmationRespondedAsync(didUserAcceptTracking: boolean) {
|
||||
if (!didUserAcceptTracking) {
|
||||
this.props.onToggleDialog(didUserAcceptTracking);
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
isAddingTokenToTracked: true,
|
||||
});
|
||||
for (const token of this.props.tokens) {
|
||||
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,
|
||||
});
|
||||
this.props.onToggleDialog(didUserAcceptTracking);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as React from 'react';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import FlatButton from 'material-ui/FlatButton';
|
||||
import Dialog from 'material-ui/Dialog';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
|
||||
interface U2fNotSupportedDialogProps {
|
||||
isOpen: boolean;
|
||||
onToggleDialog: () => void;
|
||||
}
|
||||
|
||||
export function U2fNotSupportedDialog(props: U2fNotSupportedDialogProps) {
|
||||
return (
|
||||
<Dialog
|
||||
title="U2F Not Supported"
|
||||
titleStyle={{fontWeight: 100}}
|
||||
actions={[
|
||||
<FlatButton
|
||||
label="Ok"
|
||||
onTouchTap={props.onToggleDialog.bind(this)}
|
||||
/>,
|
||||
]}
|
||||
open={props.isOpen}
|
||||
onRequestClose={props.onToggleDialog.bind(this)}
|
||||
autoScrollBodyContent={true}
|
||||
>
|
||||
<div className="pt2" style={{color: colors.grey700}}>
|
||||
<div>
|
||||
It looks like your browser does not support U2F connections
|
||||
required for us to communicate with your hardware wallet.
|
||||
Please use a browser that supports U2F connections and try
|
||||
again.
|
||||
</div>
|
||||
<div>
|
||||
<ul>
|
||||
<li className="pb1">Chrome version 38 or later</li>
|
||||
<li className="pb1">Opera version 40 of later</li>
|
||||
<li>
|
||||
Firefox with{' '}
|
||||
<a
|
||||
href={constants.FIREFOX_U2F_ADDON}
|
||||
target="_blank"
|
||||
style={{textDecoration: 'underline'}}
|
||||
>
|
||||
this extension
|
||||
</a>.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
101
packages/website/ts/components/eth_weth_conversion_button.tsx
Normal file
101
packages/website/ts/components/eth_weth_conversion_button.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import * as _ from 'lodash';
|
||||
import {ZeroEx} from '0x.js';
|
||||
import * as React from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import RaisedButton from 'material-ui/RaisedButton';
|
||||
import {BlockchainCallErrs, TokenState} from 'ts/types';
|
||||
import {EthWethConversionDialog} from 'ts/components/dialogs/eth_weth_conversion_dialog';
|
||||
import {Side, Token} from 'ts/types';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {Dispatcher} from 'ts/redux/dispatcher';
|
||||
import {errorReporter} from 'ts/utils/error_reporter';
|
||||
import {Blockchain} from 'ts/blockchain';
|
||||
|
||||
interface EthWethConversionButtonProps {
|
||||
ethToken: Token;
|
||||
ethTokenState: TokenState;
|
||||
dispatcher: Dispatcher;
|
||||
blockchain: Blockchain;
|
||||
userEtherBalance: BigNumber;
|
||||
onError: () => void;
|
||||
}
|
||||
|
||||
interface EthWethConversionButtonState {
|
||||
isEthConversionDialogVisible: boolean;
|
||||
isEthConversionHappening: boolean;
|
||||
}
|
||||
|
||||
export class EthWethConversionButton extends
|
||||
React.Component<EthWethConversionButtonProps, EthWethConversionButtonState> {
|
||||
public constructor(props: EthWethConversionButtonProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isEthConversionDialogVisible: false,
|
||||
isEthConversionHappening: false,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const labelStyle = this.state.isEthConversionHappening ? {fontSize: 10} : {};
|
||||
return (
|
||||
<div>
|
||||
<RaisedButton
|
||||
style={{width: '100%'}}
|
||||
labelStyle={labelStyle}
|
||||
disabled={this.state.isEthConversionHappening}
|
||||
label={this.state.isEthConversionHappening ? 'Converting...' : 'Convert'}
|
||||
onClick={this.toggleConversionDialog.bind(this)}
|
||||
/>
|
||||
<EthWethConversionDialog
|
||||
isOpen={this.state.isEthConversionDialogVisible}
|
||||
onComplete={this.onConversionAmountSelectedAsync.bind(this)}
|
||||
onCancelled={this.toggleConversionDialog.bind(this)}
|
||||
etherBalance={this.props.userEtherBalance}
|
||||
token={this.props.ethToken}
|
||||
tokenState={this.props.ethTokenState}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private toggleConversionDialog() {
|
||||
this.setState({
|
||||
isEthConversionDialogVisible: !this.state.isEthConversionDialogVisible,
|
||||
});
|
||||
}
|
||||
private async onConversionAmountSelectedAsync(direction: Side, value: BigNumber) {
|
||||
this.setState({
|
||||
isEthConversionHappening: true,
|
||||
});
|
||||
this.toggleConversionDialog();
|
||||
const token = this.props.ethToken;
|
||||
const tokenState = this.props.ethTokenState;
|
||||
let balance = tokenState.balance;
|
||||
try {
|
||||
if (direction === Side.deposit) {
|
||||
await this.props.blockchain.convertEthToWrappedEthTokensAsync(value);
|
||||
const ethAmount = ZeroEx.toUnitAmount(value, constants.ETH_DECIMAL_PLACES);
|
||||
this.props.dispatcher.showFlashMessage(`Successfully converted ${ethAmount.toString()} ETH to WETH`);
|
||||
balance = balance.plus(value);
|
||||
} else {
|
||||
await this.props.blockchain.convertWrappedEthTokensToEthAsync(value);
|
||||
const tokenAmount = ZeroEx.toUnitAmount(value, token.decimals);
|
||||
this.props.dispatcher.showFlashMessage(`Successfully converted ${tokenAmount.toString()} WETH to ETH`);
|
||||
balance = balance.minus(value);
|
||||
}
|
||||
this.props.dispatcher.replaceTokenBalanceByAddress(token.address, balance);
|
||||
} catch (err) {
|
||||
const errMsg = '' + err;
|
||||
if (_.includes(errMsg, BlockchainCallErrs.USER_HAS_NO_ASSOCIATED_ADDRESSES)) {
|
||||
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
|
||||
} else if (!_.includes(errMsg, 'User denied transaction')) {
|
||||
utils.consoleLog(`Unexpected error encountered: ${err}`);
|
||||
utils.consoleLog(err.stack);
|
||||
await errorReporter.reportAsync(err);
|
||||
this.props.onError();
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
isEthConversionHappening: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
714
packages/website/ts/components/fill_order.tsx
Normal file
714
packages/website/ts/components/fill_order.tsx
Normal file
@@ -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<FillOrderProps, FillOrderState> {
|
||||
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 (
|
||||
<div className="clearfix lg-px4 md-px4 sm-px2" style={{minHeight: 600}}>
|
||||
<h3>Fill an order</h3>
|
||||
<Divider />
|
||||
<div>
|
||||
{!this.props.isOrderInUrl &&
|
||||
<div>
|
||||
<div className="pt2 pb2">
|
||||
Paste an order JSON snippet below to begin
|
||||
</div>
|
||||
<div className="pb2">Order JSON</div>
|
||||
<FillOrderJSON
|
||||
blockchain={this.props.blockchain}
|
||||
tokenByAddress={this.props.tokenByAddress}
|
||||
networkId={this.props.networkId}
|
||||
orderJSON={this.state.orderJSON}
|
||||
onFillOrderJSONChanged={this.onFillOrderJSONChanged.bind(this)}
|
||||
/>
|
||||
{this.renderOrderJsonNotices()}
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
{!_.isUndefined(this.state.parsedOrder) && this.state.didOrderValidationRun
|
||||
&& this.state.areAllInvolvedTokensTracked &&
|
||||
this.renderVisualOrder()
|
||||
}
|
||||
</div>
|
||||
{this.props.isOrderInUrl &&
|
||||
<div className="pt2">
|
||||
<Card style={{boxShadow: 'none', backgroundColor: 'none', border: '1px solid #eceaea'}}>
|
||||
<CardHeader
|
||||
title="Order JSON"
|
||||
actAsExpander={true}
|
||||
showExpandableButton={true}
|
||||
/>
|
||||
<CardText expandable={true}>
|
||||
<FillOrderJSON
|
||||
blockchain={this.props.blockchain}
|
||||
tokenByAddress={this.props.tokenByAddress}
|
||||
networkId={this.props.networkId}
|
||||
orderJSON={this.state.orderJSON}
|
||||
onFillOrderJSONChanged={this.onFillOrderJSONChanged.bind(this)}
|
||||
/>
|
||||
</CardText>
|
||||
</Card>
|
||||
{this.renderOrderJsonNotices()}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<FillWarningDialog
|
||||
isOpen={this.state.isFillWarningDialogOpen}
|
||||
onToggleDialog={this.onFillWarningClosed.bind(this)}
|
||||
/>
|
||||
<TrackTokenConfirmationDialog
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
blockchain={this.props.blockchain}
|
||||
tokenByAddress={this.props.tokenByAddress}
|
||||
dispatcher={this.props.dispatcher}
|
||||
tokens={this.state.tokensToTrack}
|
||||
isOpen={this.state.isConfirmingTokenTracking}
|
||||
onToggleDialog={this.onToggleTrackConfirmDialog.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderOrderJsonNotices() {
|
||||
return (
|
||||
<div>
|
||||
{!_.isUndefined(this.props.initialOrder) && !this.state.didOrderValidationRun &&
|
||||
<div className="pt2">
|
||||
<span className="pr1">
|
||||
<i className="zmdi zmdi-spinner zmdi-hc-spin" />
|
||||
</span>
|
||||
<span>Validating order...</span>
|
||||
</div>
|
||||
}
|
||||
{!_.isEmpty(this.state.orderJSONErrMsg) &&
|
||||
<Alert type={AlertTypes.ERROR} message={this.state.orderJSONErrMsg} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<div className="pt3 pb1">
|
||||
<div className="clearfix pb2" style={{width: '100%'}}>
|
||||
<div className="inline left">Order details</div>
|
||||
<div className="inline right" style={{minWidth: 208}}>
|
||||
<div className="col col-4 pl2" style={{color: '#BEBEBE'}}>
|
||||
Maker:
|
||||
</div>
|
||||
<div className="col col-2 pr1">
|
||||
<Identicon
|
||||
address={this.state.parsedOrder.maker.address}
|
||||
diameter={23}
|
||||
/>
|
||||
</div>
|
||||
<div className="col col-6">
|
||||
<EthereumAddress
|
||||
address={this.state.parsedOrder.maker.address}
|
||||
networkId={this.props.networkId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg-px4 md-px4 sm-px0">
|
||||
<div className="lg-px4 md-px4 sm-px1 pt1">
|
||||
<VisualOrder
|
||||
orderTakerAddress={orderTaker}
|
||||
orderMakerAddress={this.state.parsedOrder.maker.address}
|
||||
makerAssetToken={makerAssetToken}
|
||||
takerAssetToken={takerAssetToken}
|
||||
tokenByAddress={this.props.tokenByAddress}
|
||||
makerToken={makerToken}
|
||||
takerToken={takerToken}
|
||||
networkId={this.props.networkId}
|
||||
isMakerTokenAddressInRegistry={this.state.isMakerTokenAddressInRegistry}
|
||||
isTakerTokenAddressInRegistry={this.state.isTakerTokenAddressInRegistry}
|
||||
/>
|
||||
<div className="center pt3 pb2">
|
||||
Expires: {expiryDate} UTC
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!isUserMaker &&
|
||||
<div className="clearfix mx-auto" style={{width: 315, height: 108}}>
|
||||
<div className="col col-7" style={{maxWidth: 235}}>
|
||||
<TokenAmountInput
|
||||
label="Fill amount"
|
||||
onChange={this.onFillAmountChange.bind(this)}
|
||||
shouldShowIncompleteErrs={false}
|
||||
token={fillToken}
|
||||
tokenState={fillTokenState}
|
||||
amount={fillAssetToken.amount}
|
||||
shouldCheckBalance={true}
|
||||
shouldCheckAllowance={true}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col col-5 pl1"
|
||||
style={{color: CUSTOM_LIGHT_GRAY, paddingTop: 39}}
|
||||
>
|
||||
= {accounting.formatNumber(orderReceiveAmount, 6)} {makerToken.symbol}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
{isUserMaker ?
|
||||
<div>
|
||||
<RaisedButton
|
||||
style={{width: '100%'}}
|
||||
disabled={this.state.isCancelling}
|
||||
label={this.state.isCancelling ? 'Cancelling order...' : 'Cancel order'}
|
||||
onClick={this.onCancelOrderClickFireAndForgetAsync.bind(this)}
|
||||
/>
|
||||
{this.state.didCancelOrderSucceed &&
|
||||
<Alert
|
||||
type={AlertTypes.SUCCESS}
|
||||
message={this.renderCancelSuccessMsg()}
|
||||
/>
|
||||
}
|
||||
</div> :
|
||||
<div>
|
||||
<RaisedButton
|
||||
style={{width: '100%'}}
|
||||
disabled={this.state.isFilling}
|
||||
label={this.state.isFilling ? 'Filling order...' : 'Fill order'}
|
||||
onClick={this.onFillOrderClick.bind(this)}
|
||||
/>
|
||||
{!_.isEmpty(this.state.globalErrMsg) &&
|
||||
<Alert type={AlertTypes.ERROR} message={this.state.globalErrMsg} />
|
||||
}
|
||||
{this.state.didFillOrderSucceed &&
|
||||
<Alert
|
||||
type={AlertTypes.SUCCESS}
|
||||
message={this.renderFillSuccessMsg()}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderFillSuccessMsg() {
|
||||
return (
|
||||
<div>
|
||||
Order successfully filled. See the trade details in your{' '}
|
||||
<Link
|
||||
to={`${WebsitePaths.Portal}/trades`}
|
||||
style={{color: 'white'}}
|
||||
>
|
||||
trade history
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderCancelSuccessMsg() {
|
||||
return (
|
||||
<div>
|
||||
Order successfully cancelled.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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<void> {
|
||||
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<void> {
|
||||
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: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
69
packages/website/ts/components/fill_order_json.tsx
Normal file
69
packages/website/ts/components/fill_order_json.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {ZeroEx} from '0x.js';
|
||||
import Paper from 'material-ui/Paper';
|
||||
import TextField from 'material-ui/TextField';
|
||||
import {Side, TokenByAddress} from 'ts/types';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {Blockchain} from 'ts/blockchain';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
|
||||
interface FillOrderJSONProps {
|
||||
blockchain: Blockchain;
|
||||
tokenByAddress: TokenByAddress;
|
||||
networkId: number;
|
||||
orderJSON: string;
|
||||
onFillOrderJSONChanged: (event: any) => void;
|
||||
}
|
||||
|
||||
interface FillOrderJSONState {}
|
||||
|
||||
export class FillOrderJSON extends React.Component<FillOrderJSONProps, FillOrderJSONState> {
|
||||
public render() {
|
||||
const tokenAddresses = _.keys(this.props.tokenByAddress);
|
||||
const exchangeContract = this.props.blockchain.getExchangeContractAddressIfExists();
|
||||
const hintSideToAssetToken = {
|
||||
[Side.deposit]: {
|
||||
amount: new BigNumber(35),
|
||||
address: tokenAddresses[0],
|
||||
},
|
||||
[Side.receive]: {
|
||||
amount: new BigNumber(89),
|
||||
address: tokenAddresses[1],
|
||||
},
|
||||
};
|
||||
const hintOrderExpiryTimestamp = utils.initialOrderExpiryUnixTimestampSec();
|
||||
const hintSignatureData = {
|
||||
hash: '0xf965a9978a0381ab58f5a2408ad967c...',
|
||||
r: '0xf01103f759e2289a28593eaf22e5820032...',
|
||||
s: '937862111edcba395f8a9e0cc1b2c5e12320...',
|
||||
v: 27,
|
||||
};
|
||||
const hintSalt = ZeroEx.generatePseudoRandomSalt();
|
||||
const hintOrder = utils.generateOrder(this.props.networkId, exchangeContract, hintSideToAssetToken,
|
||||
hintOrderExpiryTimestamp, '', '', constants.MAKER_FEE,
|
||||
constants.TAKER_FEE, constants.FEE_RECIPIENT_ADDRESS,
|
||||
hintSignatureData, this.props.tokenByAddress, hintSalt);
|
||||
const hintOrderJSON = `${JSON.stringify(hintOrder, null, '\t').substring(0, 500)}...`;
|
||||
return (
|
||||
<div>
|
||||
<Paper className="p1 overflow-hidden" style={{height: 164}}>
|
||||
<TextField
|
||||
id="orderJSON"
|
||||
hintStyle={{bottom: 0, top: 0}}
|
||||
fullWidth={true}
|
||||
value={this.props.orderJSON}
|
||||
onChange={this.props.onFillOrderJSONChanged.bind(this)}
|
||||
hintText={hintOrderJSON}
|
||||
multiLine={true}
|
||||
rows={6}
|
||||
rowsMax={6}
|
||||
underlineStyle={{display: 'none'}}
|
||||
textareaStyle={{marginTop: 0}}
|
||||
/>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
47
packages/website/ts/components/fill_warning_dialog.tsx
Normal file
47
packages/website/ts/components/fill_warning_dialog.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as React from 'react';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import FlatButton from 'material-ui/FlatButton';
|
||||
import Dialog from 'material-ui/Dialog';
|
||||
|
||||
interface FillWarningDialogProps {
|
||||
isOpen: boolean;
|
||||
onToggleDialog: () => void;
|
||||
}
|
||||
|
||||
export function FillWarningDialog(props: FillWarningDialogProps) {
|
||||
const didCancel = true;
|
||||
return (
|
||||
<Dialog
|
||||
title="Warning"
|
||||
titleStyle={{fontWeight: 100, color: colors.red500}}
|
||||
actions={[
|
||||
<FlatButton
|
||||
label="Cancel"
|
||||
onTouchTap={props.onToggleDialog.bind(this, didCancel)}
|
||||
/>,
|
||||
<FlatButton
|
||||
label="Fill Order"
|
||||
onTouchTap={props.onToggleDialog.bind(this, !didCancel)}
|
||||
/>,
|
||||
]}
|
||||
open={props.isOpen}
|
||||
onRequestClose={props.onToggleDialog.bind(this)}
|
||||
autoScrollBodyContent={true}
|
||||
modal={true}
|
||||
>
|
||||
<div className="pt2" style={{color: colors.grey700}}>
|
||||
<div>
|
||||
At least one of the tokens in this order was not found in the
|
||||
token registry smart contract and may be counterfeit. It is your
|
||||
responsibility to verify the token addresses on Etherscan (
|
||||
<a
|
||||
href="https://0xproject.com/wiki#Verifying-Custom-Tokens"
|
||||
target="_blank"
|
||||
>
|
||||
See this how-to guide
|
||||
</a>) before filling an order. <b>This action may result in the loss of funds</b>.
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as React from 'react';
|
||||
import * as _ from 'lodash';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {ZeroEx} from '0x.js';
|
||||
import {Token} from 'ts/types';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
|
||||
interface TokenSendCompletedProps {
|
||||
etherScanLinkIfExists?: string;
|
||||
token: Token;
|
||||
toAddress: string;
|
||||
amountInBaseUnits: BigNumber;
|
||||
}
|
||||
|
||||
interface TokenSendCompletedState {}
|
||||
|
||||
export class TokenSendCompleted extends React.Component<TokenSendCompletedProps, TokenSendCompletedState> {
|
||||
public render() {
|
||||
const etherScanLink = !_.isUndefined(this.props.etherScanLinkIfExists) &&
|
||||
(
|
||||
<a
|
||||
style={{color: 'white'}}
|
||||
href={`${this.props.etherScanLinkIfExists}`}
|
||||
target="_blank"
|
||||
>
|
||||
Verify on Etherscan
|
||||
</a>
|
||||
);
|
||||
const amountInUnits = ZeroEx.toUnitAmount(this.props.amountInBaseUnits, this.props.token.decimals);
|
||||
const truncatedAddress = utils.getAddressBeginAndEnd(this.props.toAddress);
|
||||
return (
|
||||
<div>
|
||||
{`Sent ${amountInUnits} ${this.props.token.symbol} to ${truncatedAddress}: `}
|
||||
{etherScanLink}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as React from 'react';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
interface TransactionSubmittedProps {
|
||||
etherScanLinkIfExists?: string;
|
||||
}
|
||||
|
||||
interface TransactionSubmittedState {}
|
||||
|
||||
export class TransactionSubmitted extends React.Component<TransactionSubmittedProps, TransactionSubmittedState> {
|
||||
public render() {
|
||||
if (_.isUndefined(this.props.etherScanLinkIfExists)) {
|
||||
return <div>Transaction submitted to the network</div>;
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
Transaction submitted to the network:{' '}
|
||||
<a
|
||||
style={{color: 'white'}}
|
||||
href={`${this.props.etherScanLinkIfExists}`}
|
||||
target="_blank"
|
||||
>
|
||||
Verify on Etherscan
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
255
packages/website/ts/components/footer.tsx
Normal file
255
packages/website/ts/components/footer.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {HashLink} from 'react-router-hash-link';
|
||||
import {Styles, WebsitePaths} from 'ts/types';
|
||||
import {
|
||||
Link,
|
||||
} from 'react-router-dom';
|
||||
import {
|
||||
Link as ScrollLink,
|
||||
} from 'react-scroll';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
|
||||
interface MenuItemsBySection {
|
||||
[sectionName: string]: FooterMenuItem[];
|
||||
}
|
||||
|
||||
interface FooterMenuItem {
|
||||
title: string;
|
||||
path?: string;
|
||||
isExternal?: boolean;
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
enum Sections {
|
||||
Documentation = 'Documentation',
|
||||
Community = 'Community',
|
||||
Organization = 'Organization',
|
||||
}
|
||||
|
||||
const ICON_DIMENSION = 16;
|
||||
const CUSTOM_DARK_GRAY = '#393939';
|
||||
const CUSTOM_LIGHT_GRAY = '#CACACA';
|
||||
const CUSTOM_LIGHTEST_GRAY = '#9E9E9E';
|
||||
const menuItemsBySection: MenuItemsBySection = {
|
||||
Documentation: [
|
||||
{
|
||||
title: '0x.js',
|
||||
path: WebsitePaths.ZeroExJs,
|
||||
},
|
||||
{
|
||||
title: '0x Smart Contracts',
|
||||
path: WebsitePaths.SmartContracts,
|
||||
},
|
||||
{
|
||||
title: 'Whitepaper',
|
||||
path: WebsitePaths.Whitepaper,
|
||||
isExternal: true,
|
||||
},
|
||||
{
|
||||
title: 'Wiki',
|
||||
path: WebsitePaths.Wiki,
|
||||
},
|
||||
{
|
||||
title: 'FAQ',
|
||||
path: WebsitePaths.FAQ,
|
||||
},
|
||||
],
|
||||
Community: [
|
||||
{
|
||||
title: 'Rocket.chat',
|
||||
isExternal: true,
|
||||
path: constants.ZEROEX_CHAT_URL,
|
||||
fileName: 'rocketchat.png',
|
||||
},
|
||||
{
|
||||
title: 'Blog',
|
||||
isExternal: true,
|
||||
path: constants.BLOG_URL,
|
||||
fileName: 'medium.png',
|
||||
},
|
||||
{
|
||||
title: 'Twitter',
|
||||
isExternal: true,
|
||||
path: constants.TWITTER_URL,
|
||||
fileName: 'twitter.png',
|
||||
},
|
||||
{
|
||||
title: 'Reddit',
|
||||
isExternal: true,
|
||||
path: constants.REDDIT_URL,
|
||||
fileName: 'reddit.png',
|
||||
},
|
||||
],
|
||||
Organization: [
|
||||
{
|
||||
title: 'About',
|
||||
isExternal: false,
|
||||
path: WebsitePaths.About,
|
||||
},
|
||||
{
|
||||
title: 'Careers',
|
||||
isExternal: true,
|
||||
path: constants.ANGELLIST_URL,
|
||||
},
|
||||
{
|
||||
title: 'Contact',
|
||||
isExternal: true,
|
||||
path: 'mailto:team@0xproject.com',
|
||||
},
|
||||
],
|
||||
};
|
||||
const linkStyle = {
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
const titleToIcon: {[title: string]: string} = {
|
||||
'Rocket.chat': 'rocketchat.png',
|
||||
'Blog': 'medium.png',
|
||||
'Twitter': 'twitter.png',
|
||||
'Reddit': 'reddit.png',
|
||||
};
|
||||
|
||||
export interface FooterProps {
|
||||
location: Location;
|
||||
}
|
||||
|
||||
interface FooterState {}
|
||||
|
||||
export class Footer extends React.Component<FooterProps, FooterState> {
|
||||
public render() {
|
||||
return (
|
||||
<div className="relative pb4 pt2" style={{backgroundColor: CUSTOM_DARK_GRAY}}>
|
||||
<div className="mx-auto max-width-4 md-px2 lg-px0 py4 clearfix" style={{color: 'white'}}>
|
||||
<div className="col lg-col-4 md-col-4 col-12 left">
|
||||
<div className="sm-mx-auto" style={{width: 148}}>
|
||||
<div>
|
||||
<img src="/images/protocol_logo_white.png" height="30" />
|
||||
</div>
|
||||
<div style={{fontSize: 11, color: CUSTOM_LIGHTEST_GRAY, paddingLeft: 37, paddingTop: 2}}>
|
||||
© ZeroEx, Intl.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col lg-col-8 md-col-8 col-12 lg-pl4 md-pl4">
|
||||
<div className="col lg-col-4 md-col-4 col-12">
|
||||
<div className="lg-right md-right sm-center">
|
||||
{this.renderHeader(Sections.Documentation)}
|
||||
{_.map(menuItemsBySection[Sections.Documentation], this.renderMenuItem.bind(this))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col lg-col-4 md-col-4 col-12 lg-pr2 md-pr2">
|
||||
<div className="lg-right md-right sm-center">
|
||||
{this.renderHeader(Sections.Community)}
|
||||
{_.map(menuItemsBySection[Sections.Community], this.renderMenuItem.bind(this))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col lg-col-4 md-col-4 col-12">
|
||||
<div className="lg-right md-right sm-center">
|
||||
{this.renderHeader(Sections.Organization)}
|
||||
{_.map(menuItemsBySection[Sections.Organization], this.renderMenuItem.bind(this))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderIcon(fileName: string) {
|
||||
return (
|
||||
<div style={{height: ICON_DIMENSION, width: ICON_DIMENSION}}>
|
||||
<img src={`/images/social/${fileName}`} style={{width: ICON_DIMENSION}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderMenuItem(item: FooterMenuItem) {
|
||||
const iconIfExists = titleToIcon[item.title];
|
||||
return (
|
||||
<div
|
||||
key={item.title}
|
||||
className="sm-center"
|
||||
style={{fontSize: 13, paddingTop: 25}}
|
||||
>
|
||||
{item.isExternal ?
|
||||
<a
|
||||
className="text-decoration-none"
|
||||
style={linkStyle}
|
||||
target="_blank"
|
||||
href={item.path}
|
||||
>
|
||||
{!_.isUndefined(iconIfExists) ?
|
||||
<div className="sm-mx-auto" style={{width: 65}}>
|
||||
<div className="flex">
|
||||
<div className="pr1">
|
||||
{this.renderIcon(iconIfExists)}
|
||||
</div>
|
||||
<div>{item.title}</div>
|
||||
</div>
|
||||
</div> :
|
||||
item.title
|
||||
}
|
||||
</a> :
|
||||
<Link
|
||||
to={item.path}
|
||||
style={linkStyle}
|
||||
className="text-decoration-none"
|
||||
>
|
||||
<div>
|
||||
{!_.isUndefined(iconIfExists) &&
|
||||
<div className="pr1">
|
||||
{this.renderIcon(iconIfExists)}
|
||||
</div>
|
||||
}
|
||||
{item.title}
|
||||
</div>
|
||||
</Link>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderHeader(title: string) {
|
||||
const headerStyle = {
|
||||
textTransform: 'uppercase',
|
||||
color: CUSTOM_LIGHT_GRAY,
|
||||
letterSpacing: 2,
|
||||
fontFamily: 'Roboto Mono',
|
||||
fontSize: 13,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className="lg-pb2 md-pb2 sm-pt4"
|
||||
style={headerStyle}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderHomepageLink(title: string) {
|
||||
const hash = title.toLowerCase();
|
||||
if (this.props.location.pathname === WebsitePaths.Home) {
|
||||
return (
|
||||
<ScrollLink
|
||||
style={linkStyle}
|
||||
to={hash}
|
||||
smooth={true}
|
||||
offset={0}
|
||||
duration={constants.HOME_SCROLL_DURATION_MS}
|
||||
containerId="home"
|
||||
>
|
||||
{title}
|
||||
</ScrollLink>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<HashLink
|
||||
to={`/#${hash}`}
|
||||
className="text-decoration-none"
|
||||
style={linkStyle}
|
||||
>
|
||||
{title}
|
||||
</HashLink>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
291
packages/website/ts/components/generate_order/asset_picker.tsx
Normal file
291
packages/website/ts/components/generate_order/asset_picker.tsx
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
237
packages/website/ts/components/generate_order/new_token_form.tsx
Normal file
237
packages/website/ts/components/generate_order/new_token_form.tsx
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
74
packages/website/ts/components/inputs/address_input.tsx
Normal file
74
packages/website/ts/components/inputs/address_input.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {isAddress} from 'ethereum-address';
|
||||
import TextField from 'material-ui/TextField';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {Blockchain} from 'ts/blockchain';
|
||||
import {RequiredLabel} from 'ts/components/ui/required_label';
|
||||
|
||||
interface AddressInputProps {
|
||||
disabled?: boolean;
|
||||
initialAddress: string;
|
||||
isRequired?: boolean;
|
||||
hintText?: string;
|
||||
shouldHideLabel?: boolean;
|
||||
label?: string;
|
||||
shouldShowIncompleteErrs?: boolean;
|
||||
updateAddress: (address?: string) => void;
|
||||
}
|
||||
|
||||
interface AddressInputState {
|
||||
address: string;
|
||||
errMsg: string;
|
||||
}
|
||||
|
||||
export class AddressInput extends React.Component<AddressInputProps, AddressInputState> {
|
||||
constructor(props: AddressInputProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
address: this.props.initialAddress,
|
||||
errMsg: '',
|
||||
};
|
||||
}
|
||||
public componentWillReceiveProps(nextProps: AddressInputProps) {
|
||||
if (nextProps.shouldShowIncompleteErrs && this.props.isRequired &&
|
||||
this.state.address === '') {
|
||||
this.setState({
|
||||
errMsg: 'Address is required',
|
||||
});
|
||||
}
|
||||
}
|
||||
public render() {
|
||||
const label = this.props.isRequired ? <RequiredLabel label={this.props.label} /> :
|
||||
this.props.label;
|
||||
const labelDisplay = this.props.shouldHideLabel ? 'hidden' : 'block';
|
||||
const hintText = this.props.hintText ? this.props.hintText : '';
|
||||
return (
|
||||
<div className="overflow-hidden">
|
||||
<TextField
|
||||
id={`address-field-${this.props.label}`}
|
||||
disabled={_.isUndefined(this.props.disabled) ? false : this.props.disabled}
|
||||
fullWidth={true}
|
||||
hintText={hintText}
|
||||
floatingLabelFixed={true}
|
||||
floatingLabelStyle={{color: colors.grey500, display: labelDisplay}}
|
||||
floatingLabelText={label}
|
||||
errorText={this.state.errMsg}
|
||||
value={this.state.address}
|
||||
onChange={this.onOrderTakerAddressUpdated.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private onOrderTakerAddressUpdated(e: any) {
|
||||
const address = e.target.value.toLowerCase();
|
||||
const isValidAddress = isAddress(address) || address === '';
|
||||
const errMsg = isValidAddress ? '' : 'Invalid ethereum address';
|
||||
this.setState({
|
||||
address,
|
||||
errMsg,
|
||||
});
|
||||
const addressIfValid = isValidAddress ? address : undefined;
|
||||
this.props.updateAddress(addressIfValid);
|
||||
}
|
||||
}
|
||||
94
packages/website/ts/components/inputs/allowance_toggle.tsx
Normal file
94
packages/website/ts/components/inputs/allowance_toggle.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import Toggle from 'material-ui/Toggle';
|
||||
import {Blockchain} from 'ts/blockchain';
|
||||
import {Dispatcher} from 'ts/redux/dispatcher';
|
||||
import {Token, TokenState, BalanceErrs} from 'ts/types';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {errorReporter} from 'ts/utils/error_reporter';
|
||||
|
||||
const DEFAULT_ALLOWANCE_AMOUNT_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1);
|
||||
|
||||
interface AllowanceToggleProps {
|
||||
blockchain: Blockchain;
|
||||
dispatcher: Dispatcher;
|
||||
onErrorOccurred: (errType: BalanceErrs) => void;
|
||||
token: Token;
|
||||
tokenState: TokenState;
|
||||
userAddress: string;
|
||||
}
|
||||
|
||||
interface AllowanceToggleState {
|
||||
isSpinnerVisible: boolean;
|
||||
prevAllowance: BigNumber;
|
||||
}
|
||||
|
||||
export class AllowanceToggle extends React.Component<AllowanceToggleProps, AllowanceToggleState> {
|
||||
constructor(props: AllowanceToggleProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isSpinnerVisible: false,
|
||||
prevAllowance: props.tokenState.allowance,
|
||||
};
|
||||
}
|
||||
public componentWillReceiveProps(nextProps: AllowanceToggleProps) {
|
||||
if (!nextProps.tokenState.allowance.eq(this.state.prevAllowance)) {
|
||||
this.setState({
|
||||
isSpinnerVisible: false,
|
||||
prevAllowance: nextProps.tokenState.allowance,
|
||||
});
|
||||
}
|
||||
}
|
||||
public render() {
|
||||
return (
|
||||
<div className="flex">
|
||||
<div>
|
||||
<Toggle
|
||||
disabled={this.state.isSpinnerVisible}
|
||||
toggled={this.isAllowanceSet()}
|
||||
onToggle={this.onToggleAllowanceAsync.bind(this, this.props.token)}
|
||||
/>
|
||||
</div>
|
||||
{this.state.isSpinnerVisible &&
|
||||
<div className="pl1" style={{paddingTop: 3}}>
|
||||
<i className="zmdi zmdi-spinner zmdi-hc-spin" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private async onToggleAllowanceAsync() {
|
||||
if (this.props.userAddress === '') {
|
||||
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isSpinnerVisible: true,
|
||||
});
|
||||
|
||||
let newAllowanceAmountInBaseUnits = new BigNumber(0);
|
||||
if (!this.isAllowanceSet()) {
|
||||
newAllowanceAmountInBaseUnits = DEFAULT_ALLOWANCE_AMOUNT_IN_BASE_UNITS;
|
||||
}
|
||||
try {
|
||||
await this.props.blockchain.setProxyAllowanceAsync(this.props.token, newAllowanceAmountInBaseUnits);
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
isSpinnerVisible: false,
|
||||
});
|
||||
const errMsg = '' + err;
|
||||
if (_.includes(errMsg, 'User denied transaction')) {
|
||||
return false;
|
||||
}
|
||||
utils.consoleLog(`Unexpected error encountered: ${err}`);
|
||||
utils.consoleLog(err.stack);
|
||||
await errorReporter.reportAsync(err);
|
||||
this.props.onErrorOccurred(BalanceErrs.allowanceSettingFailed);
|
||||
}
|
||||
}
|
||||
private isAllowanceSet() {
|
||||
return !this.props.tokenState.allowance.eq(0);
|
||||
}
|
||||
}
|
||||
160
packages/website/ts/components/inputs/balance_bounded_input.tsx
Normal file
160
packages/website/ts/components/inputs/balance_bounded_input.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {ValidatedBigNumberCallback, InputErrMsg, WebsitePaths} from 'ts/types';
|
||||
import TextField from 'material-ui/TextField';
|
||||
import {RequiredLabel} from 'ts/components/ui/required_label';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
interface BalanceBoundedInputProps {
|
||||
label?: string;
|
||||
balance: BigNumber;
|
||||
amount?: BigNumber;
|
||||
onChange: ValidatedBigNumberCallback;
|
||||
shouldShowIncompleteErrs?: boolean;
|
||||
shouldCheckBalance: boolean;
|
||||
validate?: (amount: BigNumber) => InputErrMsg;
|
||||
onVisitBalancesPageClick?: () => void;
|
||||
shouldHideVisitBalancesLink?: boolean;
|
||||
}
|
||||
|
||||
interface BalanceBoundedInputState {
|
||||
errMsg: InputErrMsg;
|
||||
amountString: string;
|
||||
}
|
||||
|
||||
export class BalanceBoundedInput extends
|
||||
React.Component<BalanceBoundedInputProps, BalanceBoundedInputState> {
|
||||
public static defaultProps: Partial<BalanceBoundedInputProps> = {
|
||||
shouldShowIncompleteErrs: false,
|
||||
shouldHideVisitBalancesLink: false,
|
||||
};
|
||||
constructor(props: BalanceBoundedInputProps) {
|
||||
super(props);
|
||||
const amountString = this.props.amount ? this.props.amount.toString() : '';
|
||||
this.state = {
|
||||
errMsg: this.validate(amountString, props.balance),
|
||||
amountString,
|
||||
};
|
||||
}
|
||||
public componentWillReceiveProps(nextProps: BalanceBoundedInputProps) {
|
||||
if (nextProps === this.props) {
|
||||
return;
|
||||
}
|
||||
const isCurrentAmountNumeric = utils.isNumeric(this.state.amountString);
|
||||
if (!_.isUndefined(nextProps.amount)) {
|
||||
let shouldResetState = false;
|
||||
if (!isCurrentAmountNumeric) {
|
||||
shouldResetState = true;
|
||||
} else {
|
||||
const currentAmount = new BigNumber(this.state.amountString);
|
||||
if (!currentAmount.eq(nextProps.amount) || !nextProps.balance.eq(this.props.balance)) {
|
||||
shouldResetState = true;
|
||||
}
|
||||
}
|
||||
if (shouldResetState) {
|
||||
const amountString = nextProps.amount.toString();
|
||||
this.setState({
|
||||
errMsg: this.validate(amountString, nextProps.balance),
|
||||
amountString,
|
||||
});
|
||||
}
|
||||
} else if (isCurrentAmountNumeric) {
|
||||
const amountString = '';
|
||||
this.setState({
|
||||
errMsg: this.validate(amountString, nextProps.balance),
|
||||
amountString,
|
||||
});
|
||||
}
|
||||
}
|
||||
public render() {
|
||||
let errorText = this.state.errMsg;
|
||||
if (this.props.shouldShowIncompleteErrs && this.state.amountString === '') {
|
||||
errorText = 'This field is required';
|
||||
}
|
||||
let label: React.ReactNode|string = '';
|
||||
if (!_.isUndefined(this.props.label)) {
|
||||
label = <RequiredLabel label={this.props.label}/>;
|
||||
}
|
||||
return (
|
||||
<TextField
|
||||
fullWidth={true}
|
||||
floatingLabelText={label}
|
||||
floatingLabelFixed={true}
|
||||
floatingLabelStyle={{color: colors.grey500, width: 206}}
|
||||
errorText={errorText}
|
||||
value={this.state.amountString}
|
||||
hintText={<span style={{textTransform: 'capitalize'}}>amount</span>}
|
||||
onChange={this.onValueChange.bind(this)}
|
||||
underlineStyle={{width: 'calc(100% + 50px)'}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
private onValueChange(e: any, amountString: string) {
|
||||
const errMsg = this.validate(amountString, this.props.balance);
|
||||
this.setState({
|
||||
amountString,
|
||||
errMsg,
|
||||
}, () => {
|
||||
const isValid = _.isUndefined(errMsg);
|
||||
if (utils.isNumeric(amountString)) {
|
||||
this.props.onChange(isValid, new BigNumber(amountString));
|
||||
} else {
|
||||
this.props.onChange(isValid);
|
||||
}
|
||||
});
|
||||
}
|
||||
private validate(amountString: string, balance: BigNumber): InputErrMsg {
|
||||
if (!utils.isNumeric(amountString)) {
|
||||
return amountString !== '' ? 'Must be a number' : '';
|
||||
}
|
||||
const amount = new BigNumber(amountString);
|
||||
if (amount.eq(0)) {
|
||||
return 'Cannot be zero';
|
||||
}
|
||||
if (this.props.shouldCheckBalance && amount.gt(balance)) {
|
||||
return (
|
||||
<span>
|
||||
Insufficient balance.{' '}
|
||||
{this.renderIncreaseBalanceLink()}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const errMsg = _.isUndefined(this.props.validate) ? undefined : this.props.validate(amount);
|
||||
return errMsg;
|
||||
}
|
||||
private renderIncreaseBalanceLink() {
|
||||
if (this.props.shouldHideVisitBalancesLink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const increaseBalanceText = 'Increase balance';
|
||||
const linkStyle = {
|
||||
cursor: 'pointer',
|
||||
color: colors.grey900,
|
||||
textDecoration: 'underline',
|
||||
display: 'inline',
|
||||
};
|
||||
if (_.isUndefined(this.props.onVisitBalancesPageClick)) {
|
||||
return (
|
||||
<Link
|
||||
to={`${WebsitePaths.Portal}/balances`}
|
||||
style={linkStyle}
|
||||
>
|
||||
{increaseBalanceText}
|
||||
</Link>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div
|
||||
onClick={this.props.onVisitBalancesPageClick}
|
||||
style={linkStyle}
|
||||
>
|
||||
{increaseBalanceText}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
packages/website/ts/components/inputs/eth_amount_input.tsx
Normal file
51
packages/website/ts/components/inputs/eth_amount_input.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {ZeroEx} from '0x.js';
|
||||
import {ValidatedBigNumberCallback} from 'ts/types';
|
||||
import {BalanceBoundedInput} from 'ts/components/inputs/balance_bounded_input';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
|
||||
interface EthAmountInputProps {
|
||||
label?: string;
|
||||
balance: BigNumber;
|
||||
amount?: BigNumber;
|
||||
onChange: ValidatedBigNumberCallback;
|
||||
shouldShowIncompleteErrs: boolean;
|
||||
onVisitBalancesPageClick?: () => void;
|
||||
shouldCheckBalance: boolean;
|
||||
shouldHideVisitBalancesLink?: boolean;
|
||||
}
|
||||
|
||||
interface EthAmountInputState {}
|
||||
|
||||
export class EthAmountInput extends React.Component<EthAmountInputProps, EthAmountInputState> {
|
||||
public render() {
|
||||
const amount = this.props.amount ?
|
||||
ZeroEx.toUnitAmount(this.props.amount, constants.ETH_DECIMAL_PLACES) :
|
||||
undefined;
|
||||
return (
|
||||
<div className="flex overflow-hidden" style={{height: 63}}>
|
||||
<BalanceBoundedInput
|
||||
label={this.props.label}
|
||||
balance={this.props.balance}
|
||||
amount={amount}
|
||||
onChange={this.onChange.bind(this)}
|
||||
shouldCheckBalance={this.props.shouldCheckBalance}
|
||||
shouldShowIncompleteErrs={this.props.shouldShowIncompleteErrs}
|
||||
onVisitBalancesPageClick={this.props.onVisitBalancesPageClick}
|
||||
shouldHideVisitBalancesLink={this.props.shouldHideVisitBalancesLink}
|
||||
/>
|
||||
<div style={{paddingTop: _.isUndefined(this.props.label) ? 15 : 40}}>
|
||||
ETH
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private onChange(isValid: boolean, amount?: BigNumber) {
|
||||
const baseUnitAmountIfExists = _.isUndefined(amount) ?
|
||||
undefined :
|
||||
ZeroEx.toBaseUnitAmount(amount, constants.ETH_DECIMAL_PLACES);
|
||||
this.props.onChange(isValid, baseUnitAmountIfExists);
|
||||
}
|
||||
}
|
||||
108
packages/website/ts/components/inputs/expiration_input.tsx
Normal file
108
packages/website/ts/components/inputs/expiration_input.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import * as React from 'react';
|
||||
import * as _ from 'lodash';
|
||||
import DatePicker from 'material-ui/DatePicker';
|
||||
import TimePicker from 'material-ui/TimePicker';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import * as moment from 'moment';
|
||||
|
||||
interface ExpirationInputProps {
|
||||
orderExpiryTimestamp: BigNumber;
|
||||
updateOrderExpiry: (unixTimestampSec: BigNumber) => void;
|
||||
}
|
||||
|
||||
interface ExpirationInputState {
|
||||
dateMoment: moment.Moment;
|
||||
timeMoment: moment.Moment;
|
||||
}
|
||||
|
||||
export class ExpirationInput extends React.Component<ExpirationInputProps, ExpirationInputState> {
|
||||
private earliestPickableMoment: moment.Moment;
|
||||
constructor(props: ExpirationInputProps) {
|
||||
super(props);
|
||||
// Set the earliest pickable date to today at 00:00, so users can only pick the current or later dates
|
||||
this.earliestPickableMoment = moment().startOf('day');
|
||||
const expirationMoment = utils.convertToMomentFromUnixTimestamp(props.orderExpiryTimestamp);
|
||||
const initialOrderExpiryTimestamp = utils.initialOrderExpiryUnixTimestampSec();
|
||||
const didUserSetExpiry = !initialOrderExpiryTimestamp.eq(props.orderExpiryTimestamp);
|
||||
this.state = {
|
||||
dateMoment: didUserSetExpiry ? expirationMoment : undefined,
|
||||
timeMoment: didUserSetExpiry ? expirationMoment : undefined,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const date = this.state.dateMoment ? this.state.dateMoment.toDate() : undefined;
|
||||
const time = this.state.timeMoment ? this.state.timeMoment.toDate() : undefined;
|
||||
return (
|
||||
<div className="clearfix">
|
||||
<div className="col col-6 overflow-hidden pr3 flex relative">
|
||||
<DatePicker
|
||||
className="overflow-hidden"
|
||||
hintText="Date"
|
||||
mode="landscape"
|
||||
autoOk={true}
|
||||
value={date}
|
||||
onChange={this.onDateChanged.bind(this)}
|
||||
shouldDisableDate={this.shouldDisableDate.bind(this)}
|
||||
/>
|
||||
<div
|
||||
className="absolute"
|
||||
style={{fontSize: 20, right: 40, top: 13, pointerEvents: 'none'}}
|
||||
>
|
||||
<i className="zmdi zmdi-calendar" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col col-5 overflow-hidden flex relative">
|
||||
<TimePicker
|
||||
className="overflow-hidden"
|
||||
hintText="Time"
|
||||
autoOk={true}
|
||||
value={time}
|
||||
onChange={this.onTimeChanged.bind(this)}
|
||||
/>
|
||||
<div
|
||||
className="absolute"
|
||||
style={{fontSize: 20, right: 9, top: 13, pointerEvents: 'none'}}
|
||||
>
|
||||
<i className="zmdi zmdi-time" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={this.clearDates.bind(this)}
|
||||
className="col col-1 pt2"
|
||||
style={{textAlign: 'right'}}
|
||||
>
|
||||
<i style={{fontSize: 16, cursor: 'pointer'}} className="zmdi zmdi-close" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private shouldDisableDate(date: Date): boolean {
|
||||
return moment(date).startOf('day').isBefore(this.earliestPickableMoment);
|
||||
}
|
||||
private clearDates() {
|
||||
this.setState({
|
||||
dateMoment: undefined,
|
||||
timeMoment: undefined,
|
||||
});
|
||||
const defaultDateTime = utils.initialOrderExpiryUnixTimestampSec();
|
||||
this.props.updateOrderExpiry(defaultDateTime);
|
||||
}
|
||||
private onDateChanged(e: any, date: Date) {
|
||||
const dateMoment = moment(date);
|
||||
this.setState({
|
||||
dateMoment,
|
||||
});
|
||||
const timestamp = utils.convertToUnixTimestampSeconds(dateMoment, this.state.timeMoment);
|
||||
this.props.updateOrderExpiry(timestamp);
|
||||
}
|
||||
private onTimeChanged(e: any, time: Date) {
|
||||
const timeMoment = moment(time);
|
||||
this.setState({
|
||||
timeMoment,
|
||||
});
|
||||
const dateMoment = _.isUndefined(this.state.dateMoment) ? moment() : this.state.dateMoment;
|
||||
const timestamp = utils.convertToUnixTimestampSeconds(dateMoment, timeMoment);
|
||||
this.props.updateOrderExpiry(timestamp);
|
||||
}
|
||||
}
|
||||
65
packages/website/ts/components/inputs/hash_input.tsx
Normal file
65
packages/website/ts/components/inputs/hash_input.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import * as React from 'react';
|
||||
import {Blockchain} from 'ts/blockchain';
|
||||
import {ZeroEx, Order} from '0x.js';
|
||||
import {FakeTextField} from 'ts/components/ui/fake_text_field';
|
||||
import ReactTooltip = require('react-tooltip');
|
||||
import {HashData, Styles} from 'ts/types';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
|
||||
const styles: Styles = {
|
||||
textField: {
|
||||
overflow: 'hidden',
|
||||
paddingTop: 8,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
};
|
||||
|
||||
interface HashInputProps {
|
||||
blockchain: Blockchain;
|
||||
blockchainIsLoaded: boolean;
|
||||
hashData: HashData;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface HashInputState {}
|
||||
|
||||
export class HashInput extends React.Component<HashInputProps, HashInputState> {
|
||||
public render() {
|
||||
const msgHashHex = this.props.blockchainIsLoaded ? this.generateMessageHashHex() : '';
|
||||
return (
|
||||
<div>
|
||||
<FakeTextField label={this.props.label}>
|
||||
<div
|
||||
style={styles.textField}
|
||||
data-tip={true}
|
||||
data-for="hashTooltip"
|
||||
>
|
||||
{msgHashHex}
|
||||
</div>
|
||||
</FakeTextField>
|
||||
<ReactTooltip id="hashTooltip">{msgHashHex}</ReactTooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private generateMessageHashHex() {
|
||||
const exchangeContractAddress = this.props.blockchain.getExchangeContractAddressIfExists();
|
||||
const hashData = this.props.hashData;
|
||||
const order: Order = {
|
||||
exchangeContractAddress,
|
||||
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(order);
|
||||
return orderHash;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {Blockchain} from 'ts/blockchain';
|
||||
import {Identicon} from 'ts/components/ui/identicon';
|
||||
import {RequiredLabel} from 'ts/components/ui/required_label';
|
||||
import {AddressInput} from 'ts/components/inputs/address_input';
|
||||
import {InputLabel} from 'ts/components/ui/input_label';
|
||||
|
||||
interface IdenticonAddressInputProps {
|
||||
initialAddress: string;
|
||||
isRequired?: boolean;
|
||||
label: string;
|
||||
updateOrderAddress: (address?: string) => void;
|
||||
}
|
||||
|
||||
interface IdenticonAddressInputState {
|
||||
address: string;
|
||||
}
|
||||
|
||||
export class IdenticonAddressInput extends React.Component<IdenticonAddressInputProps, IdenticonAddressInputState> {
|
||||
constructor(props: IdenticonAddressInputProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
address: props.initialAddress,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const label = this.props.isRequired ? <RequiredLabel label={this.props.label} /> :
|
||||
this.props.label;
|
||||
return (
|
||||
<div className="relative" style={{width: '100%'}}>
|
||||
<InputLabel text={label} />
|
||||
<div className="flex">
|
||||
<div className="col col-1 pb1 pr1" style={{paddingTop: 13}}>
|
||||
<Identicon address={this.state.address} diameter={26} />
|
||||
</div>
|
||||
<div className="col col-11 pb1 pl1" style={{height: 65}}>
|
||||
<AddressInput
|
||||
hintText="e.g 0x75bE4F78AA3699B3A348c84bDB2a96c3Db..."
|
||||
shouldHideLabel={true}
|
||||
initialAddress={this.props.initialAddress}
|
||||
updateAddress={this.updateAddress.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private updateAddress(address?: string): void {
|
||||
this.setState({
|
||||
address,
|
||||
});
|
||||
this.props.updateOrderAddress(address);
|
||||
}
|
||||
}
|
||||
69
packages/website/ts/components/inputs/token_amount_input.tsx
Normal file
69
packages/website/ts/components/inputs/token_amount_input.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import * as React from 'react';
|
||||
import * as _ from 'lodash';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {ZeroEx} from '0x.js';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {Token, TokenState, InputErrMsg, ValidatedBigNumberCallback, WebsitePaths} from 'ts/types';
|
||||
import {BalanceBoundedInput} from 'ts/components/inputs/balance_bounded_input';
|
||||
|
||||
interface TokenAmountInputProps {
|
||||
label: string;
|
||||
token: Token;
|
||||
tokenState: TokenState;
|
||||
amount?: BigNumber;
|
||||
shouldShowIncompleteErrs: boolean;
|
||||
shouldCheckBalance: boolean;
|
||||
shouldCheckAllowance: boolean;
|
||||
onChange: ValidatedBigNumberCallback;
|
||||
onVisitBalancesPageClick?: () => void;
|
||||
}
|
||||
|
||||
interface TokenAmountInputState {}
|
||||
|
||||
export class TokenAmountInput extends React.Component<TokenAmountInputProps, TokenAmountInputState> {
|
||||
public render() {
|
||||
const amount = this.props.amount ?
|
||||
ZeroEx.toUnitAmount(this.props.amount, this.props.token.decimals) :
|
||||
undefined;
|
||||
return (
|
||||
<div className="flex overflow-hidden" style={{height: 84}}>
|
||||
<BalanceBoundedInput
|
||||
label={this.props.label}
|
||||
amount={amount}
|
||||
balance={ZeroEx.toUnitAmount(this.props.tokenState.balance, this.props.token.decimals)}
|
||||
onChange={this.onChange.bind(this)}
|
||||
validate={this.validate.bind(this)}
|
||||
shouldCheckBalance={this.props.shouldCheckBalance}
|
||||
shouldShowIncompleteErrs={this.props.shouldShowIncompleteErrs}
|
||||
onVisitBalancesPageClick={this.props.onVisitBalancesPageClick}
|
||||
/>
|
||||
<div style={{paddingTop: 39}}>
|
||||
{this.props.token.symbol}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private onChange(isValid: boolean, amount?: BigNumber) {
|
||||
let baseUnitAmount;
|
||||
if (!_.isUndefined(amount)) {
|
||||
baseUnitAmount = ZeroEx.toBaseUnitAmount(amount, this.props.token.decimals);
|
||||
}
|
||||
this.props.onChange(isValid, baseUnitAmount);
|
||||
}
|
||||
private validate(amount: BigNumber): InputErrMsg {
|
||||
if (this.props.shouldCheckAllowance && amount.gt(this.props.tokenState.allowance)) {
|
||||
return (
|
||||
<span>
|
||||
Insufficient allowance.{' '}
|
||||
<Link
|
||||
to={`${WebsitePaths.Portal}/balances`}
|
||||
style={{cursor: 'pointer', color: colors.grey900}}
|
||||
>
|
||||
Set allowance
|
||||
</Link>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
107
packages/website/ts/components/inputs/token_input.tsx
Normal file
107
packages/website/ts/components/inputs/token_input.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import Paper from 'material-ui/Paper';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {Blockchain} from 'ts/blockchain';
|
||||
import {Dispatcher} from 'ts/redux/dispatcher';
|
||||
import {AssetToken, Side, TokenByAddress, BlockchainErrs, Token, TokenState} from 'ts/types';
|
||||
import {AssetPicker} from 'ts/components/generate_order/asset_picker';
|
||||
import {InputLabel} from 'ts/components/ui/input_label';
|
||||
import {TokenIcon} from 'ts/components/ui/token_icon';
|
||||
|
||||
const TOKEN_ICON_DIMENSION = 80;
|
||||
|
||||
interface TokenInputProps {
|
||||
blockchain: Blockchain;
|
||||
blockchainErr: BlockchainErrs;
|
||||
dispatcher: Dispatcher;
|
||||
label: string;
|
||||
side: Side;
|
||||
networkId: number;
|
||||
assetToken: AssetToken;
|
||||
updateChosenAssetToken: (side: Side, token: AssetToken) => void;
|
||||
tokenByAddress: TokenByAddress;
|
||||
userAddress: string;
|
||||
}
|
||||
|
||||
interface TokenInputState {
|
||||
isHoveringIcon: boolean;
|
||||
isPickerOpen: boolean;
|
||||
trackCandidateTokenIfExists?: Token;
|
||||
}
|
||||
|
||||
export class TokenInput extends React.Component<TokenInputProps, TokenInputState> {
|
||||
constructor(props: TokenInputProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isHoveringIcon: false,
|
||||
isPickerOpen: false,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const token = this.props.tokenByAddress[this.props.assetToken.address];
|
||||
const iconStyles = {
|
||||
cursor: 'pointer',
|
||||
opacity: this.state.isHoveringIcon ? 0.5 : 1,
|
||||
};
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="pb1">
|
||||
<InputLabel text={this.props.label} />
|
||||
</div>
|
||||
<Paper
|
||||
zDepth={1}
|
||||
style={{cursor: 'pointer'}}
|
||||
onMouseEnter={this.onToggleHover.bind(this, true)}
|
||||
onMouseLeave={this.onToggleHover.bind(this, false)}
|
||||
onClick={this.onAssetClicked.bind(this)}
|
||||
>
|
||||
<div
|
||||
className="mx-auto pt2"
|
||||
style={{width: TOKEN_ICON_DIMENSION, ...iconStyles}}
|
||||
>
|
||||
<TokenIcon token={token} diameter={TOKEN_ICON_DIMENSION} />
|
||||
</div>
|
||||
<div className="py1 center" style={{color: colors.grey500}}>
|
||||
{token.name}
|
||||
</div>
|
||||
</Paper>
|
||||
<AssetPicker
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
blockchain={this.props.blockchain}
|
||||
dispatcher={this.props.dispatcher}
|
||||
isOpen={this.state.isPickerOpen}
|
||||
currentTokenAddress={this.props.assetToken.address}
|
||||
onTokenChosen={this.onTokenChosen.bind(this)}
|
||||
tokenByAddress={this.props.tokenByAddress}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private onTokenChosen(tokenAddress: string) {
|
||||
const assetToken: AssetToken = {
|
||||
address: tokenAddress,
|
||||
amount: this.props.assetToken.amount,
|
||||
};
|
||||
this.props.updateChosenAssetToken(this.props.side, assetToken);
|
||||
this.setState({
|
||||
isPickerOpen: false,
|
||||
});
|
||||
}
|
||||
private onToggleHover(isHoveringIcon: boolean) {
|
||||
this.setState({
|
||||
isHoveringIcon,
|
||||
});
|
||||
}
|
||||
private onAssetClicked() {
|
||||
if (this.props.blockchainErr !== '') {
|
||||
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isPickerOpen: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
164
packages/website/ts/components/order_json.tsx
Normal file
164
packages/website/ts/components/order_json.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {configs} from 'ts/utils/configs';
|
||||
import TextField from 'material-ui/TextField';
|
||||
import Paper from 'material-ui/Paper';
|
||||
import {CopyIcon} from 'ts/components/ui/copy_icon';
|
||||
import {SideToAssetToken, SignatureData, Order, TokenByAddress, WebsitePaths} from 'ts/types';
|
||||
import {errorReporter} from 'ts/utils/error_reporter';
|
||||
|
||||
interface OrderJSONProps {
|
||||
exchangeContractIfExists: string;
|
||||
orderExpiryTimestamp: BigNumber;
|
||||
orderSignatureData: SignatureData;
|
||||
orderTakerAddress: string;
|
||||
orderMakerAddress: string;
|
||||
orderSalt: BigNumber;
|
||||
orderMakerFee: BigNumber;
|
||||
orderTakerFee: BigNumber;
|
||||
orderFeeRecipient: string;
|
||||
networkId: number;
|
||||
sideToAssetToken: SideToAssetToken;
|
||||
tokenByAddress: TokenByAddress;
|
||||
}
|
||||
|
||||
interface OrderJSONState {
|
||||
shareLink: string;
|
||||
}
|
||||
|
||||
export class OrderJSON extends React.Component<OrderJSONProps, OrderJSONState> {
|
||||
constructor(props: OrderJSONProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
shareLink: '',
|
||||
};
|
||||
this.setShareLinkAsync();
|
||||
}
|
||||
public render() {
|
||||
const order = utils.generateOrder(this.props.networkId, this.props.exchangeContractIfExists,
|
||||
this.props.sideToAssetToken, this.props.orderExpiryTimestamp,
|
||||
this.props.orderTakerAddress, this.props.orderMakerAddress,
|
||||
this.props.orderMakerFee, this.props.orderTakerFee,
|
||||
this.props.orderFeeRecipient, this.props.orderSignatureData,
|
||||
this.props.tokenByAddress, this.props.orderSalt);
|
||||
const orderJSON = JSON.stringify(order);
|
||||
return (
|
||||
<div>
|
||||
<div className="pb2">
|
||||
You have successfully generated and cryptographically signed an order! The{' '}
|
||||
following JSON contains the order parameters and cryptographic signature that{' '}
|
||||
your counterparty will need to execute a trade with you.
|
||||
</div>
|
||||
<div className="pb2 flex">
|
||||
<div
|
||||
className="inline-block pl1"
|
||||
style={{top: 1}}
|
||||
>
|
||||
<CopyIcon data={orderJSON} callToAction="Copy" />
|
||||
</div>
|
||||
</div>
|
||||
<Paper className="center overflow-hidden">
|
||||
<TextField
|
||||
id="orderJSON"
|
||||
style={{width: 710}}
|
||||
value={JSON.stringify(order, null, '\t')}
|
||||
multiLine={true}
|
||||
rows={2}
|
||||
rowsMax={8}
|
||||
underlineStyle={{display: 'none'}}
|
||||
/>
|
||||
</Paper>
|
||||
<div className="pt3 pb2 center">
|
||||
<div>
|
||||
Share your signed order!
|
||||
</div>
|
||||
<div>
|
||||
<div className="mx-auto overflow-hidden" style={{width: 152}}>
|
||||
<TextField
|
||||
id={`${this.state.shareLink}-bitly`}
|
||||
value={this.state.shareLink}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto pt1 flex" style={{width: 91}}>
|
||||
<div>
|
||||
<i
|
||||
style={{cursor: 'pointer', fontSize: 29}}
|
||||
onClick={this.shareViaFacebook.bind(this)}
|
||||
className="zmdi zmdi-facebook-box"
|
||||
/>
|
||||
</div>
|
||||
<div className="pl1" style={{position: 'relative', width: 28}}>
|
||||
<i
|
||||
style={{cursor: 'pointer', fontSize: 32, position: 'absolute', top: -2, left: 8}}
|
||||
onClick={this.shareViaEmailAsync.bind(this)}
|
||||
className="zmdi zmdi-email"
|
||||
/>
|
||||
</div>
|
||||
<div className="pl1">
|
||||
<i
|
||||
style={{cursor: 'pointer', fontSize: 29}}
|
||||
onClick={this.shareViaTwitterAsync.bind(this)}
|
||||
className="zmdi zmdi-twitter-box"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private async shareViaTwitterAsync() {
|
||||
const tweetText = encodeURIComponent(`Fill my order using the 0x protocol: ${this.state.shareLink}`);
|
||||
window.open(`https://twitter.com/intent/tweet?text=${tweetText}`, 'Share your order', 'width=500,height=400');
|
||||
}
|
||||
private async shareViaFacebook() {
|
||||
(window as any).FB.ui({
|
||||
display: 'popup',
|
||||
href: this.state.shareLink,
|
||||
method: 'share',
|
||||
}, _.noop);
|
||||
}
|
||||
private async shareViaEmailAsync() {
|
||||
const encodedSubject = encodeURIComponent('Let\'s trade using the 0x protocol');
|
||||
const encodedBody = encodeURIComponent(`I generated an order with the 0x protocol.
|
||||
You can see and fill it here: ${this.state.shareLink}`);
|
||||
const mailToLink = `mailto:mail@example.org?subject=${encodedSubject}&body=${encodedBody}`;
|
||||
window.open(mailToLink, '_blank');
|
||||
}
|
||||
private async setShareLinkAsync() {
|
||||
const shareLink = await this.generateShareLinkAsync();
|
||||
this.setState({
|
||||
shareLink,
|
||||
});
|
||||
}
|
||||
private async generateShareLinkAsync(): Promise<string> {
|
||||
const longUrl = encodeURIComponent(this.getOrderUrl());
|
||||
const bitlyRequestUrl = constants.BITLY_ENDPOINT + '/v3/shorten?' +
|
||||
'access_token=' + constants.BITLY_ACCESS_TOKEN +
|
||||
'&longUrl=' + longUrl;
|
||||
const response = await fetch(bitlyRequestUrl);
|
||||
const responseBody = await response.text();
|
||||
const bodyObj = JSON.parse(responseBody);
|
||||
if (response.status !== 200 || bodyObj.status_code !== 200) {
|
||||
// TODO: Show error message in UI
|
||||
utils.consoleLog(`Unexpected status code: ${response.status} -> ${responseBody}`);
|
||||
await errorReporter.reportAsync(new Error(`Bitly returned non-200: ${JSON.stringify(response)}`));
|
||||
return '';
|
||||
}
|
||||
return (bodyObj as any).data.url;
|
||||
}
|
||||
private getOrderUrl() {
|
||||
const order = utils.generateOrder(this.props.networkId, this.props.exchangeContractIfExists,
|
||||
this.props.sideToAssetToken, this.props.orderExpiryTimestamp, this.props.orderTakerAddress,
|
||||
this.props.orderMakerAddress, this.props.orderMakerFee, this.props.orderTakerFee,
|
||||
this.props.orderFeeRecipient, this.props.orderSignatureData, this.props.tokenByAddress,
|
||||
this.props.orderSalt);
|
||||
const orderJSONString = JSON.stringify(order);
|
||||
const orderUrl = `${configs.BASE_URL}${WebsitePaths.Portal}/fill?order=${orderJSONString}`;
|
||||
return orderUrl;
|
||||
}
|
||||
}
|
||||
344
packages/website/ts/components/portal.tsx
Normal file
344
packages/website/ts/components/portal.tsx
Normal file
@@ -0,0 +1,344 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import * as DocumentTitle from 'react-document-title';
|
||||
import {Switch, Route} from 'react-router-dom';
|
||||
import {Dispatcher} from 'ts/redux/dispatcher';
|
||||
import {State} from 'ts/redux/reducer';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {configs} from 'ts/utils/configs';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import Paper from 'material-ui/Paper';
|
||||
import RaisedButton from 'material-ui/RaisedButton';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {GenerateOrderForm} from 'ts/containers/generate_order_form';
|
||||
import {TokenBalances} from 'ts/components/token_balances';
|
||||
import {PortalDisclaimerDialog} from 'ts/components/dialogs/portal_disclaimer_dialog';
|
||||
import {FillOrder} from 'ts/components/fill_order';
|
||||
import {Blockchain} from 'ts/blockchain';
|
||||
import {SchemaValidator} from 'ts/schemas/validator';
|
||||
import {orderSchema} from 'ts/schemas/order_schema';
|
||||
import {localStorage} from 'ts/local_storage/local_storage';
|
||||
import {TradeHistory} from 'ts/components/trade_history/trade_history';
|
||||
import {
|
||||
HashData,
|
||||
TokenByAddress,
|
||||
BlockchainErrs,
|
||||
Order,
|
||||
Fill,
|
||||
Side,
|
||||
Styles,
|
||||
ScreenWidths,
|
||||
Token,
|
||||
TokenStateByAddress,
|
||||
WebsitePaths,
|
||||
} from 'ts/types';
|
||||
import {TopBar} from 'ts/components/top_bar';
|
||||
import {Footer} from 'ts/components/footer';
|
||||
import {Loading} from 'ts/components/ui/loading';
|
||||
import {PortalMenu} from 'ts/components/portal_menu';
|
||||
import {BlockchainErrDialog} from 'ts/components/dialogs/blockchain_err_dialog';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {FlashMessage} from 'ts/components/ui/flash_message';
|
||||
|
||||
const THROTTLE_TIMEOUT = 100;
|
||||
|
||||
export interface PortalPassedProps {}
|
||||
|
||||
export interface PortalAllProps {
|
||||
blockchainErr: BlockchainErrs;
|
||||
blockchainIsLoaded: boolean;
|
||||
dispatcher: Dispatcher;
|
||||
hashData: HashData;
|
||||
networkId: number;
|
||||
nodeVersion: string;
|
||||
orderFillAmount: BigNumber;
|
||||
screenWidth: ScreenWidths;
|
||||
tokenByAddress: TokenByAddress;
|
||||
tokenStateByAddress: TokenStateByAddress;
|
||||
userEtherBalance: BigNumber;
|
||||
userAddress: string;
|
||||
shouldBlockchainErrDialogBeOpen: boolean;
|
||||
userSuppliedOrderCache: Order;
|
||||
location: Location;
|
||||
flashMessage?: string|React.ReactNode;
|
||||
}
|
||||
|
||||
interface PortalAllState {
|
||||
prevNetworkId: number;
|
||||
prevNodeVersion: string;
|
||||
prevUserAddress: string;
|
||||
hasAcceptedDisclaimer: boolean;
|
||||
}
|
||||
|
||||
const styles: Styles = {
|
||||
button: {
|
||||
color: 'white',
|
||||
},
|
||||
headline: {
|
||||
fontSize: 20,
|
||||
fontWeight: 400,
|
||||
marginBottom: 12,
|
||||
paddingTop: 16,
|
||||
},
|
||||
inkBar: {
|
||||
background: colors.amber600,
|
||||
},
|
||||
menuItem: {
|
||||
padding: '0px 16px 0px 48px',
|
||||
},
|
||||
tabItemContainer: {
|
||||
background: colors.blueGrey500,
|
||||
borderRadius: '4px 4px 0 0',
|
||||
},
|
||||
};
|
||||
|
||||
export class Portal extends React.Component<PortalAllProps, PortalAllState> {
|
||||
private blockchain: Blockchain;
|
||||
private sharedOrderIfExists: Order;
|
||||
private throttledScreenWidthUpdate: () => void;
|
||||
constructor(props: PortalAllProps) {
|
||||
super(props);
|
||||
this.sharedOrderIfExists = this.getSharedOrderIfExists();
|
||||
this.throttledScreenWidthUpdate = _.throttle(this.updateScreenWidth.bind(this), THROTTLE_TIMEOUT);
|
||||
this.state = {
|
||||
prevNetworkId: this.props.networkId,
|
||||
prevNodeVersion: this.props.nodeVersion,
|
||||
prevUserAddress: this.props.userAddress,
|
||||
hasAcceptedDisclaimer: false,
|
||||
};
|
||||
}
|
||||
public componentDidMount() {
|
||||
window.addEventListener('resize', this.throttledScreenWidthUpdate);
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
public componentWillMount() {
|
||||
this.blockchain = new Blockchain(this.props.dispatcher);
|
||||
const didAcceptPortalDisclaimer = localStorage.getItemIfExists(constants.ACCEPT_DISCLAIMER_LOCAL_STORAGE_KEY);
|
||||
const hasAcceptedDisclaimer = !_.isUndefined(didAcceptPortalDisclaimer) &&
|
||||
!_.isEmpty(didAcceptPortalDisclaimer);
|
||||
this.setState({
|
||||
hasAcceptedDisclaimer,
|
||||
});
|
||||
}
|
||||
public componentWillUnmount() {
|
||||
this.blockchain.destroy();
|
||||
window.removeEventListener('resize', this.throttledScreenWidthUpdate);
|
||||
// We re-set the entire redux state when the portal is unmounted so that when it is re-rendered
|
||||
// the initialization process always occurs from the same base state. This helps avoid
|
||||
// initialization inconsistencies (i.e While the portal was unrendered, the user might have
|
||||
// become disconnected from their backing Ethereum node, changes user accounts, etc...)
|
||||
this.props.dispatcher.resetState();
|
||||
}
|
||||
public componentWillReceiveProps(nextProps: PortalAllProps) {
|
||||
if (nextProps.networkId !== this.state.prevNetworkId) {
|
||||
this.blockchain.networkIdUpdatedFireAndForgetAsync(nextProps.networkId);
|
||||
this.setState({
|
||||
prevNetworkId: nextProps.networkId,
|
||||
});
|
||||
}
|
||||
if (nextProps.userAddress !== this.state.prevUserAddress) {
|
||||
this.blockchain.userAddressUpdatedFireAndForgetAsync(nextProps.userAddress);
|
||||
if (!_.isEmpty(nextProps.userAddress) &&
|
||||
nextProps.blockchainIsLoaded) {
|
||||
const tokens = _.values(nextProps.tokenByAddress);
|
||||
this.updateBalanceAndAllowanceWithLoadingScreenAsync(tokens);
|
||||
}
|
||||
this.setState({
|
||||
prevUserAddress: nextProps.userAddress,
|
||||
});
|
||||
}
|
||||
if (nextProps.nodeVersion !== this.state.prevNodeVersion) {
|
||||
this.blockchain.nodeVersionUpdatedFireAndForgetAsync(nextProps.nodeVersion);
|
||||
}
|
||||
}
|
||||
public render() {
|
||||
const updateShouldBlockchainErrDialogBeOpen = this.props.dispatcher
|
||||
.updateShouldBlockchainErrDialogBeOpen.bind(this.props.dispatcher);
|
||||
const portalStyle: React.CSSProperties = {
|
||||
minHeight: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
};
|
||||
return (
|
||||
<div style={portalStyle}>
|
||||
<DocumentTitle title="0x Portal DApp"/>
|
||||
<TopBar
|
||||
userAddress={this.props.userAddress}
|
||||
blockchainIsLoaded={this.props.blockchainIsLoaded}
|
||||
location={this.props.location}
|
||||
/>
|
||||
<div id="portal" className="mx-auto max-width-4 pt4" style={{width: '100%'}}>
|
||||
<Paper className="mb3 mt2">
|
||||
{!configs.isMainnetEnabled && this.props.networkId === constants.MAINNET_NETWORK_ID ?
|
||||
<div className="p3 center">
|
||||
<div className="h2 py2">Mainnet unavailable</div>
|
||||
<div className="mx-auto pb2 pt2">
|
||||
<img
|
||||
src="/images/zrx_token.png"
|
||||
style={{width: 150}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
0x portal is currently unavailable on the Ethereum mainnet.
|
||||
<div>
|
||||
To try it out, switch to the Kovan test network
|
||||
(networkId: 42).
|
||||
</div>
|
||||
<div className="py2">
|
||||
Check back soon!
|
||||
</div>
|
||||
</div>
|
||||
</div> :
|
||||
<div className="mx-auto flex">
|
||||
<div
|
||||
className="col col-2 pr2 pt1 sm-hide xs-hide"
|
||||
style={{overflow: 'hidden', backgroundColor: 'rgb(39, 39, 39)', color: 'white'}}
|
||||
>
|
||||
<PortalMenu menuItemStyle={{color: 'white'}} />
|
||||
</div>
|
||||
<div className="col col-12 lg-col-10 md-col-10 sm-col sm-col-12">
|
||||
<div className="py2" style={{backgroundColor: colors.grey50}}>
|
||||
{this.props.blockchainIsLoaded ?
|
||||
<Switch>
|
||||
<Route
|
||||
path={`${WebsitePaths.Portal}/fill`}
|
||||
render={this.renderFillOrder.bind(this)}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsitePaths.Portal}/balances`}
|
||||
render={this.renderTokenBalances.bind(this)}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsitePaths.Portal}/trades`}
|
||||
component={this.renderTradeHistory.bind(this)}
|
||||
/>
|
||||
<Route
|
||||
path={`${WebsitePaths.Home}`}
|
||||
render={this.renderGenerateOrderForm.bind(this)}
|
||||
/>
|
||||
</Switch> :
|
||||
<Loading />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</Paper>
|
||||
<BlockchainErrDialog
|
||||
blockchain={this.blockchain}
|
||||
blockchainErr={this.props.blockchainErr}
|
||||
isOpen={this.props.shouldBlockchainErrDialogBeOpen}
|
||||
userAddress={this.props.userAddress}
|
||||
toggleDialogFn={updateShouldBlockchainErrDialogBeOpen}
|
||||
networkId={this.props.networkId}
|
||||
/>
|
||||
<PortalDisclaimerDialog
|
||||
isOpen={!this.state.hasAcceptedDisclaimer}
|
||||
onToggleDialog={this.onPortalDisclaimerAccepted.bind(this)}
|
||||
/>
|
||||
<FlashMessage
|
||||
dispatcher={this.props.dispatcher}
|
||||
flashMessage={this.props.flashMessage}
|
||||
/>
|
||||
</div>
|
||||
<Footer location={this.props.location} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderTradeHistory() {
|
||||
return (
|
||||
<TradeHistory
|
||||
tokenByAddress={this.props.tokenByAddress}
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
private renderTokenBalances() {
|
||||
return (
|
||||
<TokenBalances
|
||||
blockchain={this.blockchain}
|
||||
blockchainErr={this.props.blockchainErr}
|
||||
blockchainIsLoaded={this.props.blockchainIsLoaded}
|
||||
dispatcher={this.props.dispatcher}
|
||||
screenWidth={this.props.screenWidth}
|
||||
tokenByAddress={this.props.tokenByAddress}
|
||||
tokenStateByAddress={this.props.tokenStateByAddress}
|
||||
userAddress={this.props.userAddress}
|
||||
userEtherBalance={this.props.userEtherBalance}
|
||||
networkId={this.props.networkId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
private renderFillOrder(match: any, location: Location, history: History) {
|
||||
const initialFillOrder = !_.isUndefined(this.props.userSuppliedOrderCache) ?
|
||||
this.props.userSuppliedOrderCache :
|
||||
this.sharedOrderIfExists;
|
||||
return (
|
||||
<FillOrder
|
||||
blockchain={this.blockchain}
|
||||
blockchainErr={this.props.blockchainErr}
|
||||
initialOrder={initialFillOrder}
|
||||
isOrderInUrl={!_.isUndefined(this.sharedOrderIfExists)}
|
||||
orderFillAmount={this.props.orderFillAmount}
|
||||
networkId={this.props.networkId}
|
||||
userAddress={this.props.userAddress}
|
||||
tokenByAddress={this.props.tokenByAddress}
|
||||
tokenStateByAddress={this.props.tokenStateByAddress}
|
||||
dispatcher={this.props.dispatcher}
|
||||
/>
|
||||
);
|
||||
}
|
||||
private renderGenerateOrderForm(match: any, location: Location, history: History) {
|
||||
return (
|
||||
<GenerateOrderForm
|
||||
blockchain={this.blockchain}
|
||||
hashData={this.props.hashData}
|
||||
dispatcher={this.props.dispatcher}
|
||||
/>
|
||||
);
|
||||
}
|
||||
private onPortalDisclaimerAccepted() {
|
||||
localStorage.setItem(constants.ACCEPT_DISCLAIMER_LOCAL_STORAGE_KEY, 'set');
|
||||
this.setState({
|
||||
hasAcceptedDisclaimer: true,
|
||||
});
|
||||
}
|
||||
private getSharedOrderIfExists(): Order {
|
||||
const queryString = window.location.search;
|
||||
if (queryString.length === 0) {
|
||||
return;
|
||||
}
|
||||
const queryParams = queryString.substring(1).split('&');
|
||||
const orderQueryParam = _.find(queryParams, queryParam => {
|
||||
const queryPair = queryParam.split('=');
|
||||
return queryPair[0] === 'order';
|
||||
});
|
||||
if (_.isUndefined(orderQueryParam)) {
|
||||
return;
|
||||
}
|
||||
const orderPair = orderQueryParam.split('=');
|
||||
if (orderPair.length !== 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validator = new SchemaValidator();
|
||||
const order = JSON.parse(decodeURIComponent(orderPair[1]));
|
||||
const validationResult = validator.validate(order, orderSchema);
|
||||
if (validationResult.errors.length > 0) {
|
||||
utils.consoleLog(`Invalid shared order: ${validationResult.errors}`);
|
||||
return;
|
||||
}
|
||||
return order;
|
||||
}
|
||||
private updateScreenWidth() {
|
||||
const newScreenWidth = utils.getScreenWidth();
|
||||
this.props.dispatcher.updateScreenWidth(newScreenWidth);
|
||||
}
|
||||
private async updateBalanceAndAllowanceWithLoadingScreenAsync(tokens: Token[]) {
|
||||
this.props.dispatcher.updateBlockchainIsLoaded(false);
|
||||
await this.blockchain.updateTokenBalancesAndAllowancesAsync(tokens);
|
||||
this.props.dispatcher.updateBlockchainIsLoaded(true);
|
||||
}
|
||||
}
|
||||
68
packages/website/ts/components/portal_menu.tsx
Normal file
68
packages/website/ts/components/portal_menu.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {MenuItem} from 'ts/components/ui/menu_item';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {WebsitePaths} from 'ts/types';
|
||||
|
||||
export interface PortalMenuProps {
|
||||
menuItemStyle: React.CSSProperties;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
interface PortalMenuState {}
|
||||
|
||||
export class PortalMenu extends React.Component<PortalMenuProps, PortalMenuState> {
|
||||
public static defaultProps: Partial<PortalMenuProps> = {
|
||||
onClick: _.noop,
|
||||
};
|
||||
public render() {
|
||||
return (
|
||||
<div>
|
||||
<MenuItem
|
||||
style={this.props.menuItemStyle}
|
||||
className="py2"
|
||||
to={`${WebsitePaths.Portal}`}
|
||||
onClick={this.props.onClick.bind(this)}
|
||||
>
|
||||
{this.renderMenuItemWithIcon('Generate order', 'zmdi-arrow-right-top')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
style={this.props.menuItemStyle}
|
||||
className="py2"
|
||||
to={`${WebsitePaths.Portal}/fill`}
|
||||
onClick={this.props.onClick.bind(this)}
|
||||
>
|
||||
{this.renderMenuItemWithIcon('Fill order', 'zmdi-arrow-left-bottom')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
style={this.props.menuItemStyle}
|
||||
className="py2"
|
||||
to={`${WebsitePaths.Portal}/balances`}
|
||||
onClick={this.props.onClick.bind(this)}
|
||||
>
|
||||
{this.renderMenuItemWithIcon('Balances', 'zmdi-balance-wallet')}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
style={this.props.menuItemStyle}
|
||||
className="py2"
|
||||
to={`${WebsitePaths.Portal}/trades`}
|
||||
onClick={this.props.onClick.bind(this)}
|
||||
>
|
||||
{this.renderMenuItemWithIcon('Trade history', 'zmdi-format-list-bulleted')}
|
||||
</MenuItem>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderMenuItemWithIcon(title: string, iconName: string) {
|
||||
return (
|
||||
<div className="flex" style={{fontWeight: 100}}>
|
||||
<div className="pr1 pl2">
|
||||
<i style={{fontSize: 20}} className={`zmdi ${iconName}`} />
|
||||
</div>
|
||||
<div className="pl1">
|
||||
{title}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
89
packages/website/ts/components/send_button.tsx
Normal file
89
packages/website/ts/components/send_button.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import * as _ from 'lodash';
|
||||
import {ZeroEx} from '0x.js';
|
||||
import * as React from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import RaisedButton from 'material-ui/RaisedButton';
|
||||
import {BlockchainCallErrs, Token, TokenState} from 'ts/types';
|
||||
import {SendDialog} from 'ts/components/dialogs/send_dialog';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {Dispatcher} from 'ts/redux/dispatcher';
|
||||
import {errorReporter} from 'ts/utils/error_reporter';
|
||||
import {Blockchain} from 'ts/blockchain';
|
||||
|
||||
interface SendButtonProps {
|
||||
token: Token;
|
||||
tokenState: TokenState;
|
||||
dispatcher: Dispatcher;
|
||||
blockchain: Blockchain;
|
||||
onError: () => void;
|
||||
}
|
||||
|
||||
interface SendButtonState {
|
||||
isSendDialogVisible: boolean;
|
||||
isSending: boolean;
|
||||
}
|
||||
|
||||
export class SendButton extends React.Component<SendButtonProps, SendButtonState> {
|
||||
public constructor(props: SendButtonProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isSendDialogVisible: false,
|
||||
isSending: false,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const labelStyle = this.state.isSending ? {fontSize: 10} : {};
|
||||
return (
|
||||
<div>
|
||||
<RaisedButton
|
||||
style={{width: '100%'}}
|
||||
labelStyle={labelStyle}
|
||||
disabled={this.state.isSending}
|
||||
label={this.state.isSending ? 'Sending...' : 'Send'}
|
||||
onClick={this.toggleSendDialog.bind(this)}
|
||||
/>
|
||||
<SendDialog
|
||||
isOpen={this.state.isSendDialogVisible}
|
||||
onComplete={this.onSendAmountSelectedAsync.bind(this)}
|
||||
onCancelled={this.toggleSendDialog.bind(this)}
|
||||
token={this.props.token}
|
||||
tokenState={this.props.tokenState}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private toggleSendDialog() {
|
||||
this.setState({
|
||||
isSendDialogVisible: !this.state.isSendDialogVisible,
|
||||
});
|
||||
}
|
||||
private async onSendAmountSelectedAsync(recipient: string, value: BigNumber) {
|
||||
this.setState({
|
||||
isSending: true,
|
||||
});
|
||||
this.toggleSendDialog();
|
||||
const token = this.props.token;
|
||||
const tokenState = this.props.tokenState;
|
||||
let balance = tokenState.balance;
|
||||
try {
|
||||
await this.props.blockchain.transferAsync(token, recipient, value);
|
||||
balance = balance.minus(value);
|
||||
this.props.dispatcher.replaceTokenBalanceByAddress(token.address, balance);
|
||||
} catch (err) {
|
||||
const errMsg = `${err}`;
|
||||
if (_.includes(errMsg, BlockchainCallErrs.USER_HAS_NO_ASSOCIATED_ADDRESSES)) {
|
||||
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
|
||||
return;
|
||||
} else if (!_.includes(errMsg, 'User denied transaction')) {
|
||||
utils.consoleLog(`Unexpected error encountered: ${err}`);
|
||||
utils.consoleLog(err.stack);
|
||||
await errorReporter.reportAsync(err);
|
||||
this.props.onError();
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
isSending: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
697
packages/website/ts/components/token_balances.tsx
Normal file
697
packages/website/ts/components/token_balances.tsx
Normal file
@@ -0,0 +1,697 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {ZeroEx} from '0x.js';
|
||||
import DharmaLoanFrame from 'dharma-loan-frame';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import Dialog from 'material-ui/Dialog';
|
||||
import Divider from 'material-ui/Divider';
|
||||
import FlatButton from 'material-ui/FlatButton';
|
||||
import RaisedButton from 'material-ui/RaisedButton';
|
||||
import FloatingActionButton from 'material-ui/FloatingActionButton';
|
||||
import ContentAdd from 'material-ui/svg-icons/content/add';
|
||||
import ContentRemove from 'material-ui/svg-icons/content/remove';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableHeaderColumn,
|
||||
TableRowColumn,
|
||||
} from 'material-ui/Table';
|
||||
import ReactTooltip = require('react-tooltip');
|
||||
import BigNumber from 'bignumber.js';
|
||||
import firstBy = require('thenby');
|
||||
import QueryString = require('query-string');
|
||||
import {Dispatcher} from 'ts/redux/dispatcher';
|
||||
import {
|
||||
TokenByAddress,
|
||||
TokenStateByAddress,
|
||||
Token,
|
||||
BlockchainErrs,
|
||||
BalanceErrs,
|
||||
Styles,
|
||||
ScreenWidths,
|
||||
EtherscanLinkSuffixes,
|
||||
BlockchainCallErrs,
|
||||
TokenVisibility,
|
||||
} from 'ts/types';
|
||||
import {Blockchain} from 'ts/blockchain';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {configs} from 'ts/utils/configs';
|
||||
import {LifeCycleRaisedButton} from 'ts/components/ui/lifecycle_raised_button';
|
||||
import {HelpTooltip} from 'ts/components/ui/help_tooltip';
|
||||
import {errorReporter} from 'ts/utils/error_reporter';
|
||||
import {AllowanceToggle} from 'ts/components/inputs/allowance_toggle';
|
||||
import {EthWethConversionButton} from 'ts/components/eth_weth_conversion_button';
|
||||
import {SendButton} from 'ts/components/send_button';
|
||||
import {AssetPicker} from 'ts/components/generate_order/asset_picker';
|
||||
import {TokenIcon} from 'ts/components/ui/token_icon';
|
||||
import {trackedTokenStorage} from 'ts/local_storage/tracked_token_storage';
|
||||
|
||||
const ETHER_ICON_PATH = '/images/ether.png';
|
||||
const ETHER_TOKEN_SYMBOL = 'WETH';
|
||||
const ZRX_TOKEN_SYMBOL = 'ZRX';
|
||||
|
||||
const PRECISION = 5;
|
||||
const ICON_DIMENSION = 40;
|
||||
const ARTIFICIAL_FAUCET_REQUEST_DELAY = 1000;
|
||||
const TOKEN_TABLE_ROW_HEIGHT = 60;
|
||||
const MAX_TOKEN_TABLE_HEIGHT = 420;
|
||||
const TOKEN_COL_SPAN_LG = 2;
|
||||
const TOKEN_COL_SPAN_SM = 1;
|
||||
|
||||
const styles: Styles = {
|
||||
bgColor: {
|
||||
backgroundColor: colors.grey50,
|
||||
},
|
||||
};
|
||||
|
||||
interface TokenBalancesProps {
|
||||
blockchain: Blockchain;
|
||||
blockchainErr: BlockchainErrs;
|
||||
blockchainIsLoaded: boolean;
|
||||
dispatcher: Dispatcher;
|
||||
screenWidth: ScreenWidths;
|
||||
tokenByAddress: TokenByAddress;
|
||||
tokenStateByAddress: TokenStateByAddress;
|
||||
userAddress: string;
|
||||
userEtherBalance: BigNumber;
|
||||
networkId: number;
|
||||
}
|
||||
|
||||
interface TokenBalancesState {
|
||||
errorType: BalanceErrs;
|
||||
isBalanceSpinnerVisible: boolean;
|
||||
isDharmaDialogVisible: boolean;
|
||||
isZRXSpinnerVisible: boolean;
|
||||
currentZrxBalance?: BigNumber;
|
||||
isTokenPickerOpen: boolean;
|
||||
isAddingToken: boolean;
|
||||
}
|
||||
|
||||
export class TokenBalances extends React.Component<TokenBalancesProps, TokenBalancesState> {
|
||||
public constructor(props: TokenBalancesProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
errorType: undefined,
|
||||
isBalanceSpinnerVisible: false,
|
||||
isZRXSpinnerVisible: false,
|
||||
isDharmaDialogVisible: DharmaLoanFrame.isAuthTokenPresent(),
|
||||
isTokenPickerOpen: false,
|
||||
isAddingToken: false,
|
||||
};
|
||||
}
|
||||
public componentWillReceiveProps(nextProps: TokenBalancesProps) {
|
||||
if (nextProps.userEtherBalance !== this.props.userEtherBalance) {
|
||||
if (this.state.isBalanceSpinnerVisible) {
|
||||
const receivedAmount = nextProps.userEtherBalance.minus(this.props.userEtherBalance);
|
||||
this.props.dispatcher.showFlashMessage(`Received ${receivedAmount.toString(10)} Kovan Ether`);
|
||||
}
|
||||
this.setState({
|
||||
isBalanceSpinnerVisible: false,
|
||||
});
|
||||
}
|
||||
const nextZrxToken = _.find(_.values(nextProps.tokenByAddress), t => t.symbol === ZRX_TOKEN_SYMBOL);
|
||||
const nextZrxTokenBalance = nextProps.tokenStateByAddress[nextZrxToken.address].balance;
|
||||
if (!_.isUndefined(this.state.currentZrxBalance) && !nextZrxTokenBalance.eq(this.state.currentZrxBalance)) {
|
||||
if (this.state.isZRXSpinnerVisible) {
|
||||
const receivedAmount = nextZrxTokenBalance.minus(this.state.currentZrxBalance);
|
||||
const receiveAmountInUnits = ZeroEx.toUnitAmount(receivedAmount, 18);
|
||||
this.props.dispatcher.showFlashMessage(`Received ${receiveAmountInUnits.toString(10)} Kovan ZRX`);
|
||||
}
|
||||
this.setState({
|
||||
isZRXSpinnerVisible: false,
|
||||
currentZrxBalance: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
public componentDidMount() {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
public render() {
|
||||
const errorDialogActions = [
|
||||
<FlatButton
|
||||
label="Ok"
|
||||
primary={true}
|
||||
onTouchTap={this.onErrorDialogToggle.bind(this, false)}
|
||||
/>,
|
||||
];
|
||||
const dharmaDialogActions = [
|
||||
<FlatButton
|
||||
label="Close"
|
||||
primary={true}
|
||||
onTouchTap={this.onDharmaDialogToggle.bind(this, false)}
|
||||
/>,
|
||||
];
|
||||
const isTestNetwork = this.props.networkId === constants.TESTNET_NETWORK_ID;
|
||||
const dharmaButtonColumnStyle = {
|
||||
paddingLeft: 3,
|
||||
display: isTestNetwork ? 'table-cell' : 'none',
|
||||
};
|
||||
const stubColumnStyle = {
|
||||
display: isTestNetwork ? 'none' : 'table-cell',
|
||||
};
|
||||
const allTokenRowHeight = _.size(this.props.tokenByAddress) * TOKEN_TABLE_ROW_HEIGHT;
|
||||
const tokenTableHeight = allTokenRowHeight < MAX_TOKEN_TABLE_HEIGHT ?
|
||||
allTokenRowHeight :
|
||||
MAX_TOKEN_TABLE_HEIGHT;
|
||||
const isSmallScreen = this.props.screenWidth === ScreenWidths.SM;
|
||||
const tokenColSpan = isSmallScreen ? TOKEN_COL_SPAN_SM : TOKEN_COL_SPAN_LG;
|
||||
const dharmaLoanExplanation = 'If you need access to larger amounts of ether,<br> \
|
||||
you can request a loan from the Dharma Loan<br> \
|
||||
network. Your loan should be funded in 5<br> \
|
||||
minutes or less.';
|
||||
const allowanceExplanation = '0x smart contracts require access to your<br> \
|
||||
token balances in order to execute trades.<br> \
|
||||
Toggling permissions sets an allowance for the<br> \
|
||||
smart contract so you can start trading that token.';
|
||||
return (
|
||||
<div className="lg-px4 md-px4 sm-px1 pb2">
|
||||
<h3>{isTestNetwork ? 'Test ether' : 'Ether'}</h3>
|
||||
<Divider />
|
||||
<div className="pt2 pb2">
|
||||
{isTestNetwork ?
|
||||
'In order to try out the 0x Portal Dapp, request some test ether to pay for \
|
||||
gas costs. It might take a bit of time for the test ether to show up.' :
|
||||
'Ether must be converted to Ether Tokens in order to be tradable via 0x. \
|
||||
You can convert between Ether and Ether Tokens by clicking the "convert" button below.'
|
||||
}
|
||||
</div>
|
||||
<Table
|
||||
selectable={false}
|
||||
style={styles.bgColor}
|
||||
>
|
||||
<TableHeader displaySelectAll={false} adjustForCheckbox={false}>
|
||||
<TableRow>
|
||||
<TableHeaderColumn>Currency</TableHeaderColumn>
|
||||
<TableHeaderColumn>Balance</TableHeaderColumn>
|
||||
<TableRowColumn
|
||||
className="sm-hide xs-hide"
|
||||
style={stubColumnStyle}
|
||||
/>
|
||||
{
|
||||
isTestNetwork &&
|
||||
<TableHeaderColumn
|
||||
style={{paddingLeft: 3}}
|
||||
>
|
||||
{isSmallScreen ? 'Faucet' : 'Request from faucet'}
|
||||
</TableHeaderColumn>
|
||||
}
|
||||
{
|
||||
isTestNetwork &&
|
||||
<TableHeaderColumn
|
||||
style={dharmaButtonColumnStyle}
|
||||
>
|
||||
{isSmallScreen ? 'Loan' : 'Request Dharma loan'}
|
||||
<HelpTooltip
|
||||
style={{paddingLeft: 4}}
|
||||
explanation={dharmaLoanExplanation}
|
||||
/>
|
||||
</TableHeaderColumn>
|
||||
}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody displayRowCheckbox={false}>
|
||||
<TableRow key="ETH">
|
||||
<TableRowColumn className="py1">
|
||||
<img
|
||||
style={{width: ICON_DIMENSION, height: ICON_DIMENSION}}
|
||||
src={ETHER_ICON_PATH}
|
||||
/>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn>
|
||||
{this.props.userEtherBalance.toFixed(PRECISION)} ETH
|
||||
{this.state.isBalanceSpinnerVisible &&
|
||||
<span className="pl1">
|
||||
<i className="zmdi zmdi-spinner zmdi-hc-spin" />
|
||||
</span>
|
||||
}
|
||||
</TableRowColumn>
|
||||
<TableRowColumn
|
||||
className="sm-hide xs-hide"
|
||||
style={stubColumnStyle}
|
||||
/>
|
||||
{
|
||||
isTestNetwork &&
|
||||
<TableRowColumn style={{paddingLeft: 3}}>
|
||||
<LifeCycleRaisedButton
|
||||
labelReady="Request"
|
||||
labelLoading="Sending..."
|
||||
labelComplete="Sent!"
|
||||
onClickAsyncFn={this.faucetRequestAsync.bind(this, true)}
|
||||
/>
|
||||
</TableRowColumn>
|
||||
}
|
||||
{
|
||||
isTestNetwork &&
|
||||
<TableRowColumn style={dharmaButtonColumnStyle}>
|
||||
<RaisedButton
|
||||
label="Request"
|
||||
style={{width: '100%'}}
|
||||
onTouchTap={this.onDharmaDialogToggle.bind(this)}
|
||||
/>
|
||||
</TableRowColumn>
|
||||
}
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="clearfix" style={{paddingBottom: 1}}>
|
||||
<div className="col col-10">
|
||||
<h3 className="pt2">
|
||||
{isTestNetwork ? 'Test tokens' : 'Tokens'}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="col col-1 pt3 align-right">
|
||||
<FloatingActionButton
|
||||
mini={true}
|
||||
zDepth={0}
|
||||
onClick={this.onAddTokenClicked.bind(this)}
|
||||
>
|
||||
<ContentAdd />
|
||||
</FloatingActionButton>
|
||||
</div>
|
||||
<div className="col col-1 pt3 align-right">
|
||||
<FloatingActionButton
|
||||
mini={true}
|
||||
zDepth={0}
|
||||
onClick={this.onRemoveTokenClicked.bind(this)}
|
||||
>
|
||||
<ContentRemove />
|
||||
</FloatingActionButton>
|
||||
</div>
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="pt2 pb2">
|
||||
{isTestNetwork ?
|
||||
'Mint some test tokens you\'d like to use to generate or fill an order using 0x.' :
|
||||
'Set trading permissions for a token you\'d like to start trading.'
|
||||
}
|
||||
</div>
|
||||
<Table
|
||||
selectable={false}
|
||||
bodyStyle={{height: tokenTableHeight}}
|
||||
style={styles.bgColor}
|
||||
>
|
||||
<TableHeader displaySelectAll={false} adjustForCheckbox={false}>
|
||||
<TableRow>
|
||||
<TableHeaderColumn
|
||||
colSpan={tokenColSpan}
|
||||
>
|
||||
Token
|
||||
</TableHeaderColumn>
|
||||
<TableHeaderColumn style={{paddingLeft: 3}}>Balance</TableHeaderColumn>
|
||||
<TableHeaderColumn>
|
||||
<div className="inline-block">{!isSmallScreen && 'Trade '}Permissions</div>
|
||||
<HelpTooltip
|
||||
style={{paddingLeft: 4}}
|
||||
explanation={allowanceExplanation}
|
||||
/>
|
||||
</TableHeaderColumn>
|
||||
<TableHeaderColumn>
|
||||
Action
|
||||
</TableHeaderColumn>
|
||||
{this.props.screenWidth !== ScreenWidths.SM &&
|
||||
<TableHeaderColumn>
|
||||
Send
|
||||
</TableHeaderColumn>
|
||||
}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody displayRowCheckbox={false}>
|
||||
{this.renderTokenTableRows()}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Dialog
|
||||
title="Oh oh"
|
||||
titleStyle={{fontWeight: 100}}
|
||||
actions={errorDialogActions}
|
||||
open={!_.isUndefined(this.state.errorType)}
|
||||
onRequestClose={this.onErrorDialogToggle.bind(this, false)}
|
||||
>
|
||||
{this.renderErrorDialogBody()}
|
||||
</Dialog>
|
||||
<Dialog
|
||||
title="Request Dharma Loan"
|
||||
titleStyle={{fontWeight: 100, backgroundColor: 'rgb(250, 250, 250)'}}
|
||||
bodyStyle={{backgroundColor: 'rgb(37, 37, 37)'}}
|
||||
actionsContainerStyle={{backgroundColor: 'rgb(250, 250, 250)'}}
|
||||
autoScrollBodyContent={true}
|
||||
actions={dharmaDialogActions}
|
||||
open={this.state.isDharmaDialogVisible}
|
||||
>
|
||||
{this.renderDharmaLoanFrame()}
|
||||
</Dialog>
|
||||
<AssetPicker
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
blockchain={this.props.blockchain}
|
||||
dispatcher={this.props.dispatcher}
|
||||
isOpen={this.state.isTokenPickerOpen}
|
||||
currentTokenAddress={''}
|
||||
onTokenChosen={this.onAssetTokenPicked.bind(this)}
|
||||
tokenByAddress={this.props.tokenByAddress}
|
||||
tokenVisibility={this.state.isAddingToken ? TokenVisibility.UNTRACKED : TokenVisibility.TRACKED}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderTokenTableRows() {
|
||||
if (!this.props.blockchainIsLoaded || this.props.blockchainErr !== '') {
|
||||
return '';
|
||||
}
|
||||
const isSmallScreen = this.props.screenWidth === ScreenWidths.SM;
|
||||
const tokenColSpan = isSmallScreen ? TOKEN_COL_SPAN_SM : TOKEN_COL_SPAN_LG;
|
||||
const actionPaddingX = isSmallScreen ? 2 : 24;
|
||||
const allTokens = _.values(this.props.tokenByAddress);
|
||||
const trackedTokens = _.filter(allTokens, t => t.isTracked);
|
||||
const trackedTokensStartingWithEtherToken = trackedTokens.sort(
|
||||
firstBy((t: Token) => (t.symbol !== ETHER_TOKEN_SYMBOL))
|
||||
.thenBy((t: Token) => (t.symbol !== ZRX_TOKEN_SYMBOL))
|
||||
.thenBy('address'),
|
||||
);
|
||||
const tableRows = _.map(
|
||||
trackedTokensStartingWithEtherToken,
|
||||
this.renderTokenRow.bind(this, tokenColSpan, actionPaddingX),
|
||||
);
|
||||
return tableRows;
|
||||
}
|
||||
private renderTokenRow(tokenColSpan: number, actionPaddingX: number, token: Token) {
|
||||
const tokenState = this.props.tokenStateByAddress[token.address];
|
||||
const tokenLink = utils.getEtherScanLinkIfExists(token.address, this.props.networkId,
|
||||
EtherscanLinkSuffixes.address);
|
||||
const isMintable = _.includes(configs.symbolsOfMintableTokens, token.symbol) &&
|
||||
this.props.networkId !== constants.MAINNET_NETWORK_ID;
|
||||
return (
|
||||
<TableRow key={token.address} style={{height: TOKEN_TABLE_ROW_HEIGHT}}>
|
||||
<TableRowColumn
|
||||
colSpan={tokenColSpan}
|
||||
>
|
||||
{_.isUndefined(tokenLink) ?
|
||||
this.renderTokenName(token) :
|
||||
<a href={tokenLink} target="_blank" style={{textDecoration: 'none'}}>
|
||||
{this.renderTokenName(token)}
|
||||
</a>
|
||||
}
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={{paddingRight: 3, paddingLeft: 3}}>
|
||||
{this.renderAmount(tokenState.balance, token.decimals)} {token.symbol}
|
||||
{this.state.isZRXSpinnerVisible && token.symbol === ZRX_TOKEN_SYMBOL &&
|
||||
<span className="pl1">
|
||||
<i className="zmdi zmdi-spinner zmdi-hc-spin" />
|
||||
</span>
|
||||
}
|
||||
</TableRowColumn>
|
||||
<TableRowColumn>
|
||||
<AllowanceToggle
|
||||
blockchain={this.props.blockchain}
|
||||
dispatcher={this.props.dispatcher}
|
||||
token={token}
|
||||
tokenState={tokenState}
|
||||
onErrorOccurred={this.onErrorOccurred.bind(this)}
|
||||
userAddress={this.props.userAddress}
|
||||
/>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn
|
||||
style={{paddingLeft: actionPaddingX, paddingRight: actionPaddingX}}
|
||||
>
|
||||
{isMintable &&
|
||||
<LifeCycleRaisedButton
|
||||
labelReady="Mint"
|
||||
labelLoading={<span style={{fontSize: 12}}>Minting...</span>}
|
||||
labelComplete="Minted!"
|
||||
onClickAsyncFn={this.onMintTestTokensAsync.bind(this, token)}
|
||||
/>
|
||||
}
|
||||
{token.symbol === ETHER_TOKEN_SYMBOL &&
|
||||
<EthWethConversionButton
|
||||
blockchain={this.props.blockchain}
|
||||
dispatcher={this.props.dispatcher}
|
||||
ethToken={this.getWrappedEthToken()}
|
||||
ethTokenState={tokenState}
|
||||
userEtherBalance={this.props.userEtherBalance}
|
||||
onError={this.onEthWethConversionFailed.bind(this)}
|
||||
/>
|
||||
}
|
||||
{token.symbol === ZRX_TOKEN_SYMBOL && this.props.networkId === constants.TESTNET_NETWORK_ID &&
|
||||
<LifeCycleRaisedButton
|
||||
labelReady="Request"
|
||||
labelLoading="Sending..."
|
||||
labelComplete="Sent!"
|
||||
onClickAsyncFn={this.faucetRequestAsync.bind(this, false)}
|
||||
/>
|
||||
}
|
||||
</TableRowColumn>
|
||||
{this.props.screenWidth !== ScreenWidths.SM &&
|
||||
<TableRowColumn
|
||||
style={{paddingLeft: actionPaddingX, paddingRight: actionPaddingX}}
|
||||
>
|
||||
<SendButton
|
||||
blockchain={this.props.blockchain}
|
||||
dispatcher={this.props.dispatcher}
|
||||
token={token}
|
||||
tokenState={tokenState}
|
||||
onError={this.onSendFailed.bind(this)}
|
||||
/>
|
||||
</TableRowColumn>
|
||||
}
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
private onAssetTokenPicked(tokenAddress: string) {
|
||||
if (_.isEmpty(tokenAddress)) {
|
||||
this.setState({
|
||||
isTokenPickerOpen: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const token = this.props.tokenByAddress[tokenAddress];
|
||||
const isDefaultTrackedToken = _.includes(configs.defaultTrackedTokenSymbols, token.symbol);
|
||||
if (!this.state.isAddingToken && !isDefaultTrackedToken) {
|
||||
if (token.isRegistered) {
|
||||
// Remove the token from tracked tokens
|
||||
const newToken = _.assign({}, token, {
|
||||
isTracked: false,
|
||||
});
|
||||
this.props.dispatcher.updateTokenByAddress([newToken]);
|
||||
} else {
|
||||
this.props.dispatcher.removeTokenToTokenByAddress(token);
|
||||
}
|
||||
this.props.dispatcher.removeFromTokenStateByAddress(tokenAddress);
|
||||
trackedTokenStorage.removeTrackedToken(this.props.userAddress, this.props.networkId, tokenAddress);
|
||||
} else if (isDefaultTrackedToken) {
|
||||
this.props.dispatcher.showFlashMessage(`Cannot remove ${token.name} because it's a default token`);
|
||||
}
|
||||
this.setState({
|
||||
isTokenPickerOpen: false,
|
||||
});
|
||||
}
|
||||
private onEthWethConversionFailed() {
|
||||
this.setState({
|
||||
errorType: BalanceErrs.wethConversionFailed,
|
||||
});
|
||||
}
|
||||
private onSendFailed() {
|
||||
this.setState({
|
||||
errorType: BalanceErrs.sendFailed,
|
||||
});
|
||||
}
|
||||
private renderAmount(amount: BigNumber, decimals: number) {
|
||||
const unitAmount = ZeroEx.toUnitAmount(amount, decimals);
|
||||
return unitAmount.toNumber().toFixed(PRECISION);
|
||||
}
|
||||
private renderTokenName(token: Token) {
|
||||
const tooltipId = `tooltip-${token.address}`;
|
||||
return (
|
||||
<div className="flex">
|
||||
<TokenIcon token={token} diameter={ICON_DIMENSION} />
|
||||
<div
|
||||
data-tip={true}
|
||||
data-for={tooltipId}
|
||||
className="mt2 ml2 sm-hide xs-hide"
|
||||
>
|
||||
{token.name}
|
||||
</div>
|
||||
<ReactTooltip id={tooltipId}>{token.address}</ReactTooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderErrorDialogBody() {
|
||||
switch (this.state.errorType) {
|
||||
case BalanceErrs.incorrectNetworkForFaucet:
|
||||
return (
|
||||
<div>
|
||||
Our faucet can only send test Ether to addresses on the {constants.TESTNET_NAME}
|
||||
{' '}testnet (networkId {constants.TESTNET_NETWORK_ID}). Please make sure you are
|
||||
{' '}connected to the {constants.TESTNET_NAME} testnet and try requesting ether again.
|
||||
</div>
|
||||
);
|
||||
|
||||
case BalanceErrs.faucetRequestFailed:
|
||||
return (
|
||||
<div>
|
||||
An unexpected error occurred while trying to request test Ether from our faucet.
|
||||
{' '}Please refresh the page and try again.
|
||||
</div>
|
||||
);
|
||||
|
||||
case BalanceErrs.faucetQueueIsFull:
|
||||
return (
|
||||
<div>
|
||||
Our test Ether faucet queue is full. Please try requesting test Ether again later.
|
||||
</div>
|
||||
);
|
||||
|
||||
case BalanceErrs.mintingFailed:
|
||||
return (
|
||||
<div>
|
||||
Minting your test tokens failed unexpectedly. Please refresh the page and try again.
|
||||
</div>
|
||||
);
|
||||
|
||||
case BalanceErrs.wethConversionFailed:
|
||||
return (
|
||||
<div>
|
||||
Converting between Ether and Ether Tokens failed unexpectedly.
|
||||
Please refresh the page and try again.
|
||||
</div>
|
||||
);
|
||||
|
||||
case BalanceErrs.allowanceSettingFailed:
|
||||
return (
|
||||
<div>
|
||||
An unexpected error occurred while trying to set your test token allowance.
|
||||
{' '}Please refresh the page and try again.
|
||||
</div>
|
||||
);
|
||||
|
||||
case undefined:
|
||||
return null; // No error to show
|
||||
|
||||
default:
|
||||
throw utils.spawnSwitchErr('errorType', this.state.errorType);
|
||||
}
|
||||
}
|
||||
private renderDharmaLoanFrame() {
|
||||
if (utils.isUserOnMobile()) {
|
||||
return (
|
||||
<h4 style={{ textAlign: 'center' }}>
|
||||
We apologize -- Dharma loan requests are not available on
|
||||
mobile yet. Please try again through your desktop browser.
|
||||
</h4>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<DharmaLoanFrame
|
||||
partner="0x"
|
||||
env={utils.getCurrentEnvironment()}
|
||||
screenWidth={this.props.screenWidth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
private onErrorOccurred(errorType: BalanceErrs) {
|
||||
this.setState({
|
||||
errorType,
|
||||
});
|
||||
}
|
||||
private async onMintTestTokensAsync(token: Token): Promise<boolean> {
|
||||
try {
|
||||
await this.props.blockchain.mintTestTokensAsync(token);
|
||||
const amount = ZeroEx.toUnitAmount(constants.MINT_AMOUNT, token.decimals);
|
||||
this.props.dispatcher.showFlashMessage(`Successfully minted ${amount.toString(10)} ${token.symbol}`);
|
||||
return true;
|
||||
} catch (err) {
|
||||
const errMsg = '' + err;
|
||||
if (_.includes(errMsg, BlockchainCallErrs.USER_HAS_NO_ASSOCIATED_ADDRESSES)) {
|
||||
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
|
||||
return false;
|
||||
}
|
||||
if (_.includes(errMsg, 'User denied transaction')) {
|
||||
return false;
|
||||
}
|
||||
utils.consoleLog(`Unexpected error encountered: ${err}`);
|
||||
utils.consoleLog(err.stack);
|
||||
await errorReporter.reportAsync(err);
|
||||
this.setState({
|
||||
errorType: BalanceErrs.mintingFailed,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
private async faucetRequestAsync(isEtherRequest: boolean): Promise<boolean> {
|
||||
if (this.props.userAddress === '') {
|
||||
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If on another network other then the testnet our faucet serves test ether
|
||||
// from, we must show user an error message
|
||||
if (this.props.blockchain.networkId !== constants.TESTNET_NETWORK_ID) {
|
||||
this.setState({
|
||||
errorType: BalanceErrs.incorrectNetworkForFaucet,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
await utils.sleepAsync(ARTIFICIAL_FAUCET_REQUEST_DELAY);
|
||||
|
||||
const segment = isEtherRequest ? 'ether' : 'zrx';
|
||||
const response = await fetch(`${constants.ETHER_FAUCET_ENDPOINT}/${segment}/${this.props.userAddress}`);
|
||||
const responseBody = await response.text();
|
||||
if (response.status !== constants.SUCCESS_STATUS) {
|
||||
utils.consoleLog(`Unexpected status code: ${response.status} -> ${responseBody}`);
|
||||
await errorReporter.reportAsync(new Error(`Faucet returned non-200: ${JSON.stringify(response)}`));
|
||||
const errorType = response.status === constants.UNAVAILABLE_STATUS ?
|
||||
BalanceErrs.faucetQueueIsFull :
|
||||
BalanceErrs.faucetRequestFailed;
|
||||
this.setState({
|
||||
errorType,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isEtherRequest) {
|
||||
this.setState({
|
||||
isBalanceSpinnerVisible: true,
|
||||
});
|
||||
} else {
|
||||
const tokens = _.values(this.props.tokenByAddress);
|
||||
const zrxToken = _.find(tokens, t => t.symbol === ZRX_TOKEN_SYMBOL);
|
||||
const zrxTokenState = this.props.tokenStateByAddress[zrxToken.address];
|
||||
this.setState({
|
||||
isZRXSpinnerVisible: true,
|
||||
currentZrxBalance: zrxTokenState.balance,
|
||||
});
|
||||
this.props.blockchain.pollTokenBalanceAsync(zrxToken);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
private onErrorDialogToggle(isOpen: boolean) {
|
||||
this.setState({
|
||||
errorType: undefined,
|
||||
});
|
||||
}
|
||||
private onDharmaDialogToggle() {
|
||||
this.setState({
|
||||
isDharmaDialogVisible: !this.state.isDharmaDialogVisible,
|
||||
});
|
||||
}
|
||||
private getWrappedEthToken() {
|
||||
const tokens = _.values(this.props.tokenByAddress);
|
||||
const wrappedEthToken = _.find(tokens, {symbol: ETHER_TOKEN_SYMBOL});
|
||||
return wrappedEthToken;
|
||||
}
|
||||
private onAddTokenClicked() {
|
||||
this.setState({
|
||||
isTokenPickerOpen: true,
|
||||
isAddingToken: true,
|
||||
});
|
||||
}
|
||||
private onRemoveTokenClicked() {
|
||||
this.setState({
|
||||
isTokenPickerOpen: true,
|
||||
isAddingToken: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
370
packages/website/ts/components/top_bar.tsx
Normal file
370
packages/website/ts/components/top_bar.tsx
Normal file
@@ -0,0 +1,370 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Link as ScrollLink,
|
||||
animateScroll,
|
||||
} from 'react-scroll';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {HashLink} from 'react-router-hash-link';
|
||||
import AppBar from 'material-ui/AppBar';
|
||||
import Drawer from 'material-ui/Drawer';
|
||||
import MenuItem from 'material-ui/MenuItem';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import ReactTooltip = require('react-tooltip');
|
||||
import {configs} from 'ts/utils/configs';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {Identicon} from 'ts/components/ui/identicon';
|
||||
import {NestedSidebarMenu} from 'ts/pages/shared/nested_sidebar_menu';
|
||||
import {typeDocUtils} from 'ts/utils/typedoc_utils';
|
||||
import {PortalMenu} from 'ts/components/portal_menu';
|
||||
import {Styles, TypeDocNode, MenuSubsectionsBySection, WebsitePaths, Docs} from 'ts/types';
|
||||
import {TopBarMenuItem} from 'ts/components/top_bar_menu_item';
|
||||
import {DropDownMenuItem} from 'ts/components/ui/drop_down_menu_item';
|
||||
|
||||
const CUSTOM_DARK_GRAY = '#231F20';
|
||||
const SECTION_HEADER_COLOR = 'rgb(234, 234, 234)';
|
||||
|
||||
interface TopBarProps {
|
||||
userAddress?: string;
|
||||
blockchainIsLoaded: boolean;
|
||||
location: Location;
|
||||
docsVersion?: string;
|
||||
availableDocVersions?: string[];
|
||||
menuSubsectionsBySection?: MenuSubsectionsBySection;
|
||||
shouldFullWidth?: boolean;
|
||||
doc?: Docs;
|
||||
style?: React.CSSProperties;
|
||||
isNightVersion?: boolean;
|
||||
}
|
||||
|
||||
interface TopBarState {
|
||||
isDrawerOpen: boolean;
|
||||
}
|
||||
|
||||
const styles: Styles = {
|
||||
address: {
|
||||
marginRight: 12,
|
||||
overflow: 'hidden',
|
||||
paddingTop: 4,
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: 70,
|
||||
},
|
||||
addressPopover: {
|
||||
backgroundColor: colors.blueGrey500,
|
||||
color: 'white',
|
||||
padding: 3,
|
||||
},
|
||||
topBar: {
|
||||
backgroundColor: 'white',
|
||||
height: 59,
|
||||
width: '100%',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
zIndex: 1100,
|
||||
paddingBottom: 1,
|
||||
},
|
||||
bottomBar: {
|
||||
boxShadow: 'rgba(0, 0, 0, 0.187647) 0px 1px 3px',
|
||||
},
|
||||
menuItem: {
|
||||
fontSize: 14,
|
||||
color: CUSTOM_DARK_GRAY,
|
||||
paddingTop: 6,
|
||||
paddingBottom: 6,
|
||||
marginTop: 17,
|
||||
cursor: 'pointer',
|
||||
fontWeight: 400,
|
||||
},
|
||||
};
|
||||
|
||||
export class TopBar extends React.Component<TopBarProps, TopBarState> {
|
||||
public static defaultProps: Partial<TopBarProps> = {
|
||||
shouldFullWidth: false,
|
||||
style: {},
|
||||
isNightVersion: false,
|
||||
};
|
||||
constructor(props: TopBarProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isDrawerOpen: false,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const isNightVersion = this.props.isNightVersion;
|
||||
const isFullWidthPage = this.props.shouldFullWidth;
|
||||
const parentClassNames = `flex mx-auto ${isFullWidthPage ? 'pl2' : 'max-width-4'}`;
|
||||
const developerSectionMenuItems = [
|
||||
<Link
|
||||
key="subMenuItem-zeroEx"
|
||||
to={WebsitePaths.ZeroExJs}
|
||||
className="text-decoration-none"
|
||||
>
|
||||
<MenuItem style={{fontSize: styles.menuItem.fontSize}} primaryText="0x.js" />
|
||||
</Link>,
|
||||
<Link
|
||||
key="subMenuItem-smartContracts"
|
||||
to={WebsitePaths.SmartContracts}
|
||||
className="text-decoration-none"
|
||||
>
|
||||
<MenuItem style={{fontSize: styles.menuItem.fontSize}} primaryText="Smart Contracts" />
|
||||
</Link>,
|
||||
<a
|
||||
key="subMenuItem-standard-relayer-api"
|
||||
target="_blank"
|
||||
className="text-decoration-none"
|
||||
href={constants.STANDARD_RELAYER_API_GITHUB}
|
||||
>
|
||||
<MenuItem style={{fontSize: styles.menuItem.fontSize}} primaryText="Standard Relayer API" />
|
||||
</a>,
|
||||
<a
|
||||
key="subMenuItem-github"
|
||||
target="_blank"
|
||||
className="text-decoration-none"
|
||||
href={constants.GITHUB_URL}
|
||||
>
|
||||
<MenuItem style={{ fontSize: styles.menuItem.fontSize }} primaryText="GitHub" />
|
||||
</a>,
|
||||
<a
|
||||
key="subMenuItem-whitePaper"
|
||||
target="_blank"
|
||||
className="text-decoration-none"
|
||||
href={`${WebsitePaths.Whitepaper}`}
|
||||
>
|
||||
<MenuItem style={{fontSize: styles.menuItem.fontSize}} primaryText="Whitepaper" />
|
||||
</a>,
|
||||
];
|
||||
const bottomBorderStyle = this.shouldDisplayBottomBar() ? styles.bottomBar : {};
|
||||
const fullWithClassNames = isFullWidthPage ? 'pr4' : '';
|
||||
const logoUrl = isNightVersion ? '/images/protocol_logo_white.png' : '/images/protocol_logo_black.png';
|
||||
return (
|
||||
<div style={{...styles.topBar, ...bottomBorderStyle, ...this.props.style}} className="pb1">
|
||||
<div className={parentClassNames}>
|
||||
<div className="col col-2 sm-pl2 md-pl2 lg-pl0" style={{paddingTop: 15}}>
|
||||
<Link to={`${WebsitePaths.Home}`} className="text-decoration-none">
|
||||
<img src={logoUrl} height="30" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className={`col col-${isFullWidthPage ? '8' : '9'} lg-hide md-hide`} />
|
||||
<div className={`col col-${isFullWidthPage ? '6' : '5'} sm-hide xs-hide`} />
|
||||
{!this.isViewingPortal() &&
|
||||
<div className={`col col-${isFullWidthPage ? '4' : '5'} ${fullWithClassNames} lg-pr0 md-pr2 sm-hide xs-hide`}>
|
||||
<div className="flex justify-between">
|
||||
<DropDownMenuItem
|
||||
title="Developers"
|
||||
subMenuItems={developerSectionMenuItems}
|
||||
style={styles.menuItem}
|
||||
isNightVersion={isNightVersion}
|
||||
/>
|
||||
<TopBarMenuItem
|
||||
title="Wiki"
|
||||
path={`${WebsitePaths.Wiki}`}
|
||||
style={styles.menuItem}
|
||||
isNightVersion={isNightVersion}
|
||||
/>
|
||||
<TopBarMenuItem
|
||||
title="About"
|
||||
path={`${WebsitePaths.About}`}
|
||||
style={styles.menuItem}
|
||||
isNightVersion={isNightVersion}
|
||||
/>
|
||||
<TopBarMenuItem
|
||||
title="Portal DApp"
|
||||
path={`${WebsitePaths.Portal}`}
|
||||
isPrimary={true}
|
||||
style={styles.menuItem}
|
||||
className={`${isFullWidthPage && 'md-hide'}`}
|
||||
isNightVersion={isNightVersion}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{this.props.blockchainIsLoaded && !_.isEmpty(this.props.userAddress) &&
|
||||
<div className="col col-5">
|
||||
{this.renderUser()}
|
||||
</div>
|
||||
}
|
||||
{!this.isViewingPortal() &&
|
||||
<div
|
||||
className={`col ${isFullWidthPage ? 'col-2 pl2' : 'col-1'} md-hide lg-hide`}
|
||||
>
|
||||
<div
|
||||
style={{fontSize: 25, color: isNightVersion ? 'white' : 'black', cursor: 'pointer', paddingTop: 16}}
|
||||
>
|
||||
<i
|
||||
className="zmdi zmdi-menu"
|
||||
onClick={this.onMenuButtonClick.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{this.renderDrawer()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderDrawer() {
|
||||
return (
|
||||
<Drawer
|
||||
open={this.state.isDrawerOpen}
|
||||
docked={false}
|
||||
openSecondary={true}
|
||||
onRequestChange={this.onMenuButtonClick.bind(this)}
|
||||
>
|
||||
{this.renderPortalMenu()}
|
||||
{this.renderDocsMenu()}
|
||||
{this.renderWiki()}
|
||||
<div className="pl1 py1 mt3" style={{backgroundColor: SECTION_HEADER_COLOR}}>Website</div>
|
||||
<Link to={WebsitePaths.Home} className="text-decoration-none">
|
||||
<MenuItem className="py2">Home</MenuItem>
|
||||
</Link>
|
||||
<Link to={`${WebsitePaths.Wiki}`} className="text-decoration-none">
|
||||
<MenuItem className="py2">Wiki</MenuItem>
|
||||
</Link>
|
||||
{!this.isViewing0xjsDocs() &&
|
||||
<Link to={WebsitePaths.ZeroExJs} className="text-decoration-none">
|
||||
<MenuItem className="py2">0x.js Docs</MenuItem>
|
||||
</Link>
|
||||
}
|
||||
{!this.isViewingSmartContractsDocs() &&
|
||||
<Link to={WebsitePaths.SmartContracts} className="text-decoration-none">
|
||||
<MenuItem className="py2">Smart Contract Docs</MenuItem>
|
||||
</Link>
|
||||
}
|
||||
{!this.isViewingPortal() &&
|
||||
<Link to={`${WebsitePaths.Portal}`} className="text-decoration-none">
|
||||
<MenuItem className="py2">Portal DApp</MenuItem>
|
||||
</Link>
|
||||
}
|
||||
<a
|
||||
className="text-decoration-none"
|
||||
target="_blank"
|
||||
href={`${WebsitePaths.Whitepaper}`}
|
||||
>
|
||||
<MenuItem className="py2">Whitepaper</MenuItem>
|
||||
</a>
|
||||
<Link to={`${WebsitePaths.About}`} className="text-decoration-none">
|
||||
<MenuItem className="py2">About</MenuItem>
|
||||
</Link>
|
||||
<a
|
||||
className="text-decoration-none"
|
||||
target="_blank"
|
||||
href={constants.BLOG_URL}
|
||||
>
|
||||
<MenuItem className="py2">Blog</MenuItem>
|
||||
</a>
|
||||
<Link to={`${WebsitePaths.FAQ}`} className="text-decoration-none">
|
||||
<MenuItem
|
||||
className="py2"
|
||||
onTouchTap={this.onMenuButtonClick.bind(this)}
|
||||
>
|
||||
FAQ
|
||||
</MenuItem>
|
||||
</Link>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
private renderDocsMenu() {
|
||||
if (!this.isViewing0xjsDocs() && !this.isViewingSmartContractsDocs()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const topLevelMenu = this.isViewing0xjsDocs() ?
|
||||
typeDocUtils.getFinal0xjsMenu(this.props.docsVersion) :
|
||||
constants.menuSmartContracts;
|
||||
|
||||
const sectionTitle = this.isViewing0xjsDocs() ? '0x.js Docs' : 'Smart contract Docs';
|
||||
return (
|
||||
<div className="lg-hide md-hide">
|
||||
<div className="pl1 py1" style={{backgroundColor: SECTION_HEADER_COLOR}}>{sectionTitle}</div>
|
||||
<NestedSidebarMenu
|
||||
topLevelMenu={topLevelMenu}
|
||||
menuSubsectionsBySection={this.props.menuSubsectionsBySection}
|
||||
shouldDisplaySectionHeaders={false}
|
||||
onMenuItemClick={this.onMenuButtonClick.bind(this)}
|
||||
selectedVersion={this.props.docsVersion}
|
||||
doc={this.props.doc}
|
||||
versions={this.props.availableDocVersions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderWiki() {
|
||||
if (!this.isViewingWiki()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="lg-hide md-hide">
|
||||
<div className="pl1 py1" style={{backgroundColor: SECTION_HEADER_COLOR}}>0x Protocol Wiki</div>
|
||||
<NestedSidebarMenu
|
||||
topLevelMenu={this.props.menuSubsectionsBySection}
|
||||
menuSubsectionsBySection={this.props.menuSubsectionsBySection}
|
||||
shouldDisplaySectionHeaders={false}
|
||||
onMenuItemClick={this.onMenuButtonClick.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderPortalMenu() {
|
||||
if (!this.isViewingPortal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="lg-hide md-hide">
|
||||
<div className="pl1 py1" style={{backgroundColor: SECTION_HEADER_COLOR}}>Portal DApp</div>
|
||||
<PortalMenu
|
||||
menuItemStyle={{color: 'black'}}
|
||||
onClick={this.onMenuButtonClick.bind(this)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderUser() {
|
||||
const userAddress = this.props.userAddress;
|
||||
const identiconDiameter = 26;
|
||||
return (
|
||||
<div
|
||||
className="flex right lg-pr0 md-pr2 sm-pr2"
|
||||
style={{paddingTop: 16}}
|
||||
>
|
||||
<div
|
||||
style={styles.address}
|
||||
data-tip={true}
|
||||
data-for="userAddressTooltip"
|
||||
>
|
||||
{!_.isEmpty(userAddress) ? userAddress : ''}
|
||||
</div>
|
||||
<ReactTooltip id="userAddressTooltip">{userAddress}</ReactTooltip>
|
||||
<div>
|
||||
<Identicon address={userAddress} diameter={identiconDiameter} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private onMenuButtonClick() {
|
||||
this.setState({
|
||||
isDrawerOpen: !this.state.isDrawerOpen,
|
||||
});
|
||||
}
|
||||
private isViewingPortal() {
|
||||
return _.includes(this.props.location.pathname, WebsitePaths.Portal);
|
||||
}
|
||||
private isViewingFAQ() {
|
||||
return _.includes(this.props.location.pathname, WebsitePaths.FAQ);
|
||||
}
|
||||
private isViewing0xjsDocs() {
|
||||
return _.includes(this.props.location.pathname, WebsitePaths.ZeroExJs);
|
||||
}
|
||||
private isViewingSmartContractsDocs() {
|
||||
return _.includes(this.props.location.pathname, WebsitePaths.SmartContracts);
|
||||
}
|
||||
private isViewingWiki() {
|
||||
return _.includes(this.props.location.pathname, WebsitePaths.Wiki);
|
||||
}
|
||||
private shouldDisplayBottomBar() {
|
||||
return this.isViewingWiki() || this.isViewing0xjsDocs() || this.isViewingFAQ() ||
|
||||
this.isViewingSmartContractsDocs();
|
||||
}
|
||||
}
|
||||
53
packages/website/ts/components/top_bar_menu_item.tsx
Normal file
53
packages/website/ts/components/top_bar_menu_item.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {Styles} from 'ts/types';
|
||||
|
||||
const CUSTOM_DARK_GRAY = '#231F20';
|
||||
const DEFAULT_STYLE = {
|
||||
color: CUSTOM_DARK_GRAY,
|
||||
};
|
||||
|
||||
interface TopBarMenuItemProps {
|
||||
title: string;
|
||||
path?: string;
|
||||
isPrimary?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
isNightVersion?: boolean;
|
||||
}
|
||||
|
||||
interface TopBarMenuItemState {}
|
||||
|
||||
export class TopBarMenuItem extends React.Component<TopBarMenuItemProps, TopBarMenuItemState> {
|
||||
public static defaultProps: Partial<TopBarMenuItemProps> = {
|
||||
isPrimary: false,
|
||||
style: DEFAULT_STYLE,
|
||||
className: '',
|
||||
isNightVersion: false,
|
||||
};
|
||||
public render() {
|
||||
const primaryStyles = this.props.isPrimary ? {
|
||||
borderRadius: 4,
|
||||
border: `1px solid ${this.props.isNightVersion ? '#979797' : 'rgb(230, 229, 229)'}`,
|
||||
marginTop: 15,
|
||||
paddingLeft: 9,
|
||||
paddingRight: 9,
|
||||
width: 77,
|
||||
} : {};
|
||||
const menuItemColor = this.props.isNightVersion ? 'white' : this.props.style.color;
|
||||
const linkColor = _.isUndefined(menuItemColor) ?
|
||||
CUSTOM_DARK_GRAY :
|
||||
menuItemColor;
|
||||
return (
|
||||
<div
|
||||
className={`center ${this.props.className}`}
|
||||
style={{...this.props.style, ...primaryStyles, color: menuItemColor}}
|
||||
>
|
||||
<Link to={this.props.path} className="text-decoration-none" style={{color: linkColor}}>
|
||||
{this.props.title}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
65
packages/website/ts/components/track_token_confirmation.tsx
Normal file
65
packages/website/ts/components/track_token_confirmation.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import FlatButton from 'material-ui/FlatButton';
|
||||
import Dialog from 'material-ui/Dialog';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {Party} from 'ts/components/ui/party';
|
||||
import {Token, TokenByAddress} from 'ts/types';
|
||||
|
||||
interface TrackTokenConfirmationProps {
|
||||
tokens: Token[];
|
||||
tokenByAddress: TokenByAddress;
|
||||
networkId: number;
|
||||
isAddingTokenToTracked: boolean;
|
||||
}
|
||||
|
||||
interface TrackTokenConfirmationState {}
|
||||
|
||||
export class TrackTokenConfirmation extends
|
||||
React.Component<TrackTokenConfirmationProps, TrackTokenConfirmationState> {
|
||||
public render() {
|
||||
const isMultipleTokens = this.props.tokens.length > 1;
|
||||
const allTokens = _.values(this.props.tokenByAddress);
|
||||
return (
|
||||
<div style={{color: colors.grey700}}>
|
||||
{this.props.isAddingTokenToTracked ?
|
||||
<div className="py4 my4 center">
|
||||
<span className="pr1">
|
||||
<i className="zmdi zmdi-spinner zmdi-hc-spin" />
|
||||
</span>
|
||||
<span>Adding token{isMultipleTokens && 's'}...</span>
|
||||
</div> :
|
||||
<div>
|
||||
<div>
|
||||
You do not currently track the following token{isMultipleTokens && 's'}:
|
||||
</div>
|
||||
<div className="py2 clearfix mx-auto center" style={{width: 355}}>
|
||||
{_.map(this.props.tokens, (token: Token) => (
|
||||
<div
|
||||
key={`token-profile-${token.name}`}
|
||||
className={`col col-${isMultipleTokens ? '6' : '12'} px2`}
|
||||
>
|
||||
<Party
|
||||
label={token.name}
|
||||
address={token.address}
|
||||
networkId={this.props.networkId}
|
||||
alternativeImage={token.iconUrl}
|
||||
isInTokenRegistry={token.isRegistered}
|
||||
hasUniqueNameAndSymbol={utils.hasUniqueNameAndSymbol(allTokens, token)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
Tracking a token adds it to the balances section of 0x Portal and
|
||||
allows you to generate/fill orders involving the token
|
||||
{isMultipleTokens && 's'}. Would you like to start tracking{' '}
|
||||
{isMultipleTokens ? 'these' : 'this'}{' '}token?
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
115
packages/website/ts/components/trade_history/trade_history.tsx
Normal file
115
packages/website/ts/components/trade_history/trade_history.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import Paper from 'material-ui/Paper';
|
||||
import Divider from 'material-ui/Divider';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {Fill, TokenByAddress} from 'ts/types';
|
||||
import {TradeHistoryItem} from 'ts/components/trade_history/trade_history_item';
|
||||
import {tradeHistoryStorage} from 'ts/local_storage/trade_history_storage';
|
||||
|
||||
const FILL_POLLING_INTERVAL = 1000;
|
||||
|
||||
interface TradeHistoryProps {
|
||||
tokenByAddress: TokenByAddress;
|
||||
userAddress: string;
|
||||
networkId: number;
|
||||
}
|
||||
|
||||
interface TradeHistoryState {
|
||||
sortedFills: Fill[];
|
||||
}
|
||||
|
||||
export class TradeHistory extends React.Component<TradeHistoryProps, TradeHistoryState> {
|
||||
private fillPollingIntervalId: number;
|
||||
public constructor(props: TradeHistoryProps) {
|
||||
super(props);
|
||||
const sortedFills = this.getSortedFills();
|
||||
this.state = {
|
||||
sortedFills,
|
||||
};
|
||||
}
|
||||
public componentWillMount() {
|
||||
this.startPollingForFills();
|
||||
}
|
||||
public componentWillUnmount() {
|
||||
this.stopPollingForFills();
|
||||
}
|
||||
public componentDidMount() {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
public render() {
|
||||
return (
|
||||
<div className="lg-px4 md-px4 sm-px2">
|
||||
<h3>Trade history</h3>
|
||||
<Divider />
|
||||
<div className="pt2" style={{height: 608, overflow: 'scroll'}}>
|
||||
{this.renderTrades()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderTrades() {
|
||||
const numNonCustomFills = this.numFillsWithoutCustomERC20Tokens();
|
||||
if (numNonCustomFills === 0) {
|
||||
return this.renderEmptyNotice();
|
||||
}
|
||||
|
||||
return _.map(this.state.sortedFills, (fill, index) => {
|
||||
return (
|
||||
<TradeHistoryItem
|
||||
key={`${fill.orderHash}-${fill.filledTakerTokenAmount}-${index}`}
|
||||
fill={fill}
|
||||
tokenByAddress={this.props.tokenByAddress}
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
private renderEmptyNotice() {
|
||||
return (
|
||||
<Paper className="mt1 p2 mx-auto center" style={{width: '80%'}}>
|
||||
No filled orders yet.
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
private numFillsWithoutCustomERC20Tokens() {
|
||||
let numNonCustomFills = 0;
|
||||
const tokens = _.values(this.props.tokenByAddress);
|
||||
_.each(this.state.sortedFills, fill => {
|
||||
const takerToken = _.find(tokens, token => {
|
||||
return token.address === fill.takerToken;
|
||||
});
|
||||
const makerToken = _.find(tokens, token => {
|
||||
return token.address === fill.makerToken;
|
||||
});
|
||||
// For now we don't show history items for orders using custom ERC20
|
||||
// tokens the client does not know how to display.
|
||||
// TODO: Try to retrieve the name/symbol of an unknown token in order to display it
|
||||
// Be sure to remove similar logic in trade_history_item.tsx
|
||||
if (!_.isUndefined(takerToken) && !_.isUndefined(makerToken)) {
|
||||
numNonCustomFills += 1;
|
||||
}
|
||||
});
|
||||
return numNonCustomFills;
|
||||
}
|
||||
private startPollingForFills() {
|
||||
this.fillPollingIntervalId = window.setInterval(() => {
|
||||
const sortedFills = this.getSortedFills();
|
||||
if (!utils.deepEqual(sortedFills, this.state.sortedFills)) {
|
||||
this.setState({
|
||||
sortedFills,
|
||||
});
|
||||
}
|
||||
}, FILL_POLLING_INTERVAL);
|
||||
}
|
||||
private stopPollingForFills() {
|
||||
clearInterval(this.fillPollingIntervalId);
|
||||
}
|
||||
private getSortedFills() {
|
||||
const fillsByHash = tradeHistoryStorage.getUserFillsByHash(this.props.userAddress, this.props.networkId);
|
||||
const fills = _.values(fillsByHash);
|
||||
const sortedFills = _.sortBy(fills, [(fill: Fill) => fill.blockTimestamp * -1]);
|
||||
return sortedFills;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import * as ReactTooltip from 'react-tooltip';
|
||||
import * as moment from 'moment';
|
||||
import Paper from 'material-ui/Paper';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {ZeroEx} from '0x.js';
|
||||
import {TokenByAddress, Fill, Token, EtherscanLinkSuffixes} from 'ts/types';
|
||||
import {Party} from 'ts/components/ui/party';
|
||||
import {EtherScanIcon} from 'ts/components/ui/etherscan_icon';
|
||||
|
||||
const PRECISION = 5;
|
||||
const IDENTICON_DIAMETER = 40;
|
||||
|
||||
interface TradeHistoryItemProps {
|
||||
fill: Fill;
|
||||
tokenByAddress: TokenByAddress;
|
||||
userAddress: string;
|
||||
networkId: number;
|
||||
}
|
||||
|
||||
interface TradeHistoryItemState {}
|
||||
|
||||
export class TradeHistoryItem extends React.Component<TradeHistoryItemProps, TradeHistoryItemState> {
|
||||
public render() {
|
||||
const fill = this.props.fill;
|
||||
const tokens = _.values(this.props.tokenByAddress);
|
||||
const takerToken = _.find(tokens, token => {
|
||||
return token.address === fill.takerToken;
|
||||
});
|
||||
const makerToken = _.find(tokens, token => {
|
||||
return token.address === fill.makerToken;
|
||||
});
|
||||
// For now we don't show history items for orders using custom ERC20
|
||||
// tokens the client does not know how to display.
|
||||
// TODO: Try to retrieve the name/symbol of an unknown token in order to display it
|
||||
// Be sure to remove similar logic in trade_history.tsx
|
||||
if (_.isUndefined(takerToken) || _.isUndefined(makerToken)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const amountColStyle: React.CSSProperties = {
|
||||
fontWeight: 100,
|
||||
display: 'inline-block',
|
||||
};
|
||||
const amountColClassNames = 'col col-12 lg-col-4 md-col-4 lg-py2 md-py2 sm-py1 lg-pr2 md-pr2 \
|
||||
lg-right-align md-right-align sm-center';
|
||||
|
||||
return (
|
||||
<Paper
|
||||
className="py1"
|
||||
style={{margin: '3px 3px 15px 3px'}}
|
||||
>
|
||||
<div className="clearfix">
|
||||
<div className="col col-12 lg-col-1 md-col-1 pt2 lg-pl3 md-pl3">
|
||||
{this.renderDate()}
|
||||
</div>
|
||||
<div
|
||||
className="col col-12 lg-col-6 md-col-6 lg-pl3 md-pl3"
|
||||
style={{fontSize: 12, fontWeight: 100}}
|
||||
>
|
||||
<div className="flex sm-mx-auto xs-mx-auto" style={{paddingTop: 4, width: 224}}>
|
||||
<Party
|
||||
label="Maker"
|
||||
address={fill.maker}
|
||||
identiconDiameter={IDENTICON_DIAMETER}
|
||||
networkId={this.props.networkId}
|
||||
/>
|
||||
<i style={{fontSize: 30}} className="zmdi zmdi-swap py3" />
|
||||
<Party
|
||||
label="Taker"
|
||||
address={fill.taker}
|
||||
identiconDiameter={IDENTICON_DIAMETER}
|
||||
networkId={this.props.networkId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={amountColClassNames}
|
||||
style={amountColStyle}
|
||||
>
|
||||
{this.renderAmounts(makerToken, takerToken)}
|
||||
</div>
|
||||
<div className="col col-12 lg-col-1 md-col-1 lg-pr3 md-pr3 lg-py3 md-py3 sm-pb1 sm-center">
|
||||
<div className="pt1 lg-right md-right sm-mx-auto" style={{width: 13}}>
|
||||
<EtherScanIcon
|
||||
addressOrTxHash={fill.transactionHash}
|
||||
networkId={this.props.networkId}
|
||||
etherscanLinkSuffixes={EtherscanLinkSuffixes.tx}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
private renderAmounts(makerToken: Token, takerToken: Token) {
|
||||
const fill = this.props.fill;
|
||||
const filledTakerTokenAmountInUnits = ZeroEx.toUnitAmount(fill.filledTakerTokenAmount, takerToken.decimals);
|
||||
const filledMakerTokenAmountInUnits = ZeroEx.toUnitAmount(fill.filledMakerTokenAmount, takerToken.decimals);
|
||||
let exchangeRate = filledTakerTokenAmountInUnits.div(filledMakerTokenAmountInUnits);
|
||||
const fillMakerTokenAmount = ZeroEx.toBaseUnitAmount(filledMakerTokenAmountInUnits, makerToken.decimals);
|
||||
|
||||
let receiveAmount;
|
||||
let receiveToken;
|
||||
let givenAmount;
|
||||
let givenToken;
|
||||
if (this.props.userAddress === fill.maker && this.props.userAddress === fill.taker) {
|
||||
receiveAmount = new BigNumber(0);
|
||||
givenAmount = new BigNumber(0);
|
||||
receiveToken = makerToken;
|
||||
givenToken = takerToken;
|
||||
} else if (this.props.userAddress === fill.maker) {
|
||||
receiveAmount = fill.filledTakerTokenAmount;
|
||||
givenAmount = fillMakerTokenAmount;
|
||||
receiveToken = takerToken;
|
||||
givenToken = makerToken;
|
||||
exchangeRate = new BigNumber(1).div(exchangeRate);
|
||||
} else if (this.props.userAddress === fill.taker) {
|
||||
receiveAmount = fillMakerTokenAmount;
|
||||
givenAmount = fill.filledTakerTokenAmount;
|
||||
receiveToken = makerToken;
|
||||
givenToken = takerToken;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{color: colors.green400, fontSize: 16}}
|
||||
>
|
||||
<span>+{' '}</span>
|
||||
{this.renderAmount(receiveAmount, receiveToken.symbol, receiveToken.decimals)}
|
||||
</div>
|
||||
<div
|
||||
className="pb1 inline-block"
|
||||
style={{color: colors.red200, fontSize: 16}}
|
||||
>
|
||||
<span>-{' '}</span>
|
||||
{this.renderAmount(givenAmount, givenToken.symbol, givenToken.decimals)}
|
||||
</div>
|
||||
<div style={{color: colors.grey400, fontSize: 14}}>
|
||||
{exchangeRate.toFixed(PRECISION)} {givenToken.symbol}/{receiveToken.symbol}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderDate() {
|
||||
const blockMoment = moment.unix(this.props.fill.blockTimestamp);
|
||||
if (!blockMoment.isValid()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dayOfMonth = blockMoment.format('D');
|
||||
const monthAbreviation = blockMoment.format('MMM');
|
||||
const formattedBlockDate = blockMoment.format('H:mmA - MMMM D, YYYY');
|
||||
const dateTooltipId = `${this.props.fill.transactionHash}-date`;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-tip={true}
|
||||
data-for={dateTooltipId}
|
||||
>
|
||||
<div className="center pt1" style={{fontSize: 13}}>{monthAbreviation}</div>
|
||||
<div className="center" style={{fontSize: 24, fontWeight: 100}}>{dayOfMonth}</div>
|
||||
<ReactTooltip id={dateTooltipId}>{formattedBlockDate}</ReactTooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderAmount(amount: BigNumber, symbol: string, decimals: number) {
|
||||
const unitAmount = ZeroEx.toUnitAmount(amount, decimals);
|
||||
return (
|
||||
<span>
|
||||
{unitAmount.toFixed(PRECISION)} {symbol}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
27
packages/website/ts/components/ui/alert.tsx
Normal file
27
packages/website/ts/components/ui/alert.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as React from 'react';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {AlertTypes} from 'ts/types';
|
||||
|
||||
const CUSTOM_GREEN = 'rgb(137, 199, 116)';
|
||||
|
||||
interface AlertProps {
|
||||
type: AlertTypes;
|
||||
message: string|React.ReactNode;
|
||||
}
|
||||
|
||||
export function Alert(props: AlertProps) {
|
||||
const isAlert = props.type === AlertTypes.ERROR;
|
||||
const errMsgStyles = {
|
||||
background: isAlert ? colors.red200 : CUSTOM_GREEN,
|
||||
color: 'white',
|
||||
marginTop: 10,
|
||||
padding: 4,
|
||||
paddingLeft: 8,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded center" style={errMsgStyles}>
|
||||
{props.message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
58
packages/website/ts/components/ui/badge.tsx
Normal file
58
packages/website/ts/components/ui/badge.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {Styles} from 'ts/types';
|
||||
|
||||
const styles: Styles = {
|
||||
badge: {
|
||||
width: 50,
|
||||
fontSize: 11,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
marginTop: 25,
|
||||
lineHeight: 0.9,
|
||||
fontFamily: 'Roboto Mono',
|
||||
marginLeft: 3,
|
||||
marginRight: 3,
|
||||
},
|
||||
};
|
||||
|
||||
interface BadgeProps {
|
||||
title: string;
|
||||
backgroundColor: string;
|
||||
}
|
||||
|
||||
interface BadgeState {
|
||||
isHovering: boolean;
|
||||
}
|
||||
|
||||
export class Badge extends React.Component<BadgeProps, BadgeState> {
|
||||
constructor(props: BadgeProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isHovering: false,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const badgeStyle = {
|
||||
...styles.badge,
|
||||
backgroundColor: this.props.backgroundColor,
|
||||
opacity: this.state.isHovering ? 0.7 : 1,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className="p1 center"
|
||||
style={badgeStyle}
|
||||
onMouseOver={this.setHoverState.bind(this, true)}
|
||||
onMouseOut={this.setHoverState.bind(this, false)}
|
||||
>
|
||||
{this.props.title}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private setHoverState(isHovering: boolean) {
|
||||
this.setState({
|
||||
isHovering,
|
||||
});
|
||||
}
|
||||
}
|
||||
81
packages/website/ts/components/ui/copy_icon.tsx
Normal file
81
packages/website/ts/components/ui/copy_icon.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import * as CopyToClipboard from 'react-copy-to-clipboard';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import ReactTooltip = require('react-tooltip');
|
||||
|
||||
interface CopyIconProps {
|
||||
data: string;
|
||||
callToAction?: string;
|
||||
}
|
||||
|
||||
interface CopyIconState {
|
||||
isHovering: boolean;
|
||||
}
|
||||
|
||||
export class CopyIcon extends React.Component<CopyIconProps, CopyIconState> {
|
||||
private copyTooltipTimeoutId: number;
|
||||
private copyable: HTMLInputElement;
|
||||
constructor(props: CopyIconProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isHovering: false,
|
||||
};
|
||||
}
|
||||
public componentDidUpdate() {
|
||||
// Remove tooltip if hover away
|
||||
if (!this.state.isHovering && this.copyTooltipTimeoutId) {
|
||||
clearInterval(this.copyTooltipTimeoutId);
|
||||
this.hideTooltip();
|
||||
}
|
||||
}
|
||||
public render() {
|
||||
return (
|
||||
<div className="inline-block">
|
||||
<CopyToClipboard text={this.props.data} onCopy={this.onCopy.bind(this)}>
|
||||
<div
|
||||
className="inline flex"
|
||||
style={{cursor: 'pointer', color: colors.amber600}}
|
||||
ref={this.setRefToProperty.bind(this)}
|
||||
data-tip={true}
|
||||
data-for="copy"
|
||||
data-event="click"
|
||||
data-iscapture={true} // This let's the click event continue to propogate
|
||||
onMouseOver={this.setHoverState.bind(this, true)}
|
||||
onMouseOut={this.setHoverState.bind(this, false)}
|
||||
>
|
||||
<div>
|
||||
<i style={{fontSize: 15}} className="zmdi zmdi-copy" />
|
||||
</div>
|
||||
{this.props.callToAction &&
|
||||
<div className="pl1">{this.props.callToAction}</div>
|
||||
}
|
||||
</div>
|
||||
</CopyToClipboard>
|
||||
<ReactTooltip id="copy">Copied!</ReactTooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private setRefToProperty(el: HTMLInputElement) {
|
||||
this.copyable = el;
|
||||
}
|
||||
private setHoverState(isHovering: boolean) {
|
||||
this.setState({
|
||||
isHovering,
|
||||
});
|
||||
}
|
||||
private onCopy() {
|
||||
if (this.copyTooltipTimeoutId) {
|
||||
clearInterval(this.copyTooltipTimeoutId);
|
||||
}
|
||||
|
||||
const tooltipLifespanMs = 1000;
|
||||
this.copyTooltipTimeoutId = window.setTimeout(() => {
|
||||
this.hideTooltip();
|
||||
}, tooltipLifespanMs);
|
||||
}
|
||||
private hideTooltip() {
|
||||
ReactTooltip.hide(ReactDOM.findDOMNode(this.copyable));
|
||||
}
|
||||
}
|
||||
117
packages/website/ts/components/ui/drop_down_menu_item.tsx
Normal file
117
packages/website/ts/components/ui/drop_down_menu_item.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
Link as ScrollLink,
|
||||
} from 'react-scroll';
|
||||
import {Link} from 'react-router-dom';
|
||||
import Popover from 'material-ui/Popover';
|
||||
import Menu from 'material-ui/Menu';
|
||||
import MenuItem from 'material-ui/MenuItem';
|
||||
import {Styles, WebsitePaths} from 'ts/types';
|
||||
|
||||
const CHECK_CLOSE_POPOVER_INTERVAL_MS = 300;
|
||||
const CUSTOM_LIGHT_GRAY = '#848484';
|
||||
const DEFAULT_STYLE = {
|
||||
fontSize: 14,
|
||||
};
|
||||
|
||||
interface DropDownMenuItemProps {
|
||||
title: string;
|
||||
subMenuItems: React.ReactNode[];
|
||||
style?: React.CSSProperties;
|
||||
menuItemStyle?: React.CSSProperties;
|
||||
isNightVersion?: boolean;
|
||||
}
|
||||
|
||||
interface DropDownMenuItemState {
|
||||
isDropDownOpen: boolean;
|
||||
anchorEl?: HTMLInputElement;
|
||||
}
|
||||
|
||||
export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, DropDownMenuItemState> {
|
||||
public static defaultProps: Partial<DropDownMenuItemProps> = {
|
||||
style: DEFAULT_STYLE,
|
||||
menuItemStyle: DEFAULT_STYLE,
|
||||
isNightVersion: false,
|
||||
};
|
||||
private isHovering: boolean;
|
||||
private popoverCloseCheckIntervalId: number;
|
||||
constructor(props: DropDownMenuItemProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isDropDownOpen: false,
|
||||
};
|
||||
}
|
||||
public componentDidMount() {
|
||||
this.popoverCloseCheckIntervalId = window.setInterval(() => {
|
||||
this.checkIfShouldClosePopover();
|
||||
}, CHECK_CLOSE_POPOVER_INTERVAL_MS);
|
||||
}
|
||||
public componentWillUnmount() {
|
||||
window.clearInterval(this.popoverCloseCheckIntervalId);
|
||||
}
|
||||
public render() {
|
||||
const colorStyle = this.props.isNightVersion ? 'white' : this.props.style.color;
|
||||
return (
|
||||
<div
|
||||
style={{...this.props.style, color: colorStyle}}
|
||||
onMouseEnter={this.onHover.bind(this)}
|
||||
onMouseLeave={this.onHoverOff.bind(this)}
|
||||
>
|
||||
<div className="flex relative">
|
||||
<div style={{paddingRight: 10}}>
|
||||
{this.props.title}
|
||||
</div>
|
||||
<div className="absolute" style={{paddingLeft: 3, right: 3, top: -2}}>
|
||||
<i className="zmdi zmdi-caret-right" style={{fontSize: 22}} />
|
||||
</div>
|
||||
</div>
|
||||
<Popover
|
||||
open={this.state.isDropDownOpen}
|
||||
anchorEl={this.state.anchorEl}
|
||||
anchorOrigin={{horizontal: 'middle', vertical: 'bottom'}}
|
||||
targetOrigin={{horizontal: 'middle', vertical: 'top'}}
|
||||
onRequestClose={this.closePopover.bind(this)}
|
||||
useLayerForClickAway={false}
|
||||
>
|
||||
<div
|
||||
onMouseEnter={this.onHover.bind(this)}
|
||||
onMouseLeave={this.onHoverOff.bind(this)}
|
||||
>
|
||||
<Menu style={{color: CUSTOM_LIGHT_GRAY}}>
|
||||
{this.props.subMenuItems}
|
||||
</Menu>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private onHover(event: React.FormEvent<HTMLInputElement>) {
|
||||
this.isHovering = true;
|
||||
this.checkIfShouldOpenPopover(event);
|
||||
}
|
||||
private checkIfShouldOpenPopover(event: React.FormEvent<HTMLInputElement>) {
|
||||
if (this.state.isDropDownOpen) {
|
||||
return; // noop
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isDropDownOpen: true,
|
||||
anchorEl: event.currentTarget,
|
||||
});
|
||||
}
|
||||
private onHoverOff(event: React.FormEvent<HTMLInputElement>) {
|
||||
this.isHovering = false;
|
||||
}
|
||||
private checkIfShouldClosePopover() {
|
||||
if (!this.state.isDropDownOpen || this.isHovering) {
|
||||
return; // noop
|
||||
}
|
||||
this.closePopover();
|
||||
}
|
||||
private closePopover() {
|
||||
this.setState({
|
||||
isDropDownOpen: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
35
packages/website/ts/components/ui/ethereum_address.tsx
Normal file
35
packages/website/ts/components/ui/ethereum_address.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import * as React from 'react';
|
||||
import {EtherScanIcon} from 'ts/components/ui/etherscan_icon';
|
||||
import ReactTooltip = require('react-tooltip');
|
||||
import {EtherscanLinkSuffixes} from 'ts/types';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
|
||||
interface EthereumAddressProps {
|
||||
address: string;
|
||||
networkId: number;
|
||||
}
|
||||
|
||||
export const EthereumAddress = (props: EthereumAddressProps) => {
|
||||
const tooltipId = `${props.address}-ethereum-address`;
|
||||
const truncatedAddress = utils.getAddressBeginAndEnd(props.address);
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="inline"
|
||||
style={{fontSize: 13}}
|
||||
data-tip={true}
|
||||
data-for={tooltipId}
|
||||
>
|
||||
{truncatedAddress}
|
||||
</div>
|
||||
<div className="pl1 inline">
|
||||
<EtherScanIcon
|
||||
addressOrTxHash={props.address}
|
||||
networkId={props.networkId}
|
||||
etherscanLinkSuffixes={EtherscanLinkSuffixes.address}
|
||||
/>
|
||||
</div>
|
||||
<ReactTooltip id={tooltipId}>{props.address}</ReactTooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
50
packages/website/ts/components/ui/etherscan_icon.tsx
Normal file
50
packages/website/ts/components/ui/etherscan_icon.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import ReactTooltip = require('react-tooltip');
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {EtherscanLinkSuffixes} from 'ts/types';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
|
||||
interface EtherScanIconProps {
|
||||
addressOrTxHash: string;
|
||||
etherscanLinkSuffixes: EtherscanLinkSuffixes;
|
||||
networkId: number;
|
||||
}
|
||||
|
||||
export const EtherScanIcon = (props: EtherScanIconProps) => {
|
||||
const etherscanLinkIfExists = utils.getEtherScanLinkIfExists(
|
||||
props.addressOrTxHash, props.networkId, EtherscanLinkSuffixes.address,
|
||||
);
|
||||
const transactionTooltipId = `${props.addressOrTxHash}-etherscan-icon-tooltip`;
|
||||
return (
|
||||
<div className="inline">
|
||||
{!_.isUndefined(etherscanLinkIfExists) ?
|
||||
<a
|
||||
href={etherscanLinkIfExists}
|
||||
target="_blank"
|
||||
>
|
||||
{renderIcon()}
|
||||
</a> :
|
||||
<div
|
||||
className="inline"
|
||||
data-tip={true}
|
||||
data-for={transactionTooltipId}
|
||||
>
|
||||
{renderIcon()}
|
||||
<ReactTooltip id={transactionTooltipId}>
|
||||
Your network (id: {props.networkId}) is not supported by Etherscan
|
||||
</ReactTooltip>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function renderIcon() {
|
||||
return (
|
||||
<i
|
||||
style={{color: colors.amber600}}
|
||||
className="zmdi zmdi-open-in-new"
|
||||
/>
|
||||
);
|
||||
}
|
||||
35
packages/website/ts/components/ui/fake_text_field.tsx
Normal file
35
packages/website/ts/components/ui/fake_text_field.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import * as React from 'react';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {InputLabel} from 'ts/components/ui/input_label';
|
||||
import {Styles} from 'ts/types';
|
||||
|
||||
const styles: Styles = {
|
||||
hr: {
|
||||
borderBottom: '1px solid rgb(224, 224, 224)',
|
||||
borderLeft: 'none rgb(224, 224, 224)',
|
||||
borderRight: 'none rgb(224, 224, 224)',
|
||||
borderTop: 'none rgb(224, 224, 224)',
|
||||
bottom: 6,
|
||||
boxSizing: 'content-box',
|
||||
margin: 0,
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
},
|
||||
};
|
||||
|
||||
interface FakeTextFieldProps {
|
||||
label?: React.ReactNode | string;
|
||||
children?: any;
|
||||
}
|
||||
|
||||
export function FakeTextField(props: FakeTextFieldProps) {
|
||||
return (
|
||||
<div className="relative">
|
||||
{props.label !== '' && <InputLabel text={props.label} />}
|
||||
<div className="pb2" style={{height: 23}}>
|
||||
{props.children}
|
||||
</div>
|
||||
<hr style={styles.hr} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
40
packages/website/ts/components/ui/flash_message.tsx
Normal file
40
packages/website/ts/components/ui/flash_message.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import Snackbar from 'material-ui/Snackbar';
|
||||
import {Dispatcher} from 'ts/redux/dispatcher';
|
||||
|
||||
const SHOW_DURATION_MS = 4000;
|
||||
|
||||
interface FlashMessageProps {
|
||||
dispatcher: Dispatcher;
|
||||
flashMessage?: string|React.ReactNode;
|
||||
showDurationMs?: number;
|
||||
bodyStyle?: React.CSSProperties;
|
||||
}
|
||||
|
||||
interface FlashMessageState {}
|
||||
|
||||
export class FlashMessage extends React.Component<FlashMessageProps, FlashMessageState> {
|
||||
public static defaultProps: Partial<FlashMessageProps> = {
|
||||
showDurationMs: SHOW_DURATION_MS,
|
||||
bodyStyle: {},
|
||||
};
|
||||
public render() {
|
||||
if (!_.isUndefined(this.props.flashMessage)) {
|
||||
return (
|
||||
<Snackbar
|
||||
open={true}
|
||||
message={this.props.flashMessage}
|
||||
autoHideDuration={this.props.showDurationMs}
|
||||
onRequestClose={this.onClose.bind(this)}
|
||||
bodyStyle={this.props.bodyStyle}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
private onClose() {
|
||||
this.props.dispatcher.hideFlashMessage();
|
||||
}
|
||||
}
|
||||
22
packages/website/ts/components/ui/help_tooltip.tsx
Normal file
22
packages/website/ts/components/ui/help_tooltip.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from 'react';
|
||||
import ReactTooltip = require('react-tooltip');
|
||||
|
||||
interface HelpTooltipProps {
|
||||
style?: React.CSSProperties;
|
||||
explanation: React.ReactNode;
|
||||
}
|
||||
|
||||
export const HelpTooltip = (props: HelpTooltipProps) => {
|
||||
return (
|
||||
<div
|
||||
style={{...props.style}}
|
||||
className="inline-block"
|
||||
data-tip={props.explanation}
|
||||
data-for="helpTooltip"
|
||||
data-multiline={true}
|
||||
>
|
||||
<i style={{fontSize: 16}} className="zmdi zmdi-help" />
|
||||
<ReactTooltip id="helpTooltip" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
36
packages/website/ts/components/ui/identicon.tsx
Normal file
36
packages/website/ts/components/ui/identicon.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import blockies = require('blockies');
|
||||
|
||||
interface IdenticonProps {
|
||||
address: string;
|
||||
diameter: number;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
interface IdenticonState {}
|
||||
|
||||
export class Identicon extends React.Component<IdenticonProps, IdenticonState> {
|
||||
public static defaultProps: Partial<IdenticonProps> = {
|
||||
style: {},
|
||||
};
|
||||
public render() {
|
||||
let address = this.props.address;
|
||||
if (_.isEmpty(address)) {
|
||||
address = constants.NULL_ADDRESS;
|
||||
}
|
||||
const diameter = this.props.diameter;
|
||||
const icon = blockies({
|
||||
seed: address.toLowerCase(),
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className="circle mx-auto relative transitionFix"
|
||||
style={{width: diameter, height: diameter, overflow: 'hidden', ...this.props.style}}
|
||||
>
|
||||
<img src={icon.toDataURL()} style={{width: diameter, height: diameter, imageRendering: 'pixelated'}}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
27
packages/website/ts/components/ui/input_label.tsx
Normal file
27
packages/website/ts/components/ui/input_label.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as React from 'react';
|
||||
import {colors} from 'material-ui/styles';
|
||||
|
||||
export interface InputLabelProps {
|
||||
text: string | Element | React.ReactNode;
|
||||
}
|
||||
|
||||
const styles = {
|
||||
label: {
|
||||
color: colors.grey500,
|
||||
fontSize: 12,
|
||||
pointerEvents: 'none',
|
||||
textAlign: 'left',
|
||||
transform: 'scale(0.75) translate(0px, -28px)',
|
||||
transformOrigin: 'left top 0px',
|
||||
transition: 'all 450ms cubic-bezier(0.23, 1, 0.32, 1) 0ms',
|
||||
userSelect: 'none',
|
||||
width: 240,
|
||||
zIndex: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const InputLabel = (props: InputLabelProps) => {
|
||||
return (
|
||||
<label style={styles.label}>{props.text}</label>
|
||||
);
|
||||
};
|
||||
76
packages/website/ts/components/ui/labeled_switcher.tsx
Normal file
76
packages/website/ts/components/ui/labeled_switcher.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {colors} from 'material-ui/styles';
|
||||
|
||||
const CUSTOM_BLUE = '#63A6F1';
|
||||
|
||||
interface LabeledSwitcherProps {
|
||||
labelLeft: string;
|
||||
labelRight: string;
|
||||
isLeftInitiallySelected: boolean;
|
||||
onLeftLabelClickAsync: () => Promise<boolean>;
|
||||
onRightLabelClickAsync: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
interface LabeledSwitcherState {
|
||||
isLeftSelected: boolean;
|
||||
}
|
||||
|
||||
export class LabeledSwitcher extends React.Component<LabeledSwitcherProps, LabeledSwitcherState> {
|
||||
constructor(props: LabeledSwitcherProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isLeftSelected: props.isLeftInitiallySelected,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const isLeft = true;
|
||||
return (
|
||||
<div
|
||||
className="rounded clearfix"
|
||||
>
|
||||
{this.renderLabel(this.props.labelLeft, isLeft, this.state.isLeftSelected)}
|
||||
{this.renderLabel(this.props.labelRight, !isLeft, !this.state.isLeftSelected)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderLabel(title: string, isLeft: boolean, isSelected: boolean) {
|
||||
const borderStyle = `2px solid ${isSelected ? '#4F8BCF' : '#DADADA'}`;
|
||||
const style = {
|
||||
cursor: 'pointer',
|
||||
backgroundColor: isSelected ? CUSTOM_BLUE : colors.grey200,
|
||||
color: isSelected ? 'white' : '#A5A5A5',
|
||||
boxShadow: isSelected ? `inset 0px 0px 4px #4083CE` : 'inset 0px 0px 4px #F7F6F6',
|
||||
borderTop: borderStyle,
|
||||
borderBottom: borderStyle,
|
||||
[isLeft ? 'borderLeft' : 'borderRight']: borderStyle,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={`col col-6 center p1 ${isLeft ? 'rounded-left' : 'rounded-right'}`}
|
||||
style={style}
|
||||
onClick={this.onLabelClickAsync.bind(this, isLeft)}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private async onLabelClickAsync(isLeft: boolean): Promise<void> {
|
||||
this.setState({
|
||||
isLeftSelected: isLeft,
|
||||
});
|
||||
let didSucceed;
|
||||
if (isLeft) {
|
||||
didSucceed = await this.props.onLeftLabelClickAsync();
|
||||
} else {
|
||||
didSucceed = await this.props.onRightLabelClickAsync();
|
||||
}
|
||||
if (!didSucceed) {
|
||||
this.setState({
|
||||
isLeftSelected: !isLeft,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
105
packages/website/ts/components/ui/lifecycle_raised_button.tsx
Normal file
105
packages/website/ts/components/ui/lifecycle_raised_button.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {Token} from 'ts/types';
|
||||
import {Blockchain} from 'ts/blockchain';
|
||||
import RaisedButton from 'material-ui/RaisedButton';
|
||||
|
||||
const COMPLETE_STATE_SHOW_LENGTH_MS = 2000;
|
||||
|
||||
enum ButtonState {
|
||||
READY,
|
||||
LOADING,
|
||||
COMPLETE,
|
||||
};
|
||||
|
||||
interface LifeCycleRaisedButtonProps {
|
||||
isHidden?: boolean;
|
||||
isDisabled?: boolean;
|
||||
isPrimary?: boolean;
|
||||
labelReady: React.ReactNode|string;
|
||||
labelLoading: React.ReactNode|string;
|
||||
labelComplete: React.ReactNode|string;
|
||||
onClickAsyncFn: () => boolean;
|
||||
backgroundColor?: string;
|
||||
labelColor?: string;
|
||||
}
|
||||
|
||||
interface LifeCycleRaisedButtonState {
|
||||
buttonState: ButtonState;
|
||||
}
|
||||
|
||||
export class LifeCycleRaisedButton extends
|
||||
React.Component<LifeCycleRaisedButtonProps, LifeCycleRaisedButtonState> {
|
||||
public static defaultProps: Partial<LifeCycleRaisedButtonProps> = {
|
||||
isDisabled: false,
|
||||
backgroundColor: 'white',
|
||||
labelColor: 'rgb(97, 97, 97)',
|
||||
};
|
||||
private buttonTimeoutId: number;
|
||||
private didUnmount: boolean;
|
||||
constructor(props: LifeCycleRaisedButtonProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
buttonState: ButtonState.READY,
|
||||
};
|
||||
}
|
||||
public componentWillUnmount() {
|
||||
clearTimeout(this.buttonTimeoutId);
|
||||
this.didUnmount = true;
|
||||
}
|
||||
public render() {
|
||||
if (this.props.isHidden === true) {
|
||||
return <span />;
|
||||
}
|
||||
|
||||
let label;
|
||||
switch (this.state.buttonState) {
|
||||
case ButtonState.READY:
|
||||
label = this.props.labelReady;
|
||||
break;
|
||||
case ButtonState.LOADING:
|
||||
label = this.props.labelLoading;
|
||||
break;
|
||||
case ButtonState.COMPLETE:
|
||||
label = this.props.labelComplete;
|
||||
break;
|
||||
default:
|
||||
throw utils.spawnSwitchErr('ButtonState', this.state.buttonState);
|
||||
}
|
||||
return (
|
||||
<RaisedButton
|
||||
primary={this.props.isPrimary}
|
||||
label={label}
|
||||
style={{width: '100%'}}
|
||||
backgroundColor={this.props.backgroundColor}
|
||||
labelColor={this.props.labelColor}
|
||||
onTouchTap={this.onClickAsync.bind(this)}
|
||||
disabled={this.props.isDisabled || this.state.buttonState !== ButtonState.READY}
|
||||
/>
|
||||
);
|
||||
}
|
||||
public async onClickAsync() {
|
||||
this.setState({
|
||||
buttonState: ButtonState.LOADING,
|
||||
});
|
||||
const didSucceed = await this.props.onClickAsyncFn();
|
||||
if (this.didUnmount) {
|
||||
return; // noop since unmount called before async callback returned.
|
||||
}
|
||||
if (didSucceed) {
|
||||
this.setState({
|
||||
buttonState: ButtonState.COMPLETE,
|
||||
});
|
||||
this.buttonTimeoutId = window.setTimeout(() => {
|
||||
this.setState({
|
||||
buttonState: ButtonState.READY,
|
||||
});
|
||||
}, COMPLETE_STATE_SHOW_LENGTH_MS);
|
||||
} else {
|
||||
this.setState({
|
||||
buttonState: ButtonState.READY,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
36
packages/website/ts/components/ui/loading.tsx
Normal file
36
packages/website/ts/components/ui/loading.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import Paper from 'material-ui/Paper';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {DefaultPlayer as Video} from 'react-html5video';
|
||||
import 'react-html5video/dist/styles.css';
|
||||
|
||||
interface LoadingProps {}
|
||||
|
||||
interface LoadingState {}
|
||||
|
||||
export class Loading extends React.Component<LoadingProps, LoadingState> {
|
||||
public render() {
|
||||
return (
|
||||
<div className="pt4 sm-px2 sm-pt2 sm-m1" style={{height: 500}}>
|
||||
<Paper className="mx-auto" style={{maxWidth: 400}}>
|
||||
{utils.isUserOnMobile() ?
|
||||
<img className="p1" src="/gifs/0xAnimation.gif" width="96%" /> :
|
||||
<div style={{pointerEvents: 'none'}}>
|
||||
<Video
|
||||
autoPlay={true}
|
||||
loop={true}
|
||||
muted={true}
|
||||
controls={[]}
|
||||
poster="/images/loading_poster.png"
|
||||
>
|
||||
<source src="/videos/0xAnimation.mp4" type="video/mp4" />
|
||||
</Video>
|
||||
</div>
|
||||
}
|
||||
<div className="center pt2" style={{paddingBottom: 11}}>Connecting to the blockchain...</div>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
54
packages/website/ts/components/ui/menu_item.tsx
Normal file
54
packages/website/ts/components/ui/menu_item.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {Styles} from 'ts/types';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {colors} from 'material-ui/styles';
|
||||
|
||||
interface MenuItemProps {
|
||||
to: string;
|
||||
style?: React.CSSProperties;
|
||||
onClick?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface MenuItemState {
|
||||
isHovering: boolean;
|
||||
}
|
||||
|
||||
export class MenuItem extends React.Component<MenuItemProps, MenuItemState> {
|
||||
public static defaultProps: Partial<MenuItemProps> = {
|
||||
onClick: _.noop,
|
||||
className: '',
|
||||
};
|
||||
public constructor(props: MenuItemProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isHovering: false,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const menuItemStyles = {
|
||||
cursor: 'pointer',
|
||||
opacity: this.state.isHovering ? 0.5 : 1,
|
||||
};
|
||||
return (
|
||||
<Link to={this.props.to} style={{textDecoration: 'none', ...this.props.style}}>
|
||||
<div
|
||||
onClick={this.props.onClick.bind(this)}
|
||||
className={`mx-auto ${this.props.className}`}
|
||||
style={menuItemStyles}
|
||||
onMouseEnter={this.onToggleHover.bind(this, true)}
|
||||
onMouseLeave={this.onToggleHover.bind(this, false)}
|
||||
>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
private onToggleHover(isHovering: boolean) {
|
||||
this.setState({
|
||||
isHovering,
|
||||
});
|
||||
}
|
||||
}
|
||||
150
packages/website/ts/components/ui/party.tsx
Normal file
150
packages/website/ts/components/ui/party.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import ReactTooltip = require('react-tooltip');
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {Identicon} from 'ts/components/ui/identicon';
|
||||
import {EtherscanLinkSuffixes} from 'ts/types';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {EthereumAddress} from 'ts/components/ui/ethereum_address';
|
||||
|
||||
const MIN_ADDRESS_WIDTH = 60;
|
||||
const IMAGE_DIMENSION = 100;
|
||||
const IDENTICON_DIAMETER = 95;
|
||||
const CHECK_MARK_GREEN = 'rgb(0, 195, 62)';
|
||||
|
||||
interface PartyProps {
|
||||
label: string;
|
||||
address: string;
|
||||
networkId: number;
|
||||
alternativeImage?: string;
|
||||
identiconDiameter?: number;
|
||||
identiconStyle?: React.CSSProperties;
|
||||
isInTokenRegistry?: boolean;
|
||||
hasUniqueNameAndSymbol?: boolean;
|
||||
}
|
||||
|
||||
interface PartyState {}
|
||||
|
||||
export class Party extends React.Component<PartyProps, PartyState> {
|
||||
public static defaultProps: Partial<PartyProps> = {
|
||||
identiconStyle: {},
|
||||
identiconDiameter: IDENTICON_DIAMETER,
|
||||
};
|
||||
public render() {
|
||||
const label = this.props.label;
|
||||
const address = this.props.address;
|
||||
const tooltipId = `${label}-${address}-tooltip`;
|
||||
const identiconDiameter = this.props.identiconDiameter;
|
||||
const addressWidth = identiconDiameter > MIN_ADDRESS_WIDTH ?
|
||||
identiconDiameter : MIN_ADDRESS_WIDTH;
|
||||
const emptyIdenticonStyles = {
|
||||
width: identiconDiameter,
|
||||
height: identiconDiameter,
|
||||
backgroundColor: 'lightgray',
|
||||
marginTop: 13,
|
||||
marginBottom: 10,
|
||||
};
|
||||
const tokenImageStyle = {
|
||||
width: IMAGE_DIMENSION,
|
||||
height: IMAGE_DIMENSION,
|
||||
};
|
||||
const etherscanLinkIfExists = utils.getEtherScanLinkIfExists(
|
||||
this.props.address, this.props.networkId, EtherscanLinkSuffixes.address,
|
||||
);
|
||||
const isRegistered = this.props.isInTokenRegistry;
|
||||
const registeredTooltipId = `${this.props.address}-${isRegistered}-registeredTooltip`;
|
||||
const uniqueNameAndSymbolTooltipId = `${this.props.address}-${isRegistered}-uniqueTooltip`;
|
||||
return (
|
||||
<div style={{overflow: 'hidden'}}>
|
||||
<div className="pb1 center">{label}</div>
|
||||
{_.isEmpty(address) ?
|
||||
<div
|
||||
className="circle mx-auto"
|
||||
style={emptyIdenticonStyles}
|
||||
/> :
|
||||
<a
|
||||
href={etherscanLinkIfExists}
|
||||
target="_blank"
|
||||
>
|
||||
{isRegistered && !_.isUndefined(this.props.alternativeImage) ?
|
||||
<img
|
||||
style={tokenImageStyle}
|
||||
src={this.props.alternativeImage}
|
||||
/> :
|
||||
<div
|
||||
className="mx-auto"
|
||||
style={{height: IMAGE_DIMENSION, width: IMAGE_DIMENSION}}
|
||||
>
|
||||
<Identicon
|
||||
address={this.props.address}
|
||||
diameter={identiconDiameter}
|
||||
style={this.props.identiconStyle}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</a>
|
||||
}
|
||||
<div
|
||||
className="mx-auto center pt1"
|
||||
>
|
||||
<div style={{height: 25}}>
|
||||
<EthereumAddress address={address} networkId={this.props.networkId} />
|
||||
</div>
|
||||
{!_.isUndefined(this.props.isInTokenRegistry) &&
|
||||
<div>
|
||||
<div
|
||||
data-tip={true}
|
||||
data-for={registeredTooltipId}
|
||||
className="mx-auto"
|
||||
style={{fontSize: 13, width: 127}}
|
||||
>
|
||||
<span style={{color: isRegistered ? CHECK_MARK_GREEN : colors.red500}}>
|
||||
<i
|
||||
className={`zmdi ${isRegistered ? 'zmdi-check-circle' : 'zmdi-alert-triangle'}`}
|
||||
/>
|
||||
</span>{' '}
|
||||
<span>{isRegistered ? 'Registered' : 'Unregistered'} token</span>
|
||||
<ReactTooltip id={registeredTooltipId}>
|
||||
{isRegistered ?
|
||||
<div>
|
||||
This token address was found in the token registry<br />
|
||||
smart contract and is therefore believed to be a<br />
|
||||
legitimate token.
|
||||
</div> :
|
||||
<div>
|
||||
This token is not included in the token registry<br />
|
||||
smart contract. We cannot guarantee the legitimacy<br />
|
||||
of this token. Make sure to verify its address on Etherscan.
|
||||
</div>
|
||||
}
|
||||
</ReactTooltip>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{!_.isUndefined(this.props.hasUniqueNameAndSymbol) && !this.props.hasUniqueNameAndSymbol &&
|
||||
<div>
|
||||
<div
|
||||
data-tip={true}
|
||||
data-for={uniqueNameAndSymbolTooltipId}
|
||||
className="mx-auto"
|
||||
style={{fontSize: 13, width: 127}}
|
||||
>
|
||||
<span style={{color: colors.red500}}>
|
||||
<i
|
||||
className="zmdi zmdi-alert-octagon"
|
||||
/>
|
||||
</span>{' '}
|
||||
<span>Suspicious token</span>
|
||||
<ReactTooltip id={uniqueNameAndSymbolTooltipId}>
|
||||
This token shares it's name, symbol or both with<br />
|
||||
a token in the 0x Token Registry but it has a different<br />
|
||||
smart contract address. This is most likely a scam token!
|
||||
</ReactTooltip>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
15
packages/website/ts/components/ui/required_label.tsx
Normal file
15
packages/website/ts/components/ui/required_label.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import {colors} from 'material-ui/styles';
|
||||
|
||||
export interface RequiredLabelProps {
|
||||
label: string|React.ReactNode;
|
||||
}
|
||||
|
||||
export const RequiredLabel = (props: RequiredLabelProps) => {
|
||||
return (
|
||||
<span>
|
||||
{props.label}
|
||||
<span style={{color: colors.red600}}>*</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
23
packages/website/ts/components/ui/simple_loading.tsx
Normal file
23
packages/website/ts/components/ui/simple_loading.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as React from 'react';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import CircularProgress from 'material-ui/CircularProgress';
|
||||
|
||||
export interface SimpleLoadingProps {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const SimpleLoading = (props: SimpleLoadingProps) => {
|
||||
return (
|
||||
<div className="mx-auto pt3" style={{maxWidth: 400, height: 409}}>
|
||||
<div
|
||||
className="relative"
|
||||
style={{top: '50%', transform: 'translateY(-50%)', height: 95}}
|
||||
>
|
||||
<CircularProgress />
|
||||
<div className="pt3 pb3">
|
||||
{props.message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
46
packages/website/ts/components/ui/swap_icon.tsx
Normal file
46
packages/website/ts/components/ui/swap_icon.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {colors} from 'material-ui/styles';
|
||||
|
||||
interface SwapIconProps {
|
||||
swapTokensFn: () => void;
|
||||
}
|
||||
|
||||
interface SwapIconState {
|
||||
isHovering: boolean;
|
||||
}
|
||||
|
||||
export class SwapIcon extends React.Component<SwapIconProps, SwapIconState> {
|
||||
public constructor(props: SwapIconProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isHovering: false,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const swapStyles = {
|
||||
color: this.state.isHovering ? colors.amber600 : colors.amber800,
|
||||
fontSize: 50,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className="mx-auto pt4"
|
||||
style={{cursor: 'pointer', height: 50, width: 37.5}}
|
||||
onClick={this.props.swapTokensFn}
|
||||
onMouseEnter={this.onToggleHover.bind(this, true)}
|
||||
onMouseLeave={this.onToggleHover.bind(this, false)}
|
||||
>
|
||||
<i
|
||||
style={swapStyles}
|
||||
className="zmdi zmdi-swap"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private onToggleHover(isHovering: boolean) {
|
||||
this.setState({
|
||||
isHovering,
|
||||
});
|
||||
}
|
||||
}
|
||||
29
packages/website/ts/components/ui/token_icon.tsx
Normal file
29
packages/website/ts/components/ui/token_icon.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {Token} from 'ts/types';
|
||||
import {Identicon} from 'ts/components/ui/identicon';
|
||||
|
||||
interface TokenIconProps {
|
||||
token: Token;
|
||||
diameter: number;
|
||||
}
|
||||
|
||||
interface TokenIconState {}
|
||||
|
||||
export class TokenIcon extends React.Component<TokenIconProps, TokenIconState> {
|
||||
public render() {
|
||||
const token = this.props.token;
|
||||
const diameter = this.props.diameter;
|
||||
return (
|
||||
<div>
|
||||
{(token.isRegistered && !_.isUndefined(token.iconUrl)) ?
|
||||
<img
|
||||
style={{width: diameter, height: diameter}}
|
||||
src={token.iconUrl}
|
||||
/> :
|
||||
<Identicon address={token.address} diameter={diameter} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
77
packages/website/ts/components/visual_order.tsx
Normal file
77
packages/website/ts/components/visual_order.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {ZeroEx} from '0x.js';
|
||||
import {AssetToken, Token, TokenByAddress} from 'ts/types';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {Party} from 'ts/components/ui/party';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
|
||||
const PRECISION = 5;
|
||||
|
||||
interface VisualOrderProps {
|
||||
orderTakerAddress: string;
|
||||
orderMakerAddress: string;
|
||||
makerAssetToken: AssetToken;
|
||||
takerAssetToken: AssetToken;
|
||||
makerToken: Token;
|
||||
takerToken: Token;
|
||||
networkId: number;
|
||||
tokenByAddress: TokenByAddress;
|
||||
isMakerTokenAddressInRegistry: boolean;
|
||||
isTakerTokenAddressInRegistry: boolean;
|
||||
}
|
||||
|
||||
interface VisualOrderState {}
|
||||
|
||||
export class VisualOrder extends React.Component<VisualOrderProps, VisualOrderState> {
|
||||
public render() {
|
||||
const allTokens = _.values(this.props.tokenByAddress);
|
||||
const makerImage = this.props.makerToken.iconUrl;
|
||||
const takerImage = this.props.takerToken.iconUrl;
|
||||
return (
|
||||
<div>
|
||||
<div className="clearfix">
|
||||
<div className="col col-5 center">
|
||||
<Party
|
||||
label="Send"
|
||||
address={this.props.takerToken.address}
|
||||
alternativeImage={takerImage}
|
||||
networkId={this.props.networkId}
|
||||
isInTokenRegistry={this.props.isTakerTokenAddressInRegistry}
|
||||
hasUniqueNameAndSymbol={utils.hasUniqueNameAndSymbol(allTokens, this.props.takerToken)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col col-2 center pt1">
|
||||
<div className="pb1">
|
||||
{this.renderAmount(this.props.takerAssetToken, this.props.takerToken)}
|
||||
</div>
|
||||
<div className="lg-p2 md-p2 sm-p1">
|
||||
<img src="/images/trade_arrows.png" style={{width: 47}} />
|
||||
</div>
|
||||
<div className="pt1">
|
||||
{this.renderAmount(this.props.makerAssetToken, this.props.makerToken)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col col-5 center">
|
||||
<Party
|
||||
label="Receive"
|
||||
address={this.props.makerToken.address}
|
||||
alternativeImage={makerImage}
|
||||
networkId={this.props.networkId}
|
||||
isInTokenRegistry={this.props.isMakerTokenAddressInRegistry}
|
||||
hasUniqueNameAndSymbol={utils.hasUniqueNameAndSymbol(allTokens, this.props.makerToken)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderAmount(assetToken: AssetToken, token: Token) {
|
||||
const unitAmount = ZeroEx.toUnitAmount(assetToken.amount, token.decimals);
|
||||
return (
|
||||
<div style={{fontSize: 13}}>
|
||||
{unitAmount.toNumber().toFixed(PRECISION)} {token.symbol}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
54
packages/website/ts/containers/generate_order_form.tsx
Normal file
54
packages/website/ts/containers/generate_order_form.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {Store as ReduxStore, Dispatch} from 'redux';
|
||||
import {Dispatcher} from 'ts/redux/dispatcher';
|
||||
import {State} from 'ts/redux/reducer';
|
||||
import {Blockchain} from 'ts/blockchain';
|
||||
import {GenerateOrderForm as GenerateOrderFormComponent} from 'ts/components/generate_order/generate_order_form';
|
||||
import {
|
||||
SideToAssetToken,
|
||||
SignatureData,
|
||||
HashData,
|
||||
TokenByAddress,
|
||||
TokenStateByAddress,
|
||||
BlockchainErrs,
|
||||
} from 'ts/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
interface GenerateOrderFormProps {
|
||||
blockchain: Blockchain;
|
||||
hashData: HashData;
|
||||
dispatcher: Dispatcher;
|
||||
}
|
||||
|
||||
interface ConnectedState {
|
||||
blockchainErr: BlockchainErrs;
|
||||
blockchainIsLoaded: boolean;
|
||||
orderExpiryTimestamp: BigNumber;
|
||||
orderSignatureData: SignatureData;
|
||||
userAddress: string;
|
||||
orderTakerAddress: string;
|
||||
orderSalt: BigNumber;
|
||||
networkId: number;
|
||||
sideToAssetToken: SideToAssetToken;
|
||||
tokenByAddress: TokenByAddress;
|
||||
tokenStateByAddress: TokenStateByAddress;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: State, ownProps: GenerateOrderFormProps): ConnectedState => ({
|
||||
blockchainErr: state.blockchainErr,
|
||||
blockchainIsLoaded: state.blockchainIsLoaded,
|
||||
orderExpiryTimestamp: state.orderExpiryTimestamp,
|
||||
orderSignatureData: state.orderSignatureData,
|
||||
orderTakerAddress: state.orderTakerAddress,
|
||||
orderSalt: state.orderSalt,
|
||||
networkId: state.networkId,
|
||||
sideToAssetToken: state.sideToAssetToken,
|
||||
tokenByAddress: state.tokenByAddress,
|
||||
tokenStateByAddress: state.tokenStateByAddress,
|
||||
userAddress: state.userAddress,
|
||||
});
|
||||
|
||||
export const GenerateOrderForm: React.ComponentClass<GenerateOrderFormProps> =
|
||||
connect(mapStateToProps)(GenerateOrderFormComponent);
|
||||
94
packages/website/ts/containers/portal.tsx
Normal file
94
packages/website/ts/containers/portal.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {Store as ReduxStore, Dispatch} from 'redux';
|
||||
import {State} from 'ts/redux/reducer';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {Dispatcher} from 'ts/redux/dispatcher';
|
||||
import {
|
||||
Side,
|
||||
HashData,
|
||||
TokenByAddress,
|
||||
BlockchainErrs,
|
||||
Fill,
|
||||
Order,
|
||||
ScreenWidths,
|
||||
TokenStateByAddress,
|
||||
} from 'ts/types';
|
||||
import {
|
||||
Portal as PortalComponent,
|
||||
PortalAllProps as PortalComponentAllProps,
|
||||
PortalPassedProps as PortalComponentPassedProps,
|
||||
} from 'ts/components/portal';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
interface MapStateToProps {
|
||||
blockchainErr: BlockchainErrs;
|
||||
blockchainIsLoaded: boolean;
|
||||
hashData: HashData;
|
||||
networkId: number;
|
||||
nodeVersion: string;
|
||||
orderFillAmount: number;
|
||||
tokenByAddress: TokenByAddress;
|
||||
tokenStateByAddress: TokenStateByAddress;
|
||||
userEtherBalance: number;
|
||||
screenWidth: ScreenWidths;
|
||||
shouldBlockchainErrDialogBeOpen: boolean;
|
||||
userAddress: string;
|
||||
userSuppliedOrderCache: Order;
|
||||
}
|
||||
|
||||
interface ConnectedState {}
|
||||
|
||||
interface ConnectedDispatch {
|
||||
dispatcher: Dispatcher;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: State, ownProps: PortalComponentAllProps): ConnectedState => {
|
||||
const receiveAssetToken = state.sideToAssetToken[Side.receive];
|
||||
const depositAssetToken = state.sideToAssetToken[Side.deposit];
|
||||
const receiveAddress = !_.isUndefined(receiveAssetToken.address) ?
|
||||
receiveAssetToken.address : constants.NULL_ADDRESS;
|
||||
const depositAddress = !_.isUndefined(depositAssetToken.address) ?
|
||||
depositAssetToken.address : constants.NULL_ADDRESS;
|
||||
const receiveAmount = !_.isUndefined(receiveAssetToken.amount) ?
|
||||
receiveAssetToken.amount : new BigNumber(0);
|
||||
const depositAmount = !_.isUndefined(depositAssetToken.amount) ?
|
||||
depositAssetToken.amount : new BigNumber(0);
|
||||
const hashData = {
|
||||
depositAmount,
|
||||
depositTokenContractAddr: depositAddress,
|
||||
feeRecipientAddress: constants.FEE_RECIPIENT_ADDRESS,
|
||||
makerFee: constants.MAKER_FEE,
|
||||
orderExpiryTimestamp: state.orderExpiryTimestamp,
|
||||
orderMakerAddress: state.userAddress,
|
||||
orderTakerAddress: state.orderTakerAddress !== '' ? state.orderTakerAddress : constants.NULL_ADDRESS,
|
||||
receiveAmount,
|
||||
receiveTokenContractAddr: receiveAddress,
|
||||
takerFee: constants.TAKER_FEE,
|
||||
orderSalt: state.orderSalt,
|
||||
};
|
||||
return {
|
||||
blockchainErr: state.blockchainErr,
|
||||
blockchainIsLoaded: state.blockchainIsLoaded,
|
||||
networkId: state.networkId,
|
||||
nodeVersion: state.nodeVersion,
|
||||
orderFillAmount: state.orderFillAmount,
|
||||
hashData,
|
||||
screenWidth: state.screenWidth,
|
||||
shouldBlockchainErrDialogBeOpen: state.shouldBlockchainErrDialogBeOpen,
|
||||
tokenByAddress: state.tokenByAddress,
|
||||
tokenStateByAddress: state.tokenStateByAddress,
|
||||
userAddress: state.userAddress,
|
||||
userEtherBalance: state.userEtherBalance,
|
||||
userSuppliedOrderCache: state.userSuppliedOrderCache,
|
||||
flashMessage: state.flashMessage,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch<State>): ConnectedDispatch => ({
|
||||
dispatcher: new Dispatcher(dispatch),
|
||||
});
|
||||
|
||||
export const Portal: React.ComponentClass<PortalComponentPassedProps> =
|
||||
connect(mapStateToProps, mapDispatchToProps)(PortalComponent);
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {Store as ReduxStore, Dispatch} from 'redux';
|
||||
import {Dispatcher} from 'ts/redux/dispatcher';
|
||||
import {State} from 'ts/redux/reducer';
|
||||
import {
|
||||
SmartContractsDocumentation as SmartContractsDocumentationComponent,
|
||||
SmartContractsDocumentationAllProps,
|
||||
} from 'ts/pages/documentation/smart_contracts_documentation';
|
||||
|
||||
interface ConnectedState {
|
||||
docsVersion: string;
|
||||
availableDocVersions: string[];
|
||||
}
|
||||
|
||||
interface ConnectedDispatch {
|
||||
dispatcher: Dispatcher;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: State, ownProps: SmartContractsDocumentationAllProps): ConnectedState => ({
|
||||
docsVersion: state.docsVersion,
|
||||
availableDocVersions: state.availableDocVersions,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch<State>): ConnectedDispatch => ({
|
||||
dispatcher: new Dispatcher(dispatch),
|
||||
});
|
||||
|
||||
export const SmartContractsDocumentation: React.ComponentClass<SmartContractsDocumentationAllProps> =
|
||||
connect(mapStateToProps, mapDispatchToProps)(SmartContractsDocumentationComponent);
|
||||
33
packages/website/ts/containers/zero_ex_js_documentation.tsx
Normal file
33
packages/website/ts/containers/zero_ex_js_documentation.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
import {Store as ReduxStore, Dispatch} from 'redux';
|
||||
import {Dispatcher} from 'ts/redux/dispatcher';
|
||||
import {State} from 'ts/redux/reducer';
|
||||
import {Blockchain} from 'ts/blockchain';
|
||||
import {
|
||||
ZeroExJSDocumentation as ZeroExJSDocumentationComponent,
|
||||
ZeroExJSDocumentationAllProps,
|
||||
} from 'ts/pages/documentation/zero_ex_js_documentation';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
interface ConnectedState {
|
||||
docsVersion: string;
|
||||
availableDocVersions: string[];
|
||||
}
|
||||
|
||||
interface ConnectedDispatch {
|
||||
dispatcher: Dispatcher;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: State, ownProps: ZeroExJSDocumentationAllProps): ConnectedState => ({
|
||||
docsVersion: state.docsVersion,
|
||||
availableDocVersions: state.availableDocVersions,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch<State>): ConnectedDispatch => ({
|
||||
dispatcher: new Dispatcher(dispatch),
|
||||
});
|
||||
|
||||
export const ZeroExJSDocumentation: React.ComponentClass<ZeroExJSDocumentationAllProps> =
|
||||
connect(mapStateToProps, mapDispatchToProps)(ZeroExJSDocumentationComponent);
|
||||
154
packages/website/ts/globals.d.ts
vendored
Normal file
154
packages/website/ts/globals.d.ts
vendored
Normal file
@@ -0,0 +1,154 @@
|
||||
declare module 'react-tooltip';
|
||||
declare module 'react-router-hash-link';
|
||||
declare module 'es6-promisify';
|
||||
declare module 'truffle-contract';
|
||||
declare module 'ethereumjs-util';
|
||||
declare module 'keccak';
|
||||
declare module 'web3-provider-engine';
|
||||
declare module 'whatwg-fetch';
|
||||
declare module 'react-html5video';
|
||||
declare module 'web3-provider-engine/subproviders/filters';
|
||||
declare module 'thenby';
|
||||
declare module 'react-highlight';
|
||||
declare module 'react-recaptcha';
|
||||
declare module 'react-document-title';
|
||||
declare module 'ledgerco';
|
||||
declare module 'ethereumjs-tx';
|
||||
|
||||
declare module '*.json' {
|
||||
const json: any;
|
||||
/* tslint:disable */
|
||||
export default json;
|
||||
/* tslint:enable */
|
||||
}
|
||||
|
||||
// find-version declarations
|
||||
declare function findVersions(version: string): string[];
|
||||
declare module 'find-versions' {
|
||||
export = findVersions;
|
||||
}
|
||||
|
||||
// compare-version declarations
|
||||
declare function compareVersions(firstVersion: string, secondVersion: string): number;
|
||||
declare module 'compare-versions' {
|
||||
export = compareVersions;
|
||||
}
|
||||
|
||||
// semver-sort declarations
|
||||
declare module 'semver-sort' {
|
||||
const desc: (versions: string[]) => string[];
|
||||
}
|
||||
|
||||
// xml-js declarations
|
||||
declare interface XML2JSONOpts {
|
||||
compact?: boolean;
|
||||
spaces?: number;
|
||||
}
|
||||
declare module 'xml-js' {
|
||||
const xml2json: (xml: string, opts: XML2JSONOpts) => string;
|
||||
}
|
||||
|
||||
// This will be defined by default in TS 2.4
|
||||
// Source: https://github.com/Microsoft/TypeScript/issues/12364
|
||||
interface System {
|
||||
import<T>(module: string): Promise<T>;
|
||||
}
|
||||
declare var System: System;
|
||||
|
||||
// ethereum-address declarations
|
||||
declare module 'ethereum-address' {
|
||||
export const isAddress: (address: string) => boolean;
|
||||
}
|
||||
|
||||
// jsonschema declarations
|
||||
// Source: https://github.com/tdegrunt/jsonschema/blob/master/lib/index.d.ts
|
||||
declare interface Schema {
|
||||
id?: string;
|
||||
$schema?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
multipleOf?: number;
|
||||
maximum?: number;
|
||||
exclusiveMaximum?: boolean;
|
||||
minimum?: number;
|
||||
exclusiveMinimum?: boolean;
|
||||
maxLength?: number;
|
||||
minLength?: number;
|
||||
pattern?: string;
|
||||
additionalItems?: boolean | Schema;
|
||||
items?: Schema | Schema[];
|
||||
maxItems?: number;
|
||||
minItems?: number;
|
||||
uniqueItems?: boolean;
|
||||
maxProperties?: number;
|
||||
minProperties?: number;
|
||||
required?: string[];
|
||||
additionalProperties?: boolean | Schema;
|
||||
definitions?: {
|
||||
[name: string]: Schema;
|
||||
};
|
||||
properties?: {
|
||||
[name: string]: Schema;
|
||||
};
|
||||
patternProperties?: {
|
||||
[name: string]: Schema;
|
||||
};
|
||||
dependencies?: {
|
||||
[name: string]: Schema | string[];
|
||||
};
|
||||
'enum'?: any[];
|
||||
type?: string | string[];
|
||||
allOf?: Schema[];
|
||||
anyOf?: Schema[];
|
||||
oneOf?: Schema[];
|
||||
not?: Schema;
|
||||
// This is the only property that's not defined in https://github.com/tdegrunt/jsonschema/blob/master/lib/index.d.ts
|
||||
// There is an open issue for that: https://github.com/tdegrunt/jsonschema/issues/194
|
||||
// There is also an opened PR: https://github.com/tdegrunt/jsonschema/pull/218/files
|
||||
// As soon as it gets merged we should be good to use types from 'jsonschema' package
|
||||
$ref?: string;
|
||||
}
|
||||
|
||||
// blockies declarations
|
||||
declare interface BlockiesIcon {
|
||||
toDataURL(): string;
|
||||
}
|
||||
declare interface BlockiesConfig {
|
||||
seed: string;
|
||||
}
|
||||
declare function blockies(config: BlockiesConfig): BlockiesIcon;
|
||||
declare module 'blockies' {
|
||||
export = blockies;
|
||||
}
|
||||
|
||||
// is-mobile declarations
|
||||
declare function isMobile(): boolean;
|
||||
declare module 'is-mobile' {
|
||||
export = isMobile;
|
||||
}
|
||||
|
||||
// web3-provider-engine declarations
|
||||
declare class Subprovider {}
|
||||
declare module 'web3-provider-engine/subproviders/subprovider' {
|
||||
export = Subprovider;
|
||||
}
|
||||
declare class RpcSubprovider {
|
||||
constructor(options: {rpcUrl: string});
|
||||
public handleRequest(payload: any, next: any, end: (err?: Error, data?: any) => void): void;
|
||||
}
|
||||
declare module 'web3-provider-engine/subproviders/rpc' {
|
||||
export = RpcSubprovider;
|
||||
}
|
||||
declare class HookedWalletSubprovider {
|
||||
constructor(wallet: any);
|
||||
}
|
||||
declare module 'web3-provider-engine/subproviders/hooked-wallet' {
|
||||
export = HookedWalletSubprovider;
|
||||
}
|
||||
|
||||
declare interface Artifact {
|
||||
abi: any;
|
||||
networks: {[networkId: number]: {
|
||||
address: string;
|
||||
}};
|
||||
}
|
||||
116
packages/website/ts/index.tsx
Normal file
116
packages/website/ts/index.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
// Polyfills
|
||||
import 'whatwg-fetch';
|
||||
|
||||
import * as React from 'react';
|
||||
import {render} from 'react-dom';
|
||||
import {Provider} from 'react-redux';
|
||||
import {createStore, Store as ReduxStore} from 'redux';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {Landing} from 'ts/pages/landing/landing';
|
||||
import {FAQ} from 'ts/pages/faq/faq';
|
||||
import {About} from 'ts/pages/about/about';
|
||||
import {Wiki} from 'ts/pages/wiki/wiki';
|
||||
import {NotFound} from 'ts/pages/not_found';
|
||||
import {createLazyComponent} from 'ts/lazy_component';
|
||||
import {State, reducer} from 'ts/redux/reducer';
|
||||
import {colors, getMuiTheme, MuiThemeProvider} from 'material-ui/styles';
|
||||
import {Switch, BrowserRouter as Router, Route, Link, Redirect} from 'react-router-dom';
|
||||
import {tradeHistoryStorage} from 'ts/local_storage/trade_history_storage';
|
||||
import * as injectTapEventPlugin from 'react-tap-event-plugin';
|
||||
import {WebsitePaths} from 'ts/types';
|
||||
injectTapEventPlugin();
|
||||
|
||||
// By default BigNumber's `toString` method converts to exponential notation if the value has
|
||||
// more then 20 digits. We want to avoid this behavior, so we set EXPONENTIAL_AT to a high number
|
||||
BigNumber.config({
|
||||
EXPONENTIAL_AT: 1000,
|
||||
});
|
||||
|
||||
// Check if we've introduced an update that requires us to clear the tradeHistory local storage entries
|
||||
tradeHistoryStorage.clearIfRequired();
|
||||
|
||||
const CUSTOM_GREY = 'rgb(39, 39, 39)';
|
||||
const CUSTOM_GREEN = 'rgb(102, 222, 117)';
|
||||
const CUSTOM_DARKER_GREEN = 'rgb(77, 197, 92)';
|
||||
|
||||
import 'basscss/css/basscss.css';
|
||||
import 'less/all.less';
|
||||
|
||||
const muiTheme = getMuiTheme({
|
||||
appBar: {
|
||||
height: 45,
|
||||
color: 'white',
|
||||
textColor: 'black',
|
||||
},
|
||||
palette: {
|
||||
pickerHeaderColor: constants.CUSTOM_BLUE,
|
||||
primary1Color: constants.CUSTOM_BLUE,
|
||||
primary2Color: constants.CUSTOM_BLUE,
|
||||
textColor: colors.grey700,
|
||||
},
|
||||
datePicker: {
|
||||
color: colors.grey700,
|
||||
textColor: 'white',
|
||||
calendarTextColor: 'white',
|
||||
selectColor: CUSTOM_GREY,
|
||||
selectTextColor: 'white',
|
||||
},
|
||||
timePicker: {
|
||||
color: colors.grey700,
|
||||
textColor: 'white',
|
||||
accentColor: 'white',
|
||||
headerColor: CUSTOM_GREY,
|
||||
selectColor: CUSTOM_GREY,
|
||||
selectTextColor: CUSTOM_GREY,
|
||||
},
|
||||
toggle: {
|
||||
thumbOnColor: CUSTOM_GREEN,
|
||||
trackOnColor: CUSTOM_DARKER_GREEN,
|
||||
},
|
||||
});
|
||||
|
||||
// We pass modulePromise returning lambda instead of module promise,
|
||||
// cause we only want to import the module when the user navigates to the page.
|
||||
// At the same time webpack statically parses for System.import() to determine bundle chunk split points
|
||||
// so each lazy import needs it's own `System.import()` declaration.
|
||||
const LazyPortal = createLazyComponent(
|
||||
'Portal', () => System.import<any>(/* webpackChunkName: "portal" */'ts/containers/portal'),
|
||||
);
|
||||
const LazyZeroExJSDocumentation = createLazyComponent(
|
||||
'ZeroExJSDocumentation',
|
||||
() => System.import<any>(/* webpackChunkName: "zeroExDocs" */'ts/containers/zero_ex_js_documentation'),
|
||||
);
|
||||
const LazySmartContractsDocumentation = createLazyComponent(
|
||||
'SmartContractsDocumentation',
|
||||
() => System.import<any>(/* webpackChunkName: "smartContractDocs" */'ts/containers/smart_contracts_documentation'),
|
||||
);
|
||||
|
||||
const store: ReduxStore<State> = createStore(reducer);
|
||||
render(
|
||||
<Router>
|
||||
<div>
|
||||
<MuiThemeProvider muiTheme={muiTheme}>
|
||||
<Provider store={store}>
|
||||
<div>
|
||||
<Switch>
|
||||
<Route exact={true} path="/" component={Landing as any} />
|
||||
<Redirect from="/otc" to={`${WebsitePaths.Portal}`}/>
|
||||
<Route path={`${WebsitePaths.Portal}`} component={LazyPortal} />
|
||||
<Route path={`${WebsitePaths.FAQ}`} component={FAQ as any} />
|
||||
<Route path={`${WebsitePaths.About}`} component={About as any} />
|
||||
<Route path={`${WebsitePaths.Wiki}`} component={Wiki as any} />
|
||||
<Route path={`${WebsitePaths.ZeroExJs}/:version?`} component={LazyZeroExJSDocumentation} />
|
||||
<Route
|
||||
path={`${WebsitePaths.SmartContracts}/:version?`}
|
||||
component={LazySmartContractsDocumentation}
|
||||
/>
|
||||
<Route component={NotFound as any} />
|
||||
</Switch>
|
||||
</div>
|
||||
</Provider>
|
||||
</MuiThemeProvider>
|
||||
</div>
|
||||
</Router>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
66
packages/website/ts/lazy_component.tsx
Normal file
66
packages/website/ts/lazy_component.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import * as React from 'react';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
interface LazyComponentProps {
|
||||
reactComponentPromise: Promise<React.ComponentClass<any>>;
|
||||
reactComponentProps: any;
|
||||
}
|
||||
|
||||
interface LazyComponentState {
|
||||
component?: React.ComponentClass<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This component is used for rendering components that are lazily loaded from other chunks.
|
||||
* Source: https://reacttraining.com/react-router/web/guides/code-splitting
|
||||
*/
|
||||
export class LazyComponent extends React.Component<LazyComponentProps, LazyComponentState> {
|
||||
constructor(props: LazyComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
component: undefined,
|
||||
};
|
||||
}
|
||||
public componentWillMount() {
|
||||
this.loadComponentFireAndForgetAsync(this.props);
|
||||
}
|
||||
public componentWillReceiveProps(nextProps: LazyComponentProps) {
|
||||
if (nextProps.reactComponentPromise !== this.props.reactComponentPromise) {
|
||||
this.loadComponentFireAndForgetAsync(nextProps);
|
||||
}
|
||||
}
|
||||
public render() {
|
||||
return _.isUndefined(this.state.component) ?
|
||||
null :
|
||||
React.createElement(this.state.component, this.props.reactComponentProps);
|
||||
}
|
||||
private async loadComponentFireAndForgetAsync(props: LazyComponentProps) {
|
||||
const component = await props.reactComponentPromise;
|
||||
this.setState({
|
||||
component,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [createLazyComponent description]
|
||||
* @param componentName name of exported component
|
||||
* @param lazyImport lambda returning module promise
|
||||
* we pass a lambda because we only want to require a module if it's used
|
||||
* @example `const LazyPortal = createLazyComponent('Portal', () => System.import<any>('ts/containers/portal'));``
|
||||
*/
|
||||
export const createLazyComponent = (componentName: string, lazyImport: () => Promise<any>) => {
|
||||
return (props: any) => {
|
||||
const reactComponentPromise = (async (): Promise<React.ComponentClass<any>> => {
|
||||
const mod = await lazyImport();
|
||||
const component = mod[componentName];
|
||||
return component;
|
||||
})();
|
||||
return (
|
||||
<LazyComponent
|
||||
reactComponentPromise={reactComponentPromise}
|
||||
reactComponentProps={props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
};
|
||||
35
packages/website/ts/local_storage/local_storage.ts
Normal file
35
packages/website/ts/local_storage/local_storage.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import * as _ from 'lodash';
|
||||
|
||||
export const localStorage = {
|
||||
doesExist() {
|
||||
return !!window.localStorage;
|
||||
},
|
||||
getItemIfExists(key: string): string {
|
||||
if (!this.doesExist) {
|
||||
return undefined;
|
||||
}
|
||||
const item = window.localStorage.getItem(key);
|
||||
if (_.isNull(item) || item === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
return item;
|
||||
},
|
||||
setItem(key: string, value: string) {
|
||||
if (!this.doesExist || _.isUndefined(value)) {
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(key, value);
|
||||
},
|
||||
removeItem(key: string) {
|
||||
if (!this.doesExist) {
|
||||
return;
|
||||
}
|
||||
window.localStorage.removeItem(key);
|
||||
},
|
||||
getAllKeys(): string[] {
|
||||
if (!this.doesExist) {
|
||||
return [];
|
||||
}
|
||||
return _.keys(window.localStorage);
|
||||
},
|
||||
};
|
||||
56
packages/website/ts/local_storage/tracked_token_storage.ts
Normal file
56
packages/website/ts/local_storage/tracked_token_storage.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as _ from 'lodash';
|
||||
import {Token, TrackedTokensByNetworkId} from 'ts/types';
|
||||
import {localStorage} from 'ts/local_storage/local_storage';
|
||||
|
||||
const TRACKED_TOKENS_KEY = 'trackedTokens';
|
||||
|
||||
export const trackedTokenStorage = {
|
||||
addTrackedTokenToUser(userAddress: string, networkId: number, token: Token) {
|
||||
const trackedTokensByUserAddress = this.getTrackedTokensByUserAddress();
|
||||
let trackedTokensByNetworkId = trackedTokensByUserAddress[userAddress];
|
||||
if (_.isUndefined(trackedTokensByNetworkId)) {
|
||||
trackedTokensByNetworkId = {};
|
||||
}
|
||||
const trackedTokens = !_.isUndefined(trackedTokensByNetworkId[networkId]) ?
|
||||
trackedTokensByNetworkId[networkId] :
|
||||
[];
|
||||
trackedTokens.push(token);
|
||||
trackedTokensByNetworkId[networkId] = trackedTokens;
|
||||
trackedTokensByUserAddress[userAddress] = trackedTokensByNetworkId;
|
||||
const trackedTokensByUserAddressJSONString = JSON.stringify(trackedTokensByUserAddress);
|
||||
localStorage.setItem(TRACKED_TOKENS_KEY, trackedTokensByUserAddressJSONString);
|
||||
},
|
||||
getTrackedTokensByUserAddress(): TrackedTokensByNetworkId {
|
||||
const trackedTokensJSONString = localStorage.getItemIfExists(TRACKED_TOKENS_KEY);
|
||||
if (_.isEmpty(trackedTokensJSONString)) {
|
||||
return {};
|
||||
}
|
||||
const trackedTokensByUserAddress = JSON.parse(trackedTokensJSONString);
|
||||
return trackedTokensByUserAddress;
|
||||
},
|
||||
getTrackedTokensIfExists(userAddress: string, networkId: number): Token[] {
|
||||
const trackedTokensJSONString = localStorage.getItemIfExists(TRACKED_TOKENS_KEY);
|
||||
if (_.isEmpty(trackedTokensJSONString)) {
|
||||
return undefined;
|
||||
}
|
||||
const trackedTokensByUserAddress = JSON.parse(trackedTokensJSONString);
|
||||
const trackedTokensByNetworkId = trackedTokensByUserAddress[userAddress];
|
||||
if (_.isUndefined(trackedTokensByNetworkId)) {
|
||||
return undefined;
|
||||
}
|
||||
const trackedTokens = trackedTokensByNetworkId[networkId];
|
||||
return trackedTokens;
|
||||
},
|
||||
removeTrackedToken(userAddress: string, networkId: number, tokenAddress: string) {
|
||||
const trackedTokensByUserAddress = this.getTrackedTokensByUserAddress();
|
||||
const trackedTokensByNetworkId = trackedTokensByUserAddress[userAddress];
|
||||
const trackedTokens = trackedTokensByNetworkId[networkId];
|
||||
const remainingTrackedTokens = _.filter(trackedTokens, (token: Token) => {
|
||||
return token.address !== tokenAddress;
|
||||
});
|
||||
trackedTokensByNetworkId[networkId] = remainingTrackedTokens;
|
||||
trackedTokensByUserAddress[userAddress] = trackedTokensByNetworkId;
|
||||
const trackedTokensByUserAddressJSONString = JSON.stringify(trackedTokensByUserAddress);
|
||||
localStorage.setItem(TRACKED_TOKENS_KEY, trackedTokensByUserAddressJSONString);
|
||||
},
|
||||
};
|
||||
82
packages/website/ts/local_storage/trade_history_storage.tsx
Normal file
82
packages/website/ts/local_storage/trade_history_storage.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import * as _ from 'lodash';
|
||||
import {Fill} from 'ts/types';
|
||||
import {configs} from 'ts/utils/configs';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {localStorage} from 'ts/local_storage/local_storage';
|
||||
import ethUtil = require('ethereumjs-util');
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
const FILLS_KEY = 'fills';
|
||||
const FILLS_LATEST_BLOCK = 'fillsLatestBlock';
|
||||
const FILL_CLEAR_KEY = 'lastClearFillDate';
|
||||
|
||||
export const tradeHistoryStorage = {
|
||||
// Clear all fill related localStorage if we've updated the config variable in an update
|
||||
// that introduced a backward incompatible change requiring the user to re-fetch the fills from
|
||||
// the blockchain
|
||||
clearIfRequired() {
|
||||
const lastClearFillDate = localStorage.getItemIfExists(FILL_CLEAR_KEY);
|
||||
if (lastClearFillDate !== configs.lastLocalStorageFillClearanceDate) {
|
||||
const localStorageKeys = localStorage.getAllKeys();
|
||||
_.each(localStorageKeys, key => {
|
||||
if (_.startsWith(key, `${FILLS_KEY}-`) || _.startsWith(key, `${FILLS_LATEST_BLOCK}-`)) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
localStorage.setItem(FILL_CLEAR_KEY, configs.lastLocalStorageFillClearanceDate);
|
||||
},
|
||||
addFillToUser(userAddress: string, networkId: number, fill: Fill) {
|
||||
const fillsByHash = this.getUserFillsByHash(userAddress, networkId);
|
||||
const fillHash = this._getFillHash(fill);
|
||||
const doesFillExist = !_.isUndefined(fillsByHash[fillHash]);
|
||||
if (doesFillExist) {
|
||||
return;
|
||||
}
|
||||
fillsByHash[fillHash] = fill;
|
||||
const userFillsJSONString = JSON.stringify(fillsByHash);
|
||||
const userFillsKey = this._getUserFillsKey(userAddress, networkId);
|
||||
localStorage.setItem(userFillsKey, userFillsJSONString);
|
||||
},
|
||||
getUserFillsByHash(userAddress: string, networkId: number): {[fillHash: string]: Fill} {
|
||||
const userFillsKey = this._getUserFillsKey(userAddress, networkId);
|
||||
const userFillsJSONString = localStorage.getItemIfExists(userFillsKey);
|
||||
if (_.isEmpty(userFillsJSONString)) {
|
||||
return {};
|
||||
}
|
||||
const userFillsByHash = JSON.parse(userFillsJSONString);
|
||||
_.each(userFillsByHash, (fill, hash) => {
|
||||
fill.paidMakerFee = new BigNumber(fill.paidMakerFee);
|
||||
fill.paidTakerFee = new BigNumber(fill.paidTakerFee);
|
||||
fill.filledTakerTokenAmount = new BigNumber(fill.filledTakerTokenAmount);
|
||||
fill.filledMakerTokenAmount = new BigNumber(fill.filledMakerTokenAmount);
|
||||
});
|
||||
return userFillsByHash;
|
||||
},
|
||||
getFillsLatestBlock(userAddress: string, networkId: number): number {
|
||||
const userFillsLatestBlockKey = this._getFillsLatestBlockKey(userAddress, networkId);
|
||||
const blockNumberStr = localStorage.getItemIfExists(userFillsLatestBlockKey);
|
||||
if (_.isEmpty(blockNumberStr)) {
|
||||
return constants.GENESIS_ORDER_BLOCK_BY_NETWORK_ID[networkId];
|
||||
}
|
||||
const blockNumber = _.parseInt(blockNumberStr);
|
||||
return blockNumber;
|
||||
},
|
||||
setFillsLatestBlock(userAddress: string, networkId: number, blockNumber: number) {
|
||||
const userFillsLatestBlockKey = this._getFillsLatestBlockKey(userAddress, networkId);
|
||||
localStorage.setItem(userFillsLatestBlockKey, `${blockNumber}`);
|
||||
},
|
||||
_getUserFillsKey(userAddress: string, networkId: number) {
|
||||
const userFillsKey = `${FILLS_KEY}-${userAddress}-${networkId}`;
|
||||
return userFillsKey;
|
||||
},
|
||||
_getFillsLatestBlockKey(userAddress: string, networkId: number) {
|
||||
const userFillsLatestBlockKey = `${FILLS_LATEST_BLOCK}-${userAddress}-${networkId}`;
|
||||
return userFillsLatestBlockKey;
|
||||
},
|
||||
_getFillHash(fill: Fill): string {
|
||||
const fillJSON = JSON.stringify(fill);
|
||||
const fillHash = ethUtil.sha256(fillJSON);
|
||||
return fillHash.toString('hex');
|
||||
},
|
||||
};
|
||||
253
packages/website/ts/pages/about/about.tsx
Normal file
253
packages/website/ts/pages/about/about.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import * as DocumentTitle from 'react-document-title';
|
||||
import RaisedButton from 'material-ui/RaisedButton';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {Styles, ProfileInfo} from 'ts/types';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {Footer} from 'ts/components/footer';
|
||||
import {TopBar} from 'ts/components/top_bar';
|
||||
import {Question} from 'ts/pages/faq/question';
|
||||
import {configs} from 'ts/utils/configs';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {Profile} from 'ts/pages/about/profile';
|
||||
|
||||
const CUSTOM_BACKGROUND_COLOR = '#F0F0F0';
|
||||
const CUSTOM_GRAY = '#4C4C4C';
|
||||
const CUSTOM_LIGHT_GRAY = '#A2A2A2';
|
||||
|
||||
const teamRow1: ProfileInfo[] = [
|
||||
{
|
||||
name: 'Will Warren',
|
||||
title: 'Co-founder & CEO',
|
||||
description: `Smart contract R&D. Previously applied physics at Los Alamos \
|
||||
Nat Lab. Mechanical engineering at UC San Diego. PhD dropout.`,
|
||||
image: '/images/team/will.jpg',
|
||||
linkedIn: 'https://www.linkedin.com/in/will-warren-92aab62b/',
|
||||
github: 'https://github.com/willwarren89',
|
||||
medium: 'https://medium.com/@willwarren89',
|
||||
},
|
||||
{
|
||||
name: 'Amir Bandeali',
|
||||
title: 'Co-founder & CTO',
|
||||
description: `Smart contract R&D. Previously fixed income trader at DRW. \
|
||||
Finance at University of Illinois, Urbana-Champaign.`,
|
||||
image: '/images/team/amir.jpeg',
|
||||
linkedIn: 'https://www.linkedin.com/in/abandeali1/',
|
||||
github: 'https://github.com/abandeali1',
|
||||
medium: 'https://medium.com/@abandeali1',
|
||||
},
|
||||
{
|
||||
name: 'Fabio Berger',
|
||||
title: 'Senior Engineer',
|
||||
description: `Full-stack blockchain engineer. Previously software engineer \
|
||||
at Airtable and founder of WealthLift. Computer science at Duke.`,
|
||||
image: '/images/team/fabio.jpg',
|
||||
linkedIn: 'https://www.linkedin.com/in/fabio-berger-03ab261a/',
|
||||
github: 'https://github.com/fabioberger',
|
||||
medium: 'https://medium.com/@fabioberger',
|
||||
},
|
||||
{
|
||||
name: 'Alex Xu',
|
||||
title: 'Director of Operations',
|
||||
description: `Strategy and operations. Previously digital marketing at Google \
|
||||
and vendor management at Amazon. Economics at UC San Diego.`,
|
||||
image: '/images/team/alex.jpg',
|
||||
linkedIn: 'https://www.linkedin.com/in/alex-xu/',
|
||||
github: '',
|
||||
medium: '',
|
||||
},
|
||||
];
|
||||
|
||||
const teamRow2: ProfileInfo[] = [
|
||||
{
|
||||
name: 'Leonid Logvinov',
|
||||
title: 'Engineer',
|
||||
description: `Full-stack blockchain engineer. Previously blockchain engineer \
|
||||
at Neufund. Computer science at University of Warsaw.`,
|
||||
image: '/images/team/leonid.png',
|
||||
linkedIn: 'https://www.linkedin.com/in/leonidlogvinov/',
|
||||
github: 'https://github.com/LogvinovLeon',
|
||||
medium: '',
|
||||
},
|
||||
{
|
||||
name: 'Ben Burns',
|
||||
title: 'Designer',
|
||||
description: `Product, motion, and graphic designer. Previously designer \
|
||||
at Airtable and Apple. Digital Design at University of Cincinnati.`,
|
||||
image: '/images/team/ben.jpg',
|
||||
linkedIn: 'https://www.linkedin.com/in/ben-burns-30170478/',
|
||||
github: '',
|
||||
medium: '',
|
||||
},
|
||||
{
|
||||
name: 'Philippe Castonguay',
|
||||
title: 'Dev Relations Manager',
|
||||
description: `Developer relations. Previously computational neuroscience \
|
||||
research at Janelia. Statistics at Western University. MA Dropout.`,
|
||||
image: '/images/team/philippe.png',
|
||||
linkedIn: '',
|
||||
github: 'https://github.com/PhABC',
|
||||
medium: '',
|
||||
},
|
||||
{
|
||||
name: 'Brandon Millman',
|
||||
title: 'Senior Engineer',
|
||||
description: `Full-stack engineer. Previously senior software engineer at \
|
||||
Twitter. Electrical and Computer Engineering at Duke.`,
|
||||
image: '/images/team/brandon.png',
|
||||
linkedIn: 'https://www.linkedin.com/company-beta/17942619/',
|
||||
},
|
||||
];
|
||||
|
||||
const advisors: ProfileInfo[] = [
|
||||
{
|
||||
name: 'Fred Ehrsam',
|
||||
description: 'Co-founder of Coinbase. Previously FX trader at Goldman Sachs.',
|
||||
image: '/images/advisors/fred.jpg',
|
||||
linkedIn: 'https://www.linkedin.com/in/fredehrsam/',
|
||||
medium: 'https://medium.com/@FEhrsam',
|
||||
twitter: 'https://twitter.com/FEhrsam',
|
||||
},
|
||||
{
|
||||
name: 'Olaf Carlson-Wee',
|
||||
image: '/images/advisors/olaf.png',
|
||||
description: 'Founder of Polychain Capital. First hire at Coinbase. Angel investor.',
|
||||
linkedIn: 'https://www.linkedin.com/in/olafcw/',
|
||||
angellist: 'https://angel.co/olafcw',
|
||||
},
|
||||
{
|
||||
name: 'Joey Krug',
|
||||
description: `Co-CIO at Pantera Capital. Founder of Augur. Thiel 20 Under 20 Fellow.`,
|
||||
image: '/images/advisors/joey.jpg',
|
||||
linkedIn: 'https://www.linkedin.com/in/joeykrug/',
|
||||
github: 'https://github.com/joeykrug',
|
||||
angellist: 'https://angel.co/joeykrug',
|
||||
},
|
||||
{
|
||||
name: 'Linda Xie',
|
||||
description: 'Co-founder of Scalar Capital. Previously PM at Coinbase.',
|
||||
image: '/images/advisors/linda.jpg',
|
||||
linkedIn: 'https://www.linkedin.com/in/lindaxie/',
|
||||
medium: 'https://medium.com/@linda.xie',
|
||||
twitter: 'https://twitter.com/ljxie',
|
||||
},
|
||||
];
|
||||
|
||||
export interface AboutProps {
|
||||
source: string;
|
||||
location: Location;
|
||||
}
|
||||
|
||||
interface AboutState {}
|
||||
|
||||
const styles: Styles = {
|
||||
header: {
|
||||
fontFamily: 'Roboto Mono',
|
||||
fontSize: 36,
|
||||
color: 'black',
|
||||
paddingTop: 110,
|
||||
},
|
||||
};
|
||||
|
||||
export class About extends React.Component<AboutProps, AboutState> {
|
||||
public componentDidMount() {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
public render() {
|
||||
return (
|
||||
<div style={{backgroundColor: CUSTOM_BACKGROUND_COLOR}}>
|
||||
<DocumentTitle title="0x About Us"/>
|
||||
<TopBar
|
||||
blockchainIsLoaded={false}
|
||||
location={this.props.location}
|
||||
style={{backgroundColor: CUSTOM_BACKGROUND_COLOR}}
|
||||
/>
|
||||
<div
|
||||
id="about"
|
||||
className="mx-auto max-width-4 py4"
|
||||
style={{color: colors.grey800}}
|
||||
>
|
||||
<div
|
||||
className="mx-auto pb4 sm-px3"
|
||||
style={{maxWidth: 435}}
|
||||
>
|
||||
<div
|
||||
style={styles.header}
|
||||
>
|
||||
About us:
|
||||
</div>
|
||||
<div
|
||||
className="pt3"
|
||||
style={{fontSize: 17, color: CUSTOM_GRAY, lineHeight: 1.5}}
|
||||
>
|
||||
Our team is a diverse and globally distributed group with backgrounds
|
||||
in engineering, research, business and design. We are passionate about
|
||||
decentralized technology and its potential to act as an equalizing force
|
||||
in the world.
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt3 md-px4 lg-px0">
|
||||
<div className="clearfix pb3">
|
||||
{this.renderProfiles(teamRow1)}
|
||||
</div>
|
||||
<div className="clearfix">
|
||||
{this.renderProfiles(teamRow2)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt3 pb2">
|
||||
<div
|
||||
className="pt2 pb3 sm-center md-pl4 lg-pl0 md-ml3"
|
||||
style={{color: CUSTOM_LIGHT_GRAY, fontSize: 24, fontFamily: 'Roboto Mono'}}
|
||||
>
|
||||
Advisors:
|
||||
</div>
|
||||
<div className="clearfix">
|
||||
{this.renderProfiles(advisors)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto py4 sm-px3" style={{maxWidth: 308}}>
|
||||
<div
|
||||
className="pb2"
|
||||
style={{fontSize: 30, color: CUSTOM_GRAY, fontFamily: 'Roboto Mono', letterSpacing: 7.5}}
|
||||
>
|
||||
WE'RE HIRING
|
||||
</div>
|
||||
<div
|
||||
className="pb4 mb4"
|
||||
style={{fontSize: 16, color: CUSTOM_GRAY, lineHeight: 1.5, letterSpacing: '0.5px'}}
|
||||
>
|
||||
We are seeking outstanding candidates to{' '}
|
||||
<a
|
||||
href={constants.ANGELLIST_URL}
|
||||
target="_blank"
|
||||
style={{color: 'black'}}
|
||||
>
|
||||
join our team
|
||||
</a>
|
||||
. We value passion, diversity and unique perspectives.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer location={this.props.location} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderProfiles(profiles: ProfileInfo[]) {
|
||||
const numIndiv = profiles.length;
|
||||
const colSize = utils.getColSize(profiles.length);
|
||||
return _.map(profiles, profile => {
|
||||
return (
|
||||
<div
|
||||
key={`profile-${profile.name}`}
|
||||
>
|
||||
<Profile
|
||||
colSize={colSize}
|
||||
profileInfo={profile}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
99
packages/website/ts/pages/about/profile.tsx
Normal file
99
packages/website/ts/pages/about/profile.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {Element as ScrollElement} from 'react-scroll';
|
||||
import {Styles, ProfileInfo} from 'ts/types';
|
||||
|
||||
const IMAGE_DIMENSION = 149;
|
||||
const styles: Styles = {
|
||||
subheader: {
|
||||
textTransform: 'uppercase',
|
||||
fontSize: 32,
|
||||
margin: 0,
|
||||
},
|
||||
imageContainer: {
|
||||
width: IMAGE_DIMENSION,
|
||||
height: IMAGE_DIMENSION,
|
||||
boxShadow: 'rgba(0, 0, 0, 0.19) 2px 5px 10px',
|
||||
},
|
||||
};
|
||||
|
||||
interface ProfileProps {
|
||||
colSize: number;
|
||||
profileInfo: ProfileInfo;
|
||||
}
|
||||
|
||||
export function Profile(props: ProfileProps) {
|
||||
return (
|
||||
<div
|
||||
className={`lg-col md-col lg-col-${props.colSize} md-col-6`}
|
||||
>
|
||||
<div
|
||||
style={{maxWidth: 300}}
|
||||
className="mx-auto lg-px3 md-px3 sm-px4 sm-pb3"
|
||||
>
|
||||
<div
|
||||
className="circle overflow-hidden mx-auto"
|
||||
style={styles.imageContainer}
|
||||
>
|
||||
<img
|
||||
width={IMAGE_DIMENSION}
|
||||
src={props.profileInfo.image}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="center"
|
||||
style={{fontSize: 18, fontWeight: 'bold', paddingTop: 20}}
|
||||
>
|
||||
{props.profileInfo.name}
|
||||
</div>
|
||||
{!_.isUndefined(props.profileInfo.title) &&
|
||||
<div
|
||||
className="pt1 center"
|
||||
style={{fontSize: 14, fontFamily: 'Roboto Mono', color: '#818181'}}
|
||||
>
|
||||
{props.profileInfo.title.toUpperCase()}
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
style={{minHeight: 60, lineHeight: 1.4}}
|
||||
className="pt1 pb2 mx-auto lg-h6 md-h6 sm-h5 sm-center"
|
||||
>
|
||||
{props.profileInfo.description}
|
||||
</div>
|
||||
<div className="flex pb3 mx-auto sm-hide xs-hide" style={{width: 180, opacity: 0.5}}>
|
||||
{renderSocialMediaIcons(props.profileInfo)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSocialMediaIcons(profileInfo: ProfileInfo) {
|
||||
const icons = [
|
||||
renderSocialMediaIcon('zmdi-github-box', profileInfo.github),
|
||||
renderSocialMediaIcon('zmdi-linkedin-box', profileInfo.linkedIn),
|
||||
renderSocialMediaIcon('zmdi-twitter-box', profileInfo.twitter),
|
||||
];
|
||||
return icons;
|
||||
}
|
||||
|
||||
function renderSocialMediaIcon(iconName: string, url: string) {
|
||||
if (_.isEmpty(url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={url} className="pr1">
|
||||
<a
|
||||
href={url}
|
||||
style={{color: 'inherit'}}
|
||||
target="_blank"
|
||||
className="text-decoration-none"
|
||||
>
|
||||
<i className={`zmdi ${iconName}`} style={{...styles.socalIcon}} />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
packages/website/ts/pages/documentation/comment.tsx
Normal file
24
packages/website/ts/pages/documentation/comment.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import * as ReactMarkdown from 'react-markdown';
|
||||
import {MarkdownCodeBlock} from 'ts/pages/shared/markdown_code_block';
|
||||
|
||||
interface CommentProps {
|
||||
comment: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
className: '',
|
||||
};
|
||||
|
||||
export const Comment: React.SFC<CommentProps> = (props: CommentProps) => {
|
||||
return (
|
||||
<div className={`${props.className} comment`}>
|
||||
<ReactMarkdown
|
||||
source={props.comment}
|
||||
renderers={{CodeBlock: MarkdownCodeBlock}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
31
packages/website/ts/pages/documentation/custom_enum.tsx
Normal file
31
packages/website/ts/pages/documentation/custom_enum.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {CustomType} from 'ts/types';
|
||||
|
||||
const STRING_ENUM_CODE_PREFIX = ' strEnum(';
|
||||
|
||||
interface CustomEnumProps {
|
||||
type: CustomType;
|
||||
}
|
||||
|
||||
// This component renders custom string enums that was a work-around for versions of
|
||||
// TypeScript <2.4.0 that did not support them natively. We keep it around to support
|
||||
// older versions of 0x.js <0.9.0
|
||||
export function CustomEnum(props: CustomEnumProps) {
|
||||
const type = props.type;
|
||||
if (!_.startsWith(type.defaultValue, STRING_ENUM_CODE_PREFIX)) {
|
||||
utils.consoleLog('We do not yet support `Variable` types that are not strEnums');
|
||||
return null;
|
||||
}
|
||||
// Remove the prefix and postfix, leaving only the strEnum values without quotes.
|
||||
const enumValues = type.defaultValue.slice(10, -3).replace(/'/g, '');
|
||||
return (
|
||||
<span>
|
||||
{`{`}
|
||||
{'\t'}{enumValues}
|
||||
<br />
|
||||
{`}`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
26
packages/website/ts/pages/documentation/enum.tsx
Normal file
26
packages/website/ts/pages/documentation/enum.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {TypeDocNode, EnumValue} from 'ts/types';
|
||||
|
||||
const STRING_ENUM_CODE_PREFIX = ' strEnum(';
|
||||
|
||||
interface EnumProps {
|
||||
values: EnumValue[];
|
||||
}
|
||||
|
||||
export function Enum(props: EnumProps) {
|
||||
const values = _.map(props.values, (value, i) => {
|
||||
const isLast = i === props.values.length - 1;
|
||||
const defaultValueIfAny = !_.isUndefined(value.defaultValue) ? ` = ${value.defaultValue}` : '';
|
||||
return `\n\t${value.name}${defaultValueIfAny},`;
|
||||
});
|
||||
return (
|
||||
<span>
|
||||
{`{`}
|
||||
{values}
|
||||
<br />
|
||||
{`}`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
80
packages/website/ts/pages/documentation/event_definition.tsx
Normal file
80
packages/website/ts/pages/documentation/event_definition.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {Event, EventArg, HeaderSizes} from 'ts/types';
|
||||
import {Type} from 'ts/pages/documentation/type';
|
||||
import {AnchorTitle} from 'ts/pages/shared/anchor_title';
|
||||
|
||||
const KEYWORD_COLOR = '#a81ca6';
|
||||
const CUSTOM_GREEN = 'rgb(77, 162, 75)';
|
||||
|
||||
interface EventDefinitionProps {
|
||||
event: Event;
|
||||
}
|
||||
|
||||
interface EventDefinitionState {
|
||||
shouldShowAnchor: boolean;
|
||||
}
|
||||
|
||||
export class EventDefinition extends React.Component<EventDefinitionProps, EventDefinitionState> {
|
||||
constructor(props: EventDefinitionProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
shouldShowAnchor: false,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const event = this.props.event;
|
||||
return (
|
||||
<div
|
||||
id={event.name}
|
||||
className="pb2"
|
||||
style={{overflow: 'hidden', width: '100%'}}
|
||||
onMouseOver={this.setAnchorVisibility.bind(this, true)}
|
||||
onMouseOut={this.setAnchorVisibility.bind(this, false)}
|
||||
>
|
||||
<AnchorTitle
|
||||
headerSize={HeaderSizes.H3}
|
||||
title={`Event ${event.name}`}
|
||||
id={event.name}
|
||||
shouldShowAnchor={this.state.shouldShowAnchor}
|
||||
/>
|
||||
<div style={{fontSize: 16}}>
|
||||
<pre>
|
||||
<code className="hljs">
|
||||
{this.renderEventCode()}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderEventCode() {
|
||||
const indexed = <span style={{color: CUSTOM_GREEN}}> indexed</span>;
|
||||
const eventArgs = _.map(this.props.event.eventArgs, (eventArg: EventArg) => {
|
||||
return (
|
||||
<span key={`eventArg-${eventArg.name}`}>
|
||||
{eventArg.name}{eventArg.isIndexed ? indexed : ''}: <Type type={eventArg.type} />,
|
||||
</span>
|
||||
);
|
||||
});
|
||||
const argList = _.reduce(eventArgs, (prev: React.ReactNode, curr: React.ReactNode) => {
|
||||
return [prev, '\n\t', curr];
|
||||
});
|
||||
return (
|
||||
<span>
|
||||
{`{`}
|
||||
<br />
|
||||
{'\t'}{argList}
|
||||
<br />
|
||||
{`}`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
private setAnchorVisibility(shouldShowAnchor: boolean) {
|
||||
this.setState({
|
||||
shouldShowAnchor,
|
||||
});
|
||||
}
|
||||
}
|
||||
54
packages/website/ts/pages/documentation/interface.tsx
Normal file
54
packages/website/ts/pages/documentation/interface.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {CustomType, TypeDocTypes} from 'ts/types';
|
||||
import {Type} from 'ts/pages/documentation/type';
|
||||
import {MethodSignature} from 'ts/pages/documentation/method_signature';
|
||||
|
||||
interface InterfaceProps {
|
||||
type: CustomType;
|
||||
}
|
||||
|
||||
export function Interface(props: InterfaceProps) {
|
||||
const type = props.type;
|
||||
const properties = _.map(type.children, property => {
|
||||
return (
|
||||
<span key={`property-${property.name}-${property.type}-${type.name}`}>
|
||||
{property.name}:{' '}
|
||||
{property.type.typeDocType !== TypeDocTypes.Reflection ?
|
||||
<Type type={property.type} /> :
|
||||
<MethodSignature
|
||||
method={property.type.method}
|
||||
shouldHideMethodName={true}
|
||||
shouldUseArrowSyntax={true}
|
||||
/>
|
||||
},
|
||||
</span>
|
||||
);
|
||||
});
|
||||
const hasIndexSignature = !_.isUndefined(type.indexSignature);
|
||||
if (hasIndexSignature) {
|
||||
const is = type.indexSignature;
|
||||
const param = (
|
||||
<span key={`indexSigParams-${is.keyName}-${is.keyType}-${type.name}`}>
|
||||
{is.keyName}: <Type type={is.keyType} />
|
||||
</span>
|
||||
);
|
||||
properties.push((
|
||||
<span key={`indexSignature-${type.name}-${is.keyType.name}`}>
|
||||
[{param}]: {is.valueName},
|
||||
</span>
|
||||
));
|
||||
}
|
||||
const propertyList = _.reduce(properties, (prev: React.ReactNode, curr: React.ReactNode) => {
|
||||
return [prev, '\n\t', curr];
|
||||
});
|
||||
return (
|
||||
<span>
|
||||
{`{`}
|
||||
<br />
|
||||
{'\t'}{propertyList}
|
||||
<br />
|
||||
{`}`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
174
packages/website/ts/pages/documentation/method_block.tsx
Normal file
174
packages/website/ts/pages/documentation/method_block.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import * as ReactMarkdown from 'react-markdown';
|
||||
import {Chip} from 'material-ui/Chip';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {
|
||||
TypeDocNode,
|
||||
Styles,
|
||||
TypeDefinitionByName,
|
||||
TypescriptMethod,
|
||||
SolidityMethod,
|
||||
Parameter,
|
||||
HeaderSizes,
|
||||
} from 'ts/types';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {SourceLink} from 'ts/pages/documentation/source_link';
|
||||
import {MethodSignature} from 'ts/pages/documentation/method_signature';
|
||||
import {AnchorTitle} from 'ts/pages/shared/anchor_title';
|
||||
import {Comment} from 'ts/pages/documentation/comment';
|
||||
import {typeDocUtils} from 'ts/utils/typedoc_utils';
|
||||
|
||||
interface MethodBlockProps {
|
||||
method: SolidityMethod|TypescriptMethod;
|
||||
libraryVersion: string;
|
||||
typeDefinitionByName: TypeDefinitionByName;
|
||||
}
|
||||
|
||||
interface MethodBlockState {
|
||||
shouldShowAnchor: boolean;
|
||||
}
|
||||
|
||||
const styles: Styles = {
|
||||
chip: {
|
||||
fontSize: 13,
|
||||
backgroundColor: colors.lightBlueA700,
|
||||
color: 'white',
|
||||
height: 11,
|
||||
borderRadius: 14,
|
||||
marginTop: 19,
|
||||
lineHeight: 0.8,
|
||||
},
|
||||
};
|
||||
|
||||
export class MethodBlock extends React.Component<MethodBlockProps, MethodBlockState> {
|
||||
constructor(props: MethodBlockProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
shouldShowAnchor: false,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const method = this.props.method;
|
||||
if (typeDocUtils.isPrivateOrProtectedProperty(method.name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id={method.name}
|
||||
style={{overflow: 'hidden', width: '100%'}}
|
||||
className="pb4"
|
||||
onMouseOver={this.setAnchorVisibility.bind(this, true)}
|
||||
onMouseOut={this.setAnchorVisibility.bind(this, false)}
|
||||
>
|
||||
{!method.isConstructor &&
|
||||
<div className="flex">
|
||||
{(method as TypescriptMethod).isStatic &&
|
||||
this.renderChip('Static')
|
||||
}
|
||||
{(method as SolidityMethod).isConstant &&
|
||||
this.renderChip('Constant')
|
||||
}
|
||||
{(method as SolidityMethod).isPayable &&
|
||||
this.renderChip('Payable')
|
||||
}
|
||||
<AnchorTitle
|
||||
headerSize={HeaderSizes.H3}
|
||||
title={method.name}
|
||||
id={method.name}
|
||||
shouldShowAnchor={this.state.shouldShowAnchor}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<code className="hljs">
|
||||
<MethodSignature
|
||||
method={method}
|
||||
typeDefinitionByName={this.props.typeDefinitionByName}
|
||||
/>
|
||||
</code>
|
||||
{(method as TypescriptMethod).source &&
|
||||
<SourceLink
|
||||
version={this.props.libraryVersion}
|
||||
source={(method as TypescriptMethod).source}
|
||||
/>
|
||||
}
|
||||
{method.comment &&
|
||||
<Comment
|
||||
comment={method.comment}
|
||||
className="py2"
|
||||
/>
|
||||
}
|
||||
{method.parameters && !_.isEmpty(method.parameters) &&
|
||||
<div>
|
||||
<h4
|
||||
className="pb1 thin"
|
||||
style={{borderBottom: '1px solid #e1e8ed'}}
|
||||
>
|
||||
ARGUMENTS
|
||||
</h4>
|
||||
{this.renderParameterDescriptions(method.parameters)}
|
||||
</div>
|
||||
}
|
||||
{method.returnComment &&
|
||||
<div className="pt1 comment">
|
||||
<h4
|
||||
className="pb1 thin"
|
||||
style={{borderBottom: '1px solid #e1e8ed'}}
|
||||
>
|
||||
RETURNS
|
||||
</h4>
|
||||
<Comment
|
||||
comment={method.returnComment}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderChip(text: string) {
|
||||
return (
|
||||
<div
|
||||
className="p1 mr1"
|
||||
style={styles.chip}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderParameterDescriptions(parameters: Parameter[]) {
|
||||
const descriptions = _.map(parameters, parameter => {
|
||||
const isOptional = parameter.isOptional;
|
||||
return (
|
||||
<div
|
||||
key={`param-description-${parameter.name}`}
|
||||
className="flex pb1 mb2"
|
||||
style={{borderBottom: '1px solid #f0f4f7'}}
|
||||
>
|
||||
<div className="col lg-col-1 md-col-1 sm-hide xs-hide" />
|
||||
<div className="col lg-col-3 md-col-3 sm-col-12 col-12">
|
||||
<div className="bold">
|
||||
{parameter.name}
|
||||
</div>
|
||||
<div className="pt1" style={{color: colors.grey500, fontSize: 14}}>
|
||||
{isOptional && 'optional'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col lg-col-8 md-col-8 sm-col-12 col-12">
|
||||
{parameter.comment &&
|
||||
<Comment
|
||||
comment={parameter.comment}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return descriptions;
|
||||
}
|
||||
private setAnchorVisibility(shouldShowAnchor: boolean) {
|
||||
this.setState({
|
||||
shouldShowAnchor,
|
||||
});
|
||||
}
|
||||
}
|
||||
62
packages/website/ts/pages/documentation/method_signature.tsx
Normal file
62
packages/website/ts/pages/documentation/method_signature.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {TypescriptMethod, SolidityMethod, TypeDefinitionByName, Parameter} from 'ts/types';
|
||||
import {Type} from 'ts/pages/documentation/type';
|
||||
|
||||
interface MethodSignatureProps {
|
||||
method: TypescriptMethod|SolidityMethod;
|
||||
shouldHideMethodName?: boolean;
|
||||
shouldUseArrowSyntax?: boolean;
|
||||
typeDefinitionByName?: TypeDefinitionByName;
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
shouldHideMethodName: false,
|
||||
shouldUseArrowSyntax: false,
|
||||
};
|
||||
|
||||
export const MethodSignature: React.SFC<MethodSignatureProps> = (props: MethodSignatureProps) => {
|
||||
const parameters = renderParameters(props.method, props.typeDefinitionByName);
|
||||
const paramString = _.reduce(parameters, (prev: React.ReactNode, curr: React.ReactNode) => {
|
||||
return [prev, ', ', curr];
|
||||
});
|
||||
const methodName = props.shouldHideMethodName ? '' : props.method.name;
|
||||
const typeParameterIfExists = _.isUndefined((props.method as TypescriptMethod).typeParameter) ?
|
||||
undefined :
|
||||
renderTypeParameter(props.method, props.typeDefinitionByName);
|
||||
return (
|
||||
<span>
|
||||
{props.method.callPath}{methodName}{typeParameterIfExists}({paramString})
|
||||
{props.shouldUseArrowSyntax ? ' => ' : ': '}
|
||||
{' '}
|
||||
{props.method.returnType &&
|
||||
<Type type={props.method.returnType} typeDefinitionByName={props.typeDefinitionByName}/>
|
||||
}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
function renderParameters(method: TypescriptMethod|SolidityMethod, typeDefinitionByName?: TypeDefinitionByName) {
|
||||
const parameters = method.parameters;
|
||||
const params = _.map(parameters, (p: Parameter) => {
|
||||
const isOptional = p.isOptional;
|
||||
return (
|
||||
<span key={`param-${p.type}-${p.name}`}>
|
||||
{p.name}{isOptional && '?'}: <Type type={p.type} typeDefinitionByName={typeDefinitionByName}/>
|
||||
</span>
|
||||
);
|
||||
});
|
||||
return params;
|
||||
}
|
||||
|
||||
function renderTypeParameter(method: TypescriptMethod, typeDefinitionByName?: TypeDefinitionByName) {
|
||||
const typeParameter = method.typeParameter;
|
||||
const typeParam = (
|
||||
<span>
|
||||
{`<${typeParameter.name} extends `}
|
||||
<Type type={typeParameter.type} typeDefinitionByName={typeDefinitionByName}/>
|
||||
{`>`}
|
||||
</span>
|
||||
);
|
||||
return typeParam;
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import DocumentTitle = require('react-document-title');
|
||||
import findVersions = require('find-versions');
|
||||
import semverSort = require('semver-sort');
|
||||
import {colors} from 'material-ui/styles';
|
||||
import CircularProgress from 'material-ui/CircularProgress';
|
||||
import {
|
||||
scroller,
|
||||
} from 'react-scroll';
|
||||
import {Dispatcher} from 'ts/redux/dispatcher';
|
||||
import {
|
||||
SmartContractsDocSections,
|
||||
Styles,
|
||||
DoxityDocObj,
|
||||
TypeDefinitionByName,
|
||||
DocAgnosticFormat,
|
||||
SolidityMethod,
|
||||
Property,
|
||||
CustomType,
|
||||
MenuSubsectionsBySection,
|
||||
Event,
|
||||
Docs,
|
||||
AddressByContractName,
|
||||
Networks,
|
||||
EtherscanLinkSuffixes,
|
||||
} from 'ts/types';
|
||||
import {TopBar} from 'ts/components/top_bar';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {docUtils} from 'ts/utils/doc_utils';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {MethodBlock} from 'ts/pages/documentation/method_block';
|
||||
import {SourceLink} from 'ts/pages/documentation/source_link';
|
||||
import {Type} from 'ts/pages/documentation/type';
|
||||
import {TypeDefinition} from 'ts/pages/documentation/type_definition';
|
||||
import {MarkdownSection} from 'ts/pages/shared/markdown_section';
|
||||
import {Comment} from 'ts/pages/documentation/comment';
|
||||
import {Badge} from 'ts/components/ui/badge';
|
||||
import {EventDefinition} from 'ts/pages/documentation/event_definition';
|
||||
import {AnchorTitle} from 'ts/pages/shared/anchor_title';
|
||||
import {SectionHeader} from 'ts/pages/shared/section_header';
|
||||
import {NestedSidebarMenu} from 'ts/pages/shared/nested_sidebar_menu';
|
||||
import {doxityUtils} from 'ts/utils/doxity_utils';
|
||||
/* tslint:disable:no-var-requires */
|
||||
const IntroMarkdown = require('md/docs/smart_contracts/introduction');
|
||||
/* tslint:enable:no-var-requires */
|
||||
|
||||
const SCROLL_TO_TIMEOUT = 500;
|
||||
const CUSTOM_PURPLE = '#690596';
|
||||
const CUSTOM_RED = '#e91751';
|
||||
const CUSTOM_TURQUOIS = '#058789';
|
||||
const DOC_JSON_ROOT = constants.S3_SMART_CONTRACTS_DOCUMENTATION_JSON_ROOT;
|
||||
|
||||
const sectionNameToMarkdown = {
|
||||
[SmartContractsDocSections.Introduction]: IntroMarkdown,
|
||||
};
|
||||
const networkNameToColor: {[network: string]: string} = {
|
||||
[Networks.kovan]: CUSTOM_PURPLE,
|
||||
[Networks.ropsten]: CUSTOM_RED,
|
||||
[Networks.mainnet]: CUSTOM_TURQUOIS,
|
||||
};
|
||||
|
||||
export interface SmartContractsDocumentationAllProps {
|
||||
source: string;
|
||||
location: Location;
|
||||
dispatcher: Dispatcher;
|
||||
docsVersion: string;
|
||||
availableDocVersions: string[];
|
||||
}
|
||||
|
||||
interface SmartContractsDocumentationState {
|
||||
docAgnosticFormat?: DocAgnosticFormat;
|
||||
}
|
||||
|
||||
const styles: Styles = {
|
||||
mainContainers: {
|
||||
position: 'absolute',
|
||||
top: 60,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
overflowZ: 'hidden',
|
||||
overflowY: 'scroll',
|
||||
minHeight: 'calc(100vh - 60px)',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
},
|
||||
menuContainer: {
|
||||
borderColor: colors.grey300,
|
||||
maxWidth: 330,
|
||||
marginLeft: 20,
|
||||
},
|
||||
};
|
||||
|
||||
export class SmartContractsDocumentation extends
|
||||
React.Component<SmartContractsDocumentationAllProps, SmartContractsDocumentationState> {
|
||||
constructor(props: SmartContractsDocumentationAllProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
docAgnosticFormat: undefined,
|
||||
};
|
||||
}
|
||||
public componentWillMount() {
|
||||
const pathName = this.props.location.pathname;
|
||||
const lastSegment = pathName.substr(pathName.lastIndexOf('/') + 1);
|
||||
const versions = findVersions(lastSegment);
|
||||
const preferredVersionIfExists = versions.length > 0 ? versions[0] : undefined;
|
||||
this.fetchJSONDocsFireAndForgetAsync(preferredVersionIfExists);
|
||||
}
|
||||
public render() {
|
||||
const menuSubsectionsBySection = _.isUndefined(this.state.docAgnosticFormat)
|
||||
? {}
|
||||
: this.getMenuSubsectionsBySection(this.state.docAgnosticFormat);
|
||||
return (
|
||||
<div>
|
||||
<DocumentTitle title="0x Smart Contract Documentation"/>
|
||||
<TopBar
|
||||
blockchainIsLoaded={false}
|
||||
location={this.props.location}
|
||||
docsVersion={this.props.docsVersion}
|
||||
availableDocVersions={this.props.availableDocVersions}
|
||||
menuSubsectionsBySection={menuSubsectionsBySection}
|
||||
shouldFullWidth={true}
|
||||
doc={Docs.SmartContracts}
|
||||
/>
|
||||
{_.isUndefined(this.state.docAgnosticFormat) ?
|
||||
<div
|
||||
className="col col-12"
|
||||
style={styles.mainContainers}
|
||||
>
|
||||
<div
|
||||
className="relative sm-px2 sm-pt2 sm-m1"
|
||||
style={{height: 122, top: '50%', transform: 'translateY(-50%)'}}
|
||||
>
|
||||
<div className="center pb2">
|
||||
<CircularProgress size={40} thickness={5} />
|
||||
</div>
|
||||
<div className="center pt2" style={{paddingBottom: 11}}>Loading documentation...</div>
|
||||
</div>
|
||||
</div> :
|
||||
<div
|
||||
className="mx-auto flex"
|
||||
style={{color: colors.grey800, height: 43}}
|
||||
>
|
||||
<div className="relative col md-col-3 lg-col-3 lg-pl0 md-pl1 sm-hide xs-hide">
|
||||
<div
|
||||
className="border-right absolute"
|
||||
style={{...styles.menuContainer, ...styles.mainContainers}}
|
||||
>
|
||||
<NestedSidebarMenu
|
||||
selectedVersion={this.props.docsVersion}
|
||||
versions={this.props.availableDocVersions}
|
||||
topLevelMenu={constants.menuSmartContracts}
|
||||
menuSubsectionsBySection={menuSubsectionsBySection}
|
||||
doc={Docs.SmartContracts}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative col lg-col-9 md-col-9 sm-col-12 col-12">
|
||||
<div
|
||||
id="documentation"
|
||||
style={styles.mainContainers}
|
||||
className="absolute"
|
||||
>
|
||||
<div id="smartContractsDocs" />
|
||||
<h1 className="md-pl2 sm-pl3">
|
||||
<a href={constants.GITHUB_CONTRACTS_URL} target="_blank">
|
||||
0x Smart Contracts
|
||||
</a>
|
||||
</h1>
|
||||
{this.renderDocumentation()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderDocumentation(): React.ReactNode {
|
||||
const subMenus = _.values(constants.menuSmartContracts);
|
||||
const orderedSectionNames = _.flatten(subMenus);
|
||||
// Since smart contract method params are all base types, no need to pass
|
||||
// down the typeDefinitionByName
|
||||
const typeDefinitionByName = {};
|
||||
const sections = _.map(orderedSectionNames, this.renderSection.bind(this, typeDefinitionByName));
|
||||
|
||||
return sections;
|
||||
}
|
||||
private renderSection(typeDefinitionByName: TypeDefinitionByName, sectionName: string): React.ReactNode {
|
||||
const docSection = this.state.docAgnosticFormat[sectionName];
|
||||
|
||||
const markdownFileIfExists = sectionNameToMarkdown[sectionName];
|
||||
if (!_.isUndefined(markdownFileIfExists)) {
|
||||
return (
|
||||
<MarkdownSection
|
||||
key={`markdown-section-${sectionName}`}
|
||||
sectionName={sectionName}
|
||||
markdownContent={markdownFileIfExists}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (_.isUndefined(docSection)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sortedProperties = _.sortBy(docSection.properties, 'name');
|
||||
const propertyDefs = _.map(sortedProperties, this.renderProperty.bind(this));
|
||||
|
||||
const sortedMethods = _.sortBy(docSection.methods, 'name');
|
||||
const methodDefs = _.map(sortedMethods, method => {
|
||||
const isConstructor = false;
|
||||
return this.renderMethodBlocks(method, sectionName, isConstructor, typeDefinitionByName);
|
||||
});
|
||||
|
||||
const sortedEvents = _.sortBy(docSection.events, 'name');
|
||||
const eventDefs = _.map(sortedEvents, (event: Event, i: number) => {
|
||||
return (
|
||||
<EventDefinition
|
||||
key={`event-${event.name}-${i}`}
|
||||
event={event}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div
|
||||
key={`section-${sectionName}`}
|
||||
className="py2 pr3 md-pl2 sm-pl3"
|
||||
>
|
||||
<div className="flex">
|
||||
<div style={{marginRight: 7}}>
|
||||
<SectionHeader sectionName={sectionName} />
|
||||
</div>
|
||||
{this.renderNetworkBadges(sectionName)}
|
||||
</div>
|
||||
{docSection.comment &&
|
||||
<Comment
|
||||
comment={docSection.comment}
|
||||
/>
|
||||
}
|
||||
{docSection.constructors.length > 0 &&
|
||||
<div>
|
||||
<h2 className="thin">Constructor</h2>
|
||||
{this.renderConstructors(docSection.constructors, typeDefinitionByName)}
|
||||
</div>
|
||||
}
|
||||
{docSection.properties.length > 0 &&
|
||||
<div>
|
||||
<h2 className="thin">Properties</h2>
|
||||
<div>{propertyDefs}</div>
|
||||
</div>
|
||||
}
|
||||
{docSection.methods.length > 0 &&
|
||||
<div>
|
||||
<h2 className="thin">Methods</h2>
|
||||
<div>{methodDefs}</div>
|
||||
</div>
|
||||
}
|
||||
{docSection.events.length > 0 &&
|
||||
<div>
|
||||
<h2 className="thin">Events</h2>
|
||||
<div>{eventDefs}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderNetworkBadges(sectionName: string) {
|
||||
const networkToAddressByContractName = constants.contractAddresses[this.props.docsVersion];
|
||||
const badges = _.map(networkToAddressByContractName,
|
||||
(addressByContractName: AddressByContractName, networkName: string) => {
|
||||
const contractAddress = addressByContractName[sectionName];
|
||||
const linkIfExists = utils.getEtherScanLinkIfExists(
|
||||
contractAddress, constants.networkIdByName[networkName], EtherscanLinkSuffixes.address,
|
||||
);
|
||||
return (
|
||||
<a
|
||||
key={`badge-${networkName}-${sectionName}`}
|
||||
href={linkIfExists}
|
||||
target="_blank"
|
||||
style={{color: 'white', textDecoration: 'none'}}
|
||||
>
|
||||
<Badge
|
||||
title={networkName}
|
||||
backgroundColor={networkNameToColor[networkName]}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
});
|
||||
return badges;
|
||||
}
|
||||
private renderConstructors(constructors: SolidityMethod[],
|
||||
typeDefinitionByName: TypeDefinitionByName): React.ReactNode {
|
||||
const constructorDefs = _.map(constructors, constructor => {
|
||||
return this.renderMethodBlocks(
|
||||
constructor, SmartContractsDocSections.zeroEx, constructor.isConstructor, typeDefinitionByName,
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
{constructorDefs}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderProperty(property: Property): React.ReactNode {
|
||||
return (
|
||||
<div
|
||||
key={`property-${property.name}-${property.type.name}`}
|
||||
className="pb3"
|
||||
>
|
||||
<code className="hljs">
|
||||
{property.name}: <Type type={property.type} />
|
||||
</code>
|
||||
{property.source &&
|
||||
<SourceLink
|
||||
version={this.props.docsVersion}
|
||||
source={property.source}
|
||||
/>
|
||||
}
|
||||
{property.comment &&
|
||||
<Comment
|
||||
comment={property.comment}
|
||||
className="py2"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderMethodBlocks(method: SolidityMethod, sectionName: string, isConstructor: boolean,
|
||||
typeDefinitionByName: TypeDefinitionByName): React.ReactNode {
|
||||
return (
|
||||
<MethodBlock
|
||||
key={`method-${method.name}`}
|
||||
method={method}
|
||||
typeDefinitionByName={typeDefinitionByName}
|
||||
libraryVersion={this.props.docsVersion}
|
||||
/>
|
||||
);
|
||||
}
|
||||
private scrollToHash(): void {
|
||||
const hashWithPrefix = this.props.location.hash;
|
||||
let hash = hashWithPrefix.slice(1);
|
||||
if (_.isEmpty(hash)) {
|
||||
hash = 'smartContractsDocs'; // scroll to the top
|
||||
}
|
||||
|
||||
scroller.scrollTo(hash, {duration: 0, offset: 0, containerId: 'documentation'});
|
||||
}
|
||||
private getMenuSubsectionsBySection(docAgnosticFormat?: DocAgnosticFormat): MenuSubsectionsBySection {
|
||||
const menuSubsectionsBySection = {} as MenuSubsectionsBySection;
|
||||
if (_.isUndefined(docAgnosticFormat)) {
|
||||
return menuSubsectionsBySection;
|
||||
}
|
||||
|
||||
const docSections = _.keys(SmartContractsDocSections);
|
||||
_.each(docSections, sectionName => {
|
||||
const docSection = docAgnosticFormat[sectionName];
|
||||
if (_.isUndefined(docSection)) {
|
||||
return; // no-op
|
||||
}
|
||||
|
||||
if (sectionName === SmartContractsDocSections.types) {
|
||||
const sortedTypesNames = _.sortBy(docSection.types, 'name');
|
||||
const typeNames = _.map(sortedTypesNames, t => t.name);
|
||||
menuSubsectionsBySection[sectionName] = typeNames;
|
||||
} else {
|
||||
const sortedEventNames = _.sortBy(docSection.events, 'name');
|
||||
const eventNames = _.map(sortedEventNames, m => m.name);
|
||||
const sortedMethodNames = _.sortBy(docSection.methods, 'name');
|
||||
const methodNames = _.map(sortedMethodNames, m => m.name);
|
||||
menuSubsectionsBySection[sectionName] = [...methodNames, ...eventNames];
|
||||
}
|
||||
});
|
||||
return menuSubsectionsBySection;
|
||||
}
|
||||
private async fetchJSONDocsFireAndForgetAsync(preferredVersionIfExists?: string): Promise<void> {
|
||||
const versionToFileName = await docUtils.getVersionToFileNameAsync(DOC_JSON_ROOT);
|
||||
const versions = _.keys(versionToFileName);
|
||||
this.props.dispatcher.updateAvailableDocVersions(versions);
|
||||
const sortedVersions = semverSort.desc(versions);
|
||||
const latestVersion = sortedVersions[0];
|
||||
|
||||
let versionToFetch = latestVersion;
|
||||
if (!_.isUndefined(preferredVersionIfExists)) {
|
||||
const preferredVersionFileNameIfExists = versionToFileName[preferredVersionIfExists];
|
||||
if (!_.isUndefined(preferredVersionFileNameIfExists)) {
|
||||
versionToFetch = preferredVersionIfExists;
|
||||
}
|
||||
}
|
||||
this.props.dispatcher.updateCurrentDocsVersion(versionToFetch);
|
||||
|
||||
const versionFileNameToFetch = versionToFileName[versionToFetch];
|
||||
const versionDocObj = await docUtils.getJSONDocFileAsync(versionFileNameToFetch, DOC_JSON_ROOT);
|
||||
const docAgnosticFormat = doxityUtils.convertToDocAgnosticFormat(versionDocObj as DoxityDocObj);
|
||||
|
||||
this.setState({
|
||||
docAgnosticFormat,
|
||||
}, () => {
|
||||
this.scrollToHash();
|
||||
});
|
||||
}
|
||||
}
|
||||
27
packages/website/ts/pages/documentation/source_link.tsx
Normal file
27
packages/website/ts/pages/documentation/source_link.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as React from 'react';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {Source} from 'ts/types';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
|
||||
interface SourceLinkProps {
|
||||
source: Source;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export function SourceLink(props: SourceLinkProps) {
|
||||
const source = props.source;
|
||||
const githubUrl = constants.GITHUB_0X_JS_URL;
|
||||
const sourceCodeUrl = `${githubUrl}/blob/v${props.version}/${source.fileName}#L${source.line}`;
|
||||
return (
|
||||
<div className="pt2" style={{fontSize: 14}}>
|
||||
<a
|
||||
href={sourceCodeUrl}
|
||||
target="_blank"
|
||||
className="underline"
|
||||
style={{color: colors.grey500}}
|
||||
>
|
||||
Source
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
187
packages/website/ts/pages/documentation/type.tsx
Normal file
187
packages/website/ts/pages/documentation/type.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {Link as ScrollLink} from 'react-scroll';
|
||||
import * as ReactTooltip from 'react-tooltip';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {typeDocUtils} from 'ts/utils/typedoc_utils';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {Type as TypeDef, TypeDocTypes, TypeDefinitionByName} from 'ts/types';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {TypeDefinition} from 'ts/pages/documentation/type_definition';
|
||||
|
||||
const BUILT_IN_TYPE_COLOR = '#e69d00';
|
||||
const STRING_LITERAL_COLOR = '#4da24b';
|
||||
|
||||
// Some types reference other libraries. For these types, we want to link the user to the relevant documentation.
|
||||
const typeToUrl: {[typeName: string]: string} = {
|
||||
Web3: constants.WEB3_DOCS_URL,
|
||||
Provider: constants.WEB3_PROVIDER_DOCS_URL,
|
||||
BigNumber: constants.BIGNUMBERJS_GITHUB_URL,
|
||||
};
|
||||
|
||||
const typeToSection: {[typeName: string]: string} = {
|
||||
ExchangeWrapper: 'exchange',
|
||||
TokenWrapper: 'token',
|
||||
TokenRegistryWrapper: 'tokenRegistry',
|
||||
EtherTokenWrapper: 'etherToken',
|
||||
ProxyWrapper: 'proxy',
|
||||
TokenTransferProxyWrapper: 'proxy',
|
||||
};
|
||||
|
||||
interface TypeProps {
|
||||
type: TypeDef;
|
||||
typeDefinitionByName?: TypeDefinitionByName;
|
||||
}
|
||||
|
||||
// The return type needs to be `any` here so that we can recursively define <Type /> components within
|
||||
// <Type /> components (e.g when rendering the union type).
|
||||
export function Type(props: TypeProps): any {
|
||||
const type = props.type;
|
||||
const isIntrinsic = type.typeDocType === TypeDocTypes.Intrinsic;
|
||||
const isReference = type.typeDocType === TypeDocTypes.Reference;
|
||||
const isArray = type.typeDocType === TypeDocTypes.Array;
|
||||
const isStringLiteral = type.typeDocType === TypeDocTypes.StringLiteral;
|
||||
let typeNameColor = 'inherit';
|
||||
let typeName: string|React.ReactNode;
|
||||
let typeArgs: React.ReactNode[] = [];
|
||||
switch (type.typeDocType) {
|
||||
case TypeDocTypes.Intrinsic:
|
||||
case TypeDocTypes.Unknown:
|
||||
typeName = type.name;
|
||||
typeNameColor = BUILT_IN_TYPE_COLOR;
|
||||
break;
|
||||
|
||||
case TypeDocTypes.Reference:
|
||||
typeName = type.name;
|
||||
typeArgs = _.map(type.typeArguments, (arg: TypeDef) => {
|
||||
if (arg.typeDocType === TypeDocTypes.Array) {
|
||||
const key = `type-${arg.elementType.name}-${arg.elementType.typeDocType}`;
|
||||
return (
|
||||
<span>
|
||||
<Type
|
||||
key={key}
|
||||
type={arg.elementType}
|
||||
typeDefinitionByName={props.typeDefinitionByName}
|
||||
/>[]
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
const subType = (
|
||||
<Type
|
||||
key={`type-${arg.name}-${arg.value}-${arg.typeDocType}`}
|
||||
type={arg}
|
||||
typeDefinitionByName={props.typeDefinitionByName}
|
||||
/>
|
||||
);
|
||||
return subType;
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case TypeDocTypes.StringLiteral:
|
||||
typeName = `'${type.value}'`;
|
||||
typeNameColor = STRING_LITERAL_COLOR;
|
||||
break;
|
||||
|
||||
case TypeDocTypes.Array:
|
||||
typeName = type.elementType.name;
|
||||
break;
|
||||
|
||||
case TypeDocTypes.Union:
|
||||
const unionTypes = _.map(type.types, t => {
|
||||
return (
|
||||
<Type
|
||||
key={`type-${t.name}-${t.value}-${t.typeDocType}`}
|
||||
type={t}
|
||||
typeDefinitionByName={props.typeDefinitionByName}
|
||||
/>
|
||||
);
|
||||
});
|
||||
typeName = _.reduce(unionTypes, (prev: React.ReactNode, curr: React.ReactNode) => {
|
||||
return [prev, '|', curr];
|
||||
});
|
||||
break;
|
||||
|
||||
case TypeDocTypes.TypeParameter:
|
||||
typeName = type.name;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw utils.spawnSwitchErr('type.typeDocType', type.typeDocType);
|
||||
}
|
||||
// HACK: Normalize BigNumber to simply BigNumber. For some reason the type
|
||||
// name is unpredictably one or the other.
|
||||
if (typeName === 'BigNumber') {
|
||||
typeName = 'BigNumber';
|
||||
}
|
||||
const commaSeparatedTypeArgs = _.reduce(typeArgs, (prev: React.ReactNode, curr: React.ReactNode) => {
|
||||
return [prev, ', ', curr];
|
||||
});
|
||||
|
||||
const typeNameUrlIfExists = typeToUrl[(typeName as string)];
|
||||
const sectionNameIfExists = typeToSection[(typeName as string)];
|
||||
if (!_.isUndefined(typeNameUrlIfExists)) {
|
||||
typeName = (
|
||||
<a
|
||||
href={typeNameUrlIfExists}
|
||||
target="_blank"
|
||||
className="text-decoration-none"
|
||||
style={{color: colors.lightBlueA700}}
|
||||
>
|
||||
{typeName}
|
||||
</a>
|
||||
);
|
||||
} else if ((isReference || isArray) &&
|
||||
(typeDocUtils.isPublicType(typeName as string) ||
|
||||
!_.isUndefined(sectionNameIfExists))) {
|
||||
const id = Math.random().toString();
|
||||
const typeDefinitionAnchorId = _.isUndefined(sectionNameIfExists) ? typeName : sectionNameIfExists;
|
||||
let typeDefinition;
|
||||
if (props.typeDefinitionByName) {
|
||||
typeDefinition = props.typeDefinitionByName[typeName as string];
|
||||
}
|
||||
typeName = (
|
||||
<ScrollLink
|
||||
to={typeDefinitionAnchorId}
|
||||
offset={0}
|
||||
duration={constants.DOCS_SCROLL_DURATION_MS}
|
||||
containerId={constants.DOCS_CONTAINER_ID}
|
||||
>
|
||||
{_.isUndefined(typeDefinition) || utils.isUserOnMobile() ?
|
||||
<span
|
||||
onClick={utils.setUrlHash.bind(null, typeDefinitionAnchorId)}
|
||||
style={{color: colors.lightBlueA700, cursor: 'pointer'}}
|
||||
>
|
||||
{typeName}
|
||||
</span> :
|
||||
<span
|
||||
data-tip={true}
|
||||
data-for={id}
|
||||
onClick={utils.setUrlHash.bind(null, typeDefinitionAnchorId)}
|
||||
style={{color: colors.lightBlueA700, cursor: 'pointer', display: 'inline-block'}}
|
||||
>
|
||||
{typeName}
|
||||
<ReactTooltip
|
||||
type="light"
|
||||
effect="solid"
|
||||
id={id}
|
||||
className="typeTooltip"
|
||||
>
|
||||
<TypeDefinition customType={typeDefinition} shouldAddId={false} />
|
||||
</ReactTooltip>
|
||||
</span>
|
||||
}
|
||||
</ScrollLink>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
<span style={{color: typeNameColor}}>{typeName}</span>
|
||||
{isArray && '[]'}{!_.isEmpty(typeArgs) &&
|
||||
<span>
|
||||
{'<'}{commaSeparatedTypeArgs}{'>'}
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
135
packages/website/ts/pages/documentation/type_definition.tsx
Normal file
135
packages/website/ts/pages/documentation/type_definition.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {KindString, CustomType, TypeDocTypes, CustomTypeChild, HeaderSizes} from 'ts/types';
|
||||
import {Type} from 'ts/pages/documentation/type';
|
||||
import {Interface} from 'ts/pages/documentation/interface';
|
||||
import {CustomEnum} from 'ts/pages/documentation/custom_enum';
|
||||
import {Enum} from 'ts/pages/documentation/enum';
|
||||
import {MethodSignature} from 'ts/pages/documentation/method_signature';
|
||||
import {AnchorTitle} from 'ts/pages/shared/anchor_title';
|
||||
import {Comment} from 'ts/pages/documentation/comment';
|
||||
import {typeDocUtils} from 'ts/utils/typedoc_utils';
|
||||
|
||||
const KEYWORD_COLOR = '#a81ca6';
|
||||
|
||||
interface TypeDefinitionProps {
|
||||
customType: CustomType;
|
||||
shouldAddId?: boolean;
|
||||
}
|
||||
|
||||
interface TypeDefinitionState {
|
||||
shouldShowAnchor: boolean;
|
||||
}
|
||||
|
||||
export class TypeDefinition extends React.Component<TypeDefinitionProps, TypeDefinitionState> {
|
||||
public static defaultProps: Partial<TypeDefinitionProps> = {
|
||||
shouldAddId: true,
|
||||
};
|
||||
constructor(props: TypeDefinitionProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
shouldShowAnchor: false,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const customType = this.props.customType;
|
||||
if (!typeDocUtils.isPublicType(customType.name)) {
|
||||
return null; // no-op
|
||||
}
|
||||
|
||||
let typePrefix: string;
|
||||
let codeSnippet: React.ReactNode;
|
||||
switch (customType.kindString) {
|
||||
case KindString.Interface:
|
||||
typePrefix = 'Interface';
|
||||
codeSnippet = (
|
||||
<Interface
|
||||
type={customType}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
case KindString.Variable:
|
||||
typePrefix = 'Enum';
|
||||
codeSnippet = (
|
||||
<CustomEnum
|
||||
type={customType}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
case KindString.Enumeration:
|
||||
typePrefix = 'Enum';
|
||||
const enumValues = _.map(customType.children, (c: CustomTypeChild) => {
|
||||
return {
|
||||
name: c.name,
|
||||
defaultValue: c.defaultValue,
|
||||
};
|
||||
});
|
||||
codeSnippet = (
|
||||
<Enum
|
||||
values={enumValues}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
case KindString['Type alias']:
|
||||
typePrefix = 'Type Alias';
|
||||
codeSnippet = (
|
||||
<span>
|
||||
<span style={{color: KEYWORD_COLOR}}>type</span> {customType.name} ={' '}
|
||||
{customType.type.typeDocType !== TypeDocTypes.Reflection ?
|
||||
<Type type={customType.type} /> :
|
||||
<MethodSignature
|
||||
method={customType.type.method}
|
||||
shouldHideMethodName={true}
|
||||
shouldUseArrowSyntax={true}
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw utils.spawnSwitchErr('type.kindString', customType.kindString);
|
||||
}
|
||||
|
||||
const typeDefinitionAnchorId = customType.name;
|
||||
return (
|
||||
<div
|
||||
id={this.props.shouldAddId ? typeDefinitionAnchorId : ''}
|
||||
className="pb2"
|
||||
style={{overflow: 'hidden', width: '100%'}}
|
||||
onMouseOver={this.setAnchorVisibility.bind(this, true)}
|
||||
onMouseOut={this.setAnchorVisibility.bind(this, false)}
|
||||
>
|
||||
<AnchorTitle
|
||||
headerSize={HeaderSizes.H3}
|
||||
title={`${typePrefix} ${customType.name}`}
|
||||
id={this.props.shouldAddId ? typeDefinitionAnchorId : ''}
|
||||
shouldShowAnchor={this.state.shouldShowAnchor}
|
||||
/>
|
||||
<div style={{fontSize: 16}}>
|
||||
<pre>
|
||||
<code className="hljs">
|
||||
{codeSnippet}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
{customType.comment &&
|
||||
<Comment
|
||||
comment={customType.comment}
|
||||
className="py2"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private setAnchorVisibility(shouldShowAnchor: boolean) {
|
||||
this.setState({
|
||||
shouldShowAnchor,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import * as ReactMarkdown from 'react-markdown';
|
||||
import DocumentTitle = require('react-document-title');
|
||||
import findVersions = require('find-versions');
|
||||
import semverSort = require('semver-sort');
|
||||
import {colors} from 'material-ui/styles';
|
||||
import MenuItem from 'material-ui/MenuItem';
|
||||
import CircularProgress from 'material-ui/CircularProgress';
|
||||
import Paper from 'material-ui/Paper';
|
||||
import {
|
||||
Link as ScrollLink,
|
||||
Element as ScrollElement,
|
||||
scroller,
|
||||
} from 'react-scroll';
|
||||
import {Dispatcher} from 'ts/redux/dispatcher';
|
||||
import {
|
||||
KindString,
|
||||
TypeDocNode,
|
||||
ZeroExJsDocSections,
|
||||
Styles,
|
||||
ScreenWidths,
|
||||
TypeDefinitionByName,
|
||||
DocAgnosticFormat,
|
||||
TypescriptMethod,
|
||||
Property,
|
||||
CustomType,
|
||||
Docs,
|
||||
} from 'ts/types';
|
||||
import {TopBar} from 'ts/components/top_bar';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {docUtils} from 'ts/utils/doc_utils';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {Loading} from 'ts/components/ui/loading';
|
||||
import {MethodBlock} from 'ts/pages/documentation/method_block';
|
||||
import {SourceLink} from 'ts/pages/documentation/source_link';
|
||||
import {Type} from 'ts/pages/documentation/type';
|
||||
import {TypeDefinition} from 'ts/pages/documentation/type_definition';
|
||||
import {MarkdownSection} from 'ts/pages/shared/markdown_section';
|
||||
import {Comment} from 'ts/pages/documentation/comment';
|
||||
import {AnchorTitle} from 'ts/pages/shared/anchor_title';
|
||||
import {SectionHeader} from 'ts/pages/shared/section_header';
|
||||
import {NestedSidebarMenu} from 'ts/pages/shared/nested_sidebar_menu';
|
||||
import {typeDocUtils} from 'ts/utils/typedoc_utils';
|
||||
/* tslint:disable:no-var-requires */
|
||||
const IntroMarkdown = require('md/docs/0xjs/introduction');
|
||||
const InstallationMarkdown = require('md/docs/0xjs/installation');
|
||||
const AsyncMarkdown = require('md/docs/0xjs/async');
|
||||
const ErrorsMarkdown = require('md/docs/0xjs/errors');
|
||||
const versioningMarkdown = require('md/docs/0xjs/versioning');
|
||||
/* tslint:enable:no-var-requires */
|
||||
|
||||
const SCROLL_TO_TIMEOUT = 500;
|
||||
const DOC_JSON_ROOT = constants.S3_0XJS_DOCUMENTATION_JSON_ROOT;
|
||||
|
||||
const sectionNameToMarkdown = {
|
||||
[ZeroExJsDocSections.introduction]: IntroMarkdown,
|
||||
[ZeroExJsDocSections.installation]: InstallationMarkdown,
|
||||
[ZeroExJsDocSections.async]: AsyncMarkdown,
|
||||
[ZeroExJsDocSections.errors]: ErrorsMarkdown,
|
||||
[ZeroExJsDocSections.versioning]: versioningMarkdown,
|
||||
};
|
||||
|
||||
export interface ZeroExJSDocumentationPassedProps {
|
||||
source: string;
|
||||
location: Location;
|
||||
}
|
||||
|
||||
export interface ZeroExJSDocumentationAllProps {
|
||||
source: string;
|
||||
location: Location;
|
||||
dispatcher: Dispatcher;
|
||||
docsVersion: string;
|
||||
availableDocVersions: string[];
|
||||
}
|
||||
|
||||
interface ZeroExJSDocumentationState {
|
||||
docAgnosticFormat?: DocAgnosticFormat;
|
||||
}
|
||||
|
||||
const styles: Styles = {
|
||||
mainContainers: {
|
||||
position: 'absolute',
|
||||
top: 60,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
overflowZ: 'hidden',
|
||||
overflowY: 'scroll',
|
||||
minHeight: 'calc(100vh - 60px)',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
},
|
||||
menuContainer: {
|
||||
borderColor: colors.grey300,
|
||||
maxWidth: 330,
|
||||
marginLeft: 20,
|
||||
},
|
||||
};
|
||||
|
||||
export class ZeroExJSDocumentation extends React.Component<ZeroExJSDocumentationAllProps, ZeroExJSDocumentationState> {
|
||||
constructor(props: ZeroExJSDocumentationAllProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
docAgnosticFormat: undefined,
|
||||
};
|
||||
}
|
||||
public componentWillMount() {
|
||||
const pathName = this.props.location.pathname;
|
||||
const lastSegment = pathName.substr(pathName.lastIndexOf('/') + 1);
|
||||
const versions = findVersions(lastSegment);
|
||||
const preferredVersionIfExists = versions.length > 0 ? versions[0] : undefined;
|
||||
this.fetchJSONDocsFireAndForgetAsync(preferredVersionIfExists);
|
||||
}
|
||||
public render() {
|
||||
const menuSubsectionsBySection = _.isUndefined(this.state.docAgnosticFormat)
|
||||
? {}
|
||||
: typeDocUtils.getMenuSubsectionsBySection(this.state.docAgnosticFormat);
|
||||
return (
|
||||
<div>
|
||||
<DocumentTitle title="0x.js Documentation"/>
|
||||
<TopBar
|
||||
blockchainIsLoaded={false}
|
||||
location={this.props.location}
|
||||
docsVersion={this.props.docsVersion}
|
||||
availableDocVersions={this.props.availableDocVersions}
|
||||
menuSubsectionsBySection={menuSubsectionsBySection}
|
||||
shouldFullWidth={true}
|
||||
doc={Docs.ZeroExJs}
|
||||
/>
|
||||
{_.isUndefined(this.state.docAgnosticFormat) ?
|
||||
<div
|
||||
className="col col-12"
|
||||
style={styles.mainContainers}
|
||||
>
|
||||
<div
|
||||
className="relative sm-px2 sm-pt2 sm-m1"
|
||||
style={{height: 122, top: '50%', transform: 'translateY(-50%)'}}
|
||||
>
|
||||
<div className="center pb2">
|
||||
<CircularProgress size={40} thickness={5} />
|
||||
</div>
|
||||
<div className="center pt2" style={{paddingBottom: 11}}>Loading documentation...</div>
|
||||
</div>
|
||||
</div> :
|
||||
<div
|
||||
className="mx-auto flex"
|
||||
style={{color: colors.grey800, height: 43}}
|
||||
>
|
||||
<div className="relative col md-col-3 lg-col-3 lg-pl0 md-pl1 sm-hide xs-hide">
|
||||
<div
|
||||
className="border-right absolute"
|
||||
style={{...styles.menuContainer, ...styles.mainContainers}}
|
||||
>
|
||||
<NestedSidebarMenu
|
||||
selectedVersion={this.props.docsVersion}
|
||||
versions={this.props.availableDocVersions}
|
||||
topLevelMenu={typeDocUtils.getFinal0xjsMenu(this.props.docsVersion)}
|
||||
menuSubsectionsBySection={menuSubsectionsBySection}
|
||||
doc={Docs.ZeroExJs}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative col lg-col-9 md-col-9 sm-col-12 col-12">
|
||||
<div
|
||||
id="documentation"
|
||||
style={styles.mainContainers}
|
||||
className="absolute"
|
||||
>
|
||||
<div id="zeroExJSDocs" />
|
||||
<h1 className="md-pl2 sm-pl3">
|
||||
<a href={constants.GITHUB_0X_JS_URL} target="_blank">
|
||||
0x.js
|
||||
</a>
|
||||
</h1>
|
||||
{this.renderDocumentation()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderDocumentation(): React.ReactNode {
|
||||
const typeDocSection = this.state.docAgnosticFormat[ZeroExJsDocSections.types];
|
||||
const typeDefinitionByName = _.keyBy(typeDocSection.types, 'name');
|
||||
|
||||
const subMenus = _.values(constants.menu0xjs);
|
||||
const orderedSectionNames = _.flatten(subMenus);
|
||||
const sections = _.map(orderedSectionNames, this.renderSection.bind(this, typeDefinitionByName));
|
||||
|
||||
return sections;
|
||||
}
|
||||
private renderSection(typeDefinitionByName: TypeDefinitionByName, sectionName: string): React.ReactNode {
|
||||
const docSection = this.state.docAgnosticFormat[sectionName];
|
||||
|
||||
const markdownFileIfExists = sectionNameToMarkdown[sectionName];
|
||||
if (!_.isUndefined(markdownFileIfExists)) {
|
||||
return (
|
||||
<MarkdownSection
|
||||
key={`markdown-section-${sectionName}`}
|
||||
sectionName={sectionName}
|
||||
markdownContent={markdownFileIfExists}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (_.isUndefined(docSection)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const typeDefs = _.map(docSection.types, customType => {
|
||||
return (
|
||||
<TypeDefinition
|
||||
key={`type-${customType.name}`}
|
||||
customType={customType}
|
||||
/>
|
||||
);
|
||||
});
|
||||
const propertyDefs = _.map(docSection.properties, this.renderProperty.bind(this));
|
||||
const methodDefs = _.map(docSection.methods, method => {
|
||||
const isConstructor = false;
|
||||
return this.renderMethodBlocks(method, sectionName, isConstructor, typeDefinitionByName);
|
||||
});
|
||||
return (
|
||||
<div
|
||||
key={`section-${sectionName}`}
|
||||
className="py2 pr3 md-pl2 sm-pl3"
|
||||
>
|
||||
<SectionHeader sectionName={sectionName} />
|
||||
<Comment
|
||||
comment={docSection.comment}
|
||||
/>
|
||||
{sectionName === ZeroExJsDocSections.zeroEx && docSection.constructors.length > 0 &&
|
||||
<div>
|
||||
<h2 className="thin">Constructor</h2>
|
||||
{this.renderZeroExConstructors(docSection.constructors, typeDefinitionByName)}
|
||||
</div>
|
||||
}
|
||||
{docSection.properties.length > 0 &&
|
||||
<div>
|
||||
<h2 className="thin">Properties</h2>
|
||||
<div>{propertyDefs}</div>
|
||||
</div>
|
||||
}
|
||||
{docSection.methods.length > 0 &&
|
||||
<div>
|
||||
<h2 className="thin">Methods</h2>
|
||||
<div>{methodDefs}</div>
|
||||
</div>
|
||||
}
|
||||
{typeDefs.length > 0 &&
|
||||
<div>
|
||||
<div>{typeDefs}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderZeroExConstructors(constructors: TypescriptMethod[],
|
||||
typeDefinitionByName: TypeDefinitionByName): React.ReactNode {
|
||||
const constructorDefs = _.map(constructors, constructor => {
|
||||
return this.renderMethodBlocks(
|
||||
constructor, ZeroExJsDocSections.zeroEx, constructor.isConstructor, typeDefinitionByName,
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
{constructorDefs}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderProperty(property: Property): React.ReactNode {
|
||||
return (
|
||||
<div
|
||||
key={`property-${property.name}-${property.type.name}`}
|
||||
className="pb3"
|
||||
>
|
||||
<code className="hljs">
|
||||
{property.name}: <Type type={property.type} />
|
||||
</code>
|
||||
<SourceLink
|
||||
version={this.props.docsVersion}
|
||||
source={property.source}
|
||||
/>
|
||||
{property.comment &&
|
||||
<Comment
|
||||
comment={property.comment}
|
||||
className="py2"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderMethodBlocks(method: TypescriptMethod, sectionName: string, isConstructor: boolean,
|
||||
typeDefinitionByName: TypeDefinitionByName): React.ReactNode {
|
||||
return (
|
||||
<MethodBlock
|
||||
key={`method-${method.name}-${!_.isUndefined(method.source) ? method.source.line : ''}`}
|
||||
method={method}
|
||||
typeDefinitionByName={typeDefinitionByName}
|
||||
libraryVersion={this.props.docsVersion}
|
||||
/>
|
||||
);
|
||||
}
|
||||
private scrollToHash(): void {
|
||||
const hashWithPrefix = this.props.location.hash;
|
||||
let hash = hashWithPrefix.slice(1);
|
||||
if (_.isEmpty(hash)) {
|
||||
hash = 'zeroExJSDocs'; // scroll to the top
|
||||
}
|
||||
|
||||
scroller.scrollTo(hash, {duration: 0, offset: 0, containerId: 'documentation'});
|
||||
}
|
||||
private async fetchJSONDocsFireAndForgetAsync(preferredVersionIfExists?: string): Promise<void> {
|
||||
const versionToFileName = await docUtils.getVersionToFileNameAsync(DOC_JSON_ROOT);
|
||||
const versions = _.keys(versionToFileName);
|
||||
this.props.dispatcher.updateAvailableDocVersions(versions);
|
||||
const sortedVersions = semverSort.desc(versions);
|
||||
const latestVersion = sortedVersions[0];
|
||||
|
||||
let versionToFetch = latestVersion;
|
||||
if (!_.isUndefined(preferredVersionIfExists)) {
|
||||
const preferredVersionFileNameIfExists = versionToFileName[preferredVersionIfExists];
|
||||
if (!_.isUndefined(preferredVersionFileNameIfExists)) {
|
||||
versionToFetch = preferredVersionIfExists;
|
||||
}
|
||||
}
|
||||
this.props.dispatcher.updateCurrentDocsVersion(versionToFetch);
|
||||
|
||||
const versionFileNameToFetch = versionToFileName[versionToFetch];
|
||||
const versionDocObj = await docUtils.getJSONDocFileAsync(versionFileNameToFetch, DOC_JSON_ROOT);
|
||||
const docAgnosticFormat = typeDocUtils.convertToDocAgnosticFormat((versionDocObj as TypeDocNode));
|
||||
|
||||
this.setState({
|
||||
docAgnosticFormat,
|
||||
}, () => {
|
||||
this.scrollToHash();
|
||||
});
|
||||
}
|
||||
}
|
||||
497
packages/website/ts/pages/faq/faq.tsx
Normal file
497
packages/website/ts/pages/faq/faq.tsx
Normal file
@@ -0,0 +1,497 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import * as DocumentTitle from 'react-document-title';
|
||||
import RaisedButton from 'material-ui/RaisedButton';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {Styles, FAQSection, FAQQuestion, WebsitePaths} from 'ts/types';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {Footer} from 'ts/components/footer';
|
||||
import {TopBar} from 'ts/components/top_bar';
|
||||
import {Question} from 'ts/pages/faq/question';
|
||||
import {configs} from 'ts/utils/configs';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
|
||||
export interface FAQProps {
|
||||
source: string;
|
||||
location: Location;
|
||||
}
|
||||
|
||||
interface FAQState {}
|
||||
|
||||
const styles: Styles = {
|
||||
thin: {
|
||||
fontWeight: 100,
|
||||
},
|
||||
};
|
||||
|
||||
const sections: FAQSection[] = [
|
||||
{
|
||||
name: '0x Protocol',
|
||||
questions: [
|
||||
{
|
||||
prompt: 'What is 0x?',
|
||||
answer: (
|
||||
<div>
|
||||
At its core, 0x is an open and non-rent seeking protocol that facilitates trustless,
|
||||
low friction exchange of Ethereum-based assets. Developers can use 0x as a platform
|
||||
to build exchange applications on top of{' '}
|
||||
(<a href={`${configs.BASE_URL}${WebsitePaths.ZeroExJs}#introduction`} target="blank">0x.js</a>
|
||||
{' '}is a Javascript library for interacting with the 0x protocol). For end users, 0x will be
|
||||
the infrastructure of a wide variety of user-facing applications i.e.{' '}
|
||||
<a href={`${configs.BASE_URL}${WebsitePaths.Portal}`} target="blank">0x Portal</a>,
|
||||
a decentralized application that facilitates trustless trading of Ethereum-based tokens
|
||||
between known counterparties.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'What problem does 0x solve?',
|
||||
answer: (
|
||||
<div>
|
||||
In the two years since the Ethereum blockchain’s genesis block, numerous decentralized
|
||||
applications (dApps) have created Ethereum smart contracts for peer-to-peer exchange.
|
||||
Rapid iteration and a lack of best practices have left the blockchain scattered with
|
||||
proprietary and application-specific implementations. As a result, end users are
|
||||
exposed to numerous smart contracts of varying quality and security, with unique
|
||||
configuration processes and learning curves, all of which implement the same
|
||||
functionality. This approach imposes unnecessary costs on the network by fragmenting
|
||||
end users according to the particular dApp each user happens to be using, eliminating
|
||||
valuable network effects around liquidity. 0x is the solution to this problem by
|
||||
acting as modular, unopinionated building blocks that may be assembled and reconfigured.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'How is 0x different from a centralized exchange like Poloniex or ShapeShift?',
|
||||
answer: (
|
||||
<div>
|
||||
<ul>
|
||||
<li>
|
||||
0x is a protocol for exchange, not a user-facing exchange application.
|
||||
</li>
|
||||
<li>
|
||||
0x is decentralized and trustless; there is no central party which can be
|
||||
hacked, run away with customer funds or be subjected to government regulations.
|
||||
Hacks of Mt. Gox, Shapeshift and Bitfinex have demonstrated that these types of
|
||||
systemic risks are palpable.
|
||||
</li>
|
||||
<li>
|
||||
Rather than a proprietary system that exists to extract rent for its owners,
|
||||
0x is public infrastructure that is funded by a globally distributed community
|
||||
of stakeholders. While the protocol is free to use, it enables for-profit
|
||||
user-facing exchange applications to be built on top of the protocol.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'If 0x protocol is free to use, where do transaction fees come in?',
|
||||
answer: (
|
||||
<div>
|
||||
0x protocol uses off-chain order books to massively reduce friction costs for
|
||||
market makers and ensure that the blockchain is only used for trade settlement.
|
||||
Hosting and maintaining an off-chain order book is a service; to incent “Relayers”
|
||||
to provide this service they must be able to charge transaction fees on trading
|
||||
activity. Relayers are free to set their transaction fees to any value they desire.
|
||||
We expect Relayers to be highly competitive and transaction fees to approach an
|
||||
efficient economic equilibrium over time.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'What are the differences between 0x protocol and state channels?',
|
||||
answer: (
|
||||
<div>
|
||||
<div>
|
||||
Participants in a state channel pass cryptographically signed messages back and
|
||||
forth, accumulating intermediate state changes without publishing them to the
|
||||
canonical chain until the channel is closed. State channels are ideal for “bar tab”
|
||||
applications where numerous intermediate state changes may be accumulated off-chain
|
||||
before being settled by a final on-chain transaction (i.e. day trading, poker,
|
||||
turn-based games).
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
While state channels drastically reduce the number of on-chain transactions
|
||||
for specific use cases, numerous on-chain transactions and a security deposit
|
||||
are required to open and safely close a state channel making them less efficient
|
||||
than 0x for executing one-time trades.
|
||||
</li>
|
||||
<li>
|
||||
State channels are isolated from the Ethereum blockchain meaning that
|
||||
they cannot interact with smart contracts. 0x is designed to be integrated
|
||||
directly into smart contracts so trades can be executed programmatically
|
||||
in a single line of Solidity code.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'What types of digital assets are supported by 0x?',
|
||||
answer: (
|
||||
<div>
|
||||
0x supports all Ethereum-based assets that adhere to the ERC20 token standard.
|
||||
There are many ERC20 tokens, worth a combined $2.2B, and more tokens are created
|
||||
each month. We believe that, by 2020, thousands of assets will be tokenized and
|
||||
moved onto the Ethereum blockchain including traditional securities such as equities,
|
||||
bonds and derivatives, fiat currencies and scarce digital goods such as video game
|
||||
items. In the future, cross-blockchain solutions such as{' '}
|
||||
<a href="https://cosmos.network/" target="_blank">Cosmos</a> and{' '}
|
||||
<a href="http://polkadot.io/" target="_blank">Polkadot</a> will allow cryptocurrencies
|
||||
to freely move between blockchains and, naturally, currencies such as Bitcoin will
|
||||
end up being represented as ERC20 tokens on the Ethereum blockchain.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: '0x is open source: what prevents someone from forking the protocol?',
|
||||
answer: (
|
||||
<div>
|
||||
Ethereum and Bitcoin are both open source protocols. Each protocol has been forked,
|
||||
but the resulting clone networks have seen little adoption (as measured by transaction
|
||||
count or market cap). This is because users have little to no incentive to switch
|
||||
over to a clone network if the original has initial network effects and a talented
|
||||
developer team behind it.
|
||||
An exception is in the case that a protocol includes a controversial feature such as
|
||||
a method of rent extraction or a monetary policy that favors one group of users over
|
||||
another (Zcash developer subsidy - for better or worse - resulted in Zclassic).
|
||||
Perceived inequality can provide a strong enough incentive that users will fork the
|
||||
original protocol’s codebase and spin up a new network that eliminates the controversial
|
||||
feature. In the case of 0x, there is no rent extraction and no users are given
|
||||
special permissions.
|
||||
|
||||
0x protocol is upgradable. Cutting-edge technical capabilities can be integrated
|
||||
into 0x via decentralized governance (see section below), eliminating incentives
|
||||
to fork off of the original protocol and sacrifice the network effects surrounding
|
||||
liquidity that result from the shared protocol and settlement layer.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '0x Token (ZRX)',
|
||||
questions: [
|
||||
{
|
||||
prompt: 'Explain how the 0x protocol token (zrx) works.',
|
||||
answer: (
|
||||
<div>
|
||||
<div>
|
||||
0x protocol token (ZRX) is utilized in two ways: 1) to solve the{' '}
|
||||
<a href="https://en.wikipedia.org/wiki/Coordination_game" target="_blank">
|
||||
coordination problem
|
||||
</a> and drive network effects around liquidity, creating a feedback loop
|
||||
where early adopters of the protocol benefit from wider adoption and 2) to
|
||||
be used for decentralized governance over 0x protocol's update mechanism.
|
||||
In more detail:
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
ZRX tokens are used by Makers and Takers (market participants that generate
|
||||
and consume orders, respectively) to pay transaction fees to Relayers
|
||||
(entities that host and maintain public order books).
|
||||
</li>
|
||||
<li>
|
||||
ZRX tokens are used for decentralized governance over 0x protocol’s update
|
||||
mechanism which allows its underlying smart contracts to be replaced and
|
||||
improved over time. An update mechanism is needed because 0x is built upon
|
||||
Ethereum’s rapidly evolving technology stack, decentralized governance is
|
||||
needed because 0x protocol’s smart contracts will have access to user funds
|
||||
and numerous dApps will need to plug into 0x smart contracts. Decentralized
|
||||
governance ensures that this update process is secure and minimizes disruption
|
||||
to the network.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'Why must transaction fees be denominated in 0x token (ZRX) rather than ETH?',
|
||||
answer: (
|
||||
<div>
|
||||
0x protocol’s decentralized update mechanism is analogous to proof-of-stake.
|
||||
To maximize the alignment of stakeholder and end user incentives, the staked
|
||||
asset must provide utility within the protocol.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'How will decentralized governance work?',
|
||||
answer: (
|
||||
<div>
|
||||
Decentralized governance is an ongoing focus of research; it will involve token
|
||||
voting with ZRX. Ultimately the solution will maximize security while also maximizing
|
||||
the protocol’s ability to absorb new innovations. Until the governance structure is
|
||||
formalized and encoded within 0x DAO, a multi-sig will be used as a placeholder.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ZRX Token Launch and Fund Use',
|
||||
questions: [
|
||||
{
|
||||
prompt: 'What is the total supply of ZRX tokens?',
|
||||
answer: (
|
||||
<div>
|
||||
1,000,000,000 ZRX. Fixed supply.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'When is the Token Launch? will there be a pre-sale?',
|
||||
answer: (
|
||||
<div>
|
||||
The token launch will be on August 15th, 2017. There will not be a pre-sale.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'What will the token launch proceeds be used for?',
|
||||
answer: (
|
||||
<div>
|
||||
100% of the proceeds raised in the token launch will be used to fund the development
|
||||
of free and open source software, tools and infrastructure that support the protocol
|
||||
and surrounding ecosystem. Check out our{' '}
|
||||
<a
|
||||
href="https://docs.google.com/document/d/1_RVa-_bkU92fWRsC8eNy4vYjcTt-WC8GtqyyjbTd-oY"
|
||||
target="_blank"
|
||||
>
|
||||
development roadmap
|
||||
</a>.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'What will be the initial distribution of ZRX tokens?',
|
||||
answer: (
|
||||
<div>
|
||||
<div className="center" style={{width: '100%'}}>
|
||||
<img
|
||||
style={{width: 350}}
|
||||
src="/images/zrx_pie_chart.png"
|
||||
/>
|
||||
</div>
|
||||
<div className="py1">
|
||||
<div className="bold pb1">
|
||||
Token Launch (50%)
|
||||
</div>
|
||||
<div>
|
||||
ZRX is inherently a governance token that plays a critical role in the
|
||||
process of upgrading 0x protocol. We are fully committed to formulating
|
||||
a functional and theoretically sound governance model and we plan to dedicate
|
||||
significant resources to R&D.
|
||||
</div>
|
||||
</div>
|
||||
<div className="py1">
|
||||
<div className="bold pb1">
|
||||
Retained by 0x (15%)
|
||||
</div>
|
||||
<div>
|
||||
The 0x core development team will be able to sustain itself for approximately
|
||||
five years using funds raised through the token launch. If 0x protocol
|
||||
proves to be as foundational a technology as we believe it to be, the
|
||||
retained ZRX tokens will allow the 0x core development team to sustain
|
||||
operations beyond the first 5 years.
|
||||
</div>
|
||||
</div>
|
||||
<div className="py1">
|
||||
<div className="bold pb1">
|
||||
Developer Fund (15%)
|
||||
</div>
|
||||
<div>
|
||||
The Developer Fund will be used to make targeted capital injections
|
||||
into high potential projects and teams that are attempting to grow
|
||||
the 0x ecosystem, strategic partnerships, hackathon prizes and community
|
||||
development activities.
|
||||
</div>
|
||||
</div>
|
||||
<div className="py1">
|
||||
<div className="bold pb1">
|
||||
Founding Team (10%)
|
||||
</div>
|
||||
<div>
|
||||
The founding team’s allocation of ZRX will vest over a traditional 4
|
||||
year vesting schedule with a one year cliff. We believe this should
|
||||
be standard practice for any team that is committed to making their
|
||||
project a long term success.
|
||||
</div>
|
||||
</div>
|
||||
<div className="py1">
|
||||
<div className="bold pb1">
|
||||
Early Backers & Advisors (10%)
|
||||
</div>
|
||||
<div>
|
||||
Our backers and advisors have provided capital, resources and guidance
|
||||
that have allowed us to fill out our team, setup a robust legal entity
|
||||
and build a fully functional product before launching a token. As a result,
|
||||
we have a proven track record and can offer a token that holds genuine utility.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'Can I mine ZRX tokens?',
|
||||
answer: (
|
||||
<div>
|
||||
No, the total supply of ZRX tokens is fixed and there is no continuous issuance
|
||||
model. Users that facilitate trading over 0x protocol by operating a Relayer
|
||||
earn transaction fees denominated in ZRX; as more trading activity is generated,
|
||||
more transaction fees are earned.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'Will there be a lockup period for ZRX tokens sold in the token launch?',
|
||||
answer: (
|
||||
<div>
|
||||
No, ZRX tokens sold in the token launch will immediately be liquid.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'Will there be a lockup period for tokens allocated to the founding team?',
|
||||
answer: (
|
||||
<div>
|
||||
Yes. ZRX tokens allocated to founders, advisors and staff members will be released
|
||||
over a 4 year vesting schedule with a 25% cliff upon completion of the initial
|
||||
token launch and 25% released each subsequent year in monthly installments. Staff
|
||||
members hired after the token launch will have a 4 year vesting schedule with a
|
||||
one year cliff.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'Which cryptocurrencies will be accepted in the token launch?',
|
||||
answer: (
|
||||
<div>ETH.</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'When will 0x be live?',
|
||||
answer: (
|
||||
<div>
|
||||
An alpha version of 0x has been live on our private test network since January
|
||||
2017. Version 1.0 of 0x protocol will be deployed to the canonical Ethereum
|
||||
blockchain after a round of security audits and prior to the public token launch.
|
||||
0x will be using the 0x protocol during our token launch.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'Where can I find a development roadmap?',
|
||||
answer: (
|
||||
<div>
|
||||
Check it out{' '}
|
||||
<a
|
||||
href="https://drive.google.com/open?id=14IP1N8mt3YdsAoqYTyruMnZswpklUs3THyS1VXx71fo"
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Team',
|
||||
questions: [
|
||||
{
|
||||
prompt: 'Where is 0x based?',
|
||||
answer: (
|
||||
<div>
|
||||
0x was founded in SF and is driven by a diverse group of contributors.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'How can I get involved?',
|
||||
answer: (
|
||||
<div>
|
||||
Join our <a href={constants.ZEROEX_CHAT_URL} target="_blank">Rocket.chat</a>!
|
||||
As an open source project, 0x will rely on a worldwide community of passionate
|
||||
developers to contribute proposals, ideas and code.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'Why the name 0x?',
|
||||
answer: (
|
||||
<div>
|
||||
0x is the prefix for hexadecimal numeric constants including Ethereum addresses.
|
||||
In a more abstract context, as the first open protocol for exchange 0x represents
|
||||
the beginning of the end for the exchange industry’s rent seeking oligopoly:
|
||||
zero exchange.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
prompt: 'How do you pronounce 0x?',
|
||||
answer: (
|
||||
<div>
|
||||
We pronounce 0x as “zero-ex,” but you are free to pronounce it however you please.
|
||||
</div>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export class FAQ extends React.Component<FAQProps, FAQState> {
|
||||
public componentDidMount() {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
public render() {
|
||||
return (
|
||||
<div>
|
||||
<DocumentTitle title="0x FAQ"/>
|
||||
<TopBar
|
||||
blockchainIsLoaded={false}
|
||||
location={this.props.location}
|
||||
/>
|
||||
<div
|
||||
id="faq"
|
||||
className="mx-auto max-width-4 pt4"
|
||||
style={{color: colors.grey800}}
|
||||
>
|
||||
<h1 className="center" style={{...styles.thin}}>0x FAQ</h1>
|
||||
<div className="sm-px2 md-px2 lg-px0 pb4">
|
||||
{this.renderSections()}
|
||||
</div>
|
||||
</div>
|
||||
<Footer location={this.props.location} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderSections() {
|
||||
const renderedSections = _.map(sections, (section: FAQSection, i: number) => {
|
||||
const isFirstSection = i === 0;
|
||||
return (
|
||||
<div key={section.name}>
|
||||
<h3>{section.name}</h3>
|
||||
{this.renderQuestions(section.questions, isFirstSection)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return renderedSections;
|
||||
}
|
||||
private renderQuestions(questions: FAQQuestion[], isFirstSection: boolean) {
|
||||
const renderedQuestions = _.map(questions, (question: FAQQuestion, i: number) => {
|
||||
const isFirstQuestion = i === 0;
|
||||
return (
|
||||
<Question
|
||||
key={question.prompt}
|
||||
prompt={question.prompt}
|
||||
answer={question.answer}
|
||||
shouldDisplayExpanded={isFirstSection && isFirstQuestion}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return renderedQuestions;
|
||||
}
|
||||
}
|
||||
52
packages/website/ts/pages/faq/question.tsx
Normal file
52
packages/website/ts/pages/faq/question.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {Card, CardHeader, CardText} from 'material-ui/Card';
|
||||
|
||||
export interface QuestionProps {
|
||||
prompt: string;
|
||||
answer: React.ReactNode;
|
||||
shouldDisplayExpanded: boolean;
|
||||
}
|
||||
|
||||
interface QuestionState {
|
||||
isExpanded: boolean;
|
||||
}
|
||||
|
||||
export class Question extends React.Component<QuestionProps, QuestionState> {
|
||||
constructor(props: QuestionProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isExpanded: props.shouldDisplayExpanded,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
return (
|
||||
<div
|
||||
className="py1"
|
||||
>
|
||||
<Card
|
||||
initiallyExpanded={this.props.shouldDisplayExpanded}
|
||||
onExpandChange={this.onExchangeChange.bind(this)}
|
||||
>
|
||||
<CardHeader
|
||||
title={this.props.prompt}
|
||||
style={{borderBottom: this.state.isExpanded ? '1px solid rgba(0, 0, 0, 0.19)' : 'none'}}
|
||||
titleStyle={{color: 'rgb(66, 66, 66)'}}
|
||||
actAsExpander={true}
|
||||
showExpandableButton={true}
|
||||
/>
|
||||
<CardText expandable={true}>
|
||||
<div style={{lineHeight: 1.4}}>
|
||||
{this.props.answer}
|
||||
</div>
|
||||
</CardText>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private onExchangeChange() {
|
||||
this.setState({
|
||||
isExpanded: !this.state.isExpanded,
|
||||
});
|
||||
}
|
||||
}
|
||||
843
packages/website/ts/pages/landing/landing.tsx
Normal file
843
packages/website/ts/pages/landing/landing.tsx
Normal file
@@ -0,0 +1,843 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import DocumentTitle = require('react-document-title');
|
||||
import {Link} from 'react-router-dom';
|
||||
import RaisedButton from 'material-ui/RaisedButton';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {configs} from 'ts/utils/configs';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {Styles, WebsitePaths, ScreenWidths} from 'ts/types';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {TopBar} from 'ts/components/top_bar';
|
||||
import {Footer} from 'ts/components/footer';
|
||||
|
||||
interface BoxContent {
|
||||
title: string;
|
||||
description: string;
|
||||
imageUrl: string;
|
||||
classNames: string;
|
||||
}
|
||||
interface AssetType {
|
||||
title: string;
|
||||
imageUrl: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
interface UseCase {
|
||||
imageUrl: string;
|
||||
type: string;
|
||||
description: string;
|
||||
classNames: string;
|
||||
style?: React.CSSProperties;
|
||||
projectIconUrls: string[];
|
||||
}
|
||||
interface Project {
|
||||
logoFileName: string;
|
||||
projectUrl: string;
|
||||
}
|
||||
|
||||
const THROTTLE_TIMEOUT = 100;
|
||||
const CUSTOM_HERO_BACKGROUND_COLOR = '#404040';
|
||||
const CUSTOM_PROJECTS_BACKGROUND_COLOR = '#343333';
|
||||
const CUSTOM_WHITE_BACKGROUND = 'rgb(245, 245, 245)';
|
||||
const CUSTOM_WHITE_TEXT = '#E4E4E4';
|
||||
const CUSTOM_GRAY_TEXT = '#919191';
|
||||
|
||||
const boxContents: BoxContent[] = [
|
||||
{
|
||||
title: 'Trustless exchange',
|
||||
description: 'Built on Ethereum\'s distributed network with no centralized \
|
||||
point of failure and no down time, each trade is settled atomically \
|
||||
and without counterparty risk.',
|
||||
imageUrl: '/images/landing/distributed_network.png',
|
||||
classNames: '',
|
||||
},
|
||||
{
|
||||
title: 'Shared liquidity',
|
||||
description: 'By sharing a standard API, relayers can easily aggregate liquidity pools, \
|
||||
creating network effects around liquidity that compound as more relayers come online.',
|
||||
imageUrl: '/images/landing/liquidity.png',
|
||||
classNames: 'mx-auto',
|
||||
},
|
||||
{
|
||||
title: 'Open source',
|
||||
description: '0x is open source, permissionless and free to use. Trade directly with a known \
|
||||
counterparty for free or pay a relayer some ZRX tokens to access their liquidity \
|
||||
pool.',
|
||||
imageUrl: '/images/landing/open_source.png',
|
||||
classNames: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
const projects: Project[] = [
|
||||
{
|
||||
logoFileName: 'ethfinex-top.png',
|
||||
projectUrl: constants.ETHFINEX_URL,
|
||||
},
|
||||
{
|
||||
logoFileName: 'radar_relay_top.png',
|
||||
projectUrl: constants.RADAR_RELAY_URL,
|
||||
},
|
||||
{
|
||||
logoFileName: 'paradex_top.png',
|
||||
projectUrl: constants.PARADEX_URL,
|
||||
},
|
||||
{
|
||||
logoFileName: 'the_ocean.png',
|
||||
projectUrl: constants.OCEAN_URL,
|
||||
},
|
||||
{
|
||||
logoFileName: 'dydx.png',
|
||||
projectUrl: constants.DYDX_URL,
|
||||
},
|
||||
{
|
||||
logoFileName: 'melonport.png',
|
||||
projectUrl: constants.MELONPORT_URL,
|
||||
},
|
||||
{
|
||||
logoFileName: 'maker.png',
|
||||
projectUrl: constants.MAKER_URL,
|
||||
},
|
||||
{
|
||||
logoFileName: 'dharma.png',
|
||||
projectUrl: constants.DHARMA_URL,
|
||||
},
|
||||
{
|
||||
logoFileName: 'lendroid.png',
|
||||
projectUrl: constants.LENDROID_URL,
|
||||
},
|
||||
{
|
||||
logoFileName: 'district0x.png',
|
||||
projectUrl: constants.DISTRICT_0X_URL,
|
||||
},
|
||||
{
|
||||
logoFileName: 'aragon.png',
|
||||
projectUrl: constants.ARAGON_URL,
|
||||
},
|
||||
{
|
||||
logoFileName: 'blocknet.png',
|
||||
projectUrl: constants.BLOCKNET_URL,
|
||||
},
|
||||
{
|
||||
logoFileName: 'status.png',
|
||||
projectUrl: constants.STATUS_URL,
|
||||
},
|
||||
{
|
||||
logoFileName: 'augur.png',
|
||||
projectUrl: constants.AUGUR_URL,
|
||||
},
|
||||
{
|
||||
logoFileName: 'anx.png',
|
||||
projectUrl: constants.OPEN_ANX_URL,
|
||||
},
|
||||
{
|
||||
logoFileName: 'auctus.png',
|
||||
projectUrl: constants.AUCTUS_URL,
|
||||
},
|
||||
];
|
||||
|
||||
export interface LandingProps {
|
||||
location: Location;
|
||||
}
|
||||
|
||||
interface LandingState {
|
||||
screenWidth: ScreenWidths;
|
||||
}
|
||||
|
||||
export class Landing extends React.Component<LandingProps, LandingState> {
|
||||
private throttledScreenWidthUpdate: () => void;
|
||||
constructor(props: LandingProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
screenWidth: utils.getScreenWidth(),
|
||||
};
|
||||
this.throttledScreenWidthUpdate = _.throttle(this.updateScreenWidth.bind(this), THROTTLE_TIMEOUT);
|
||||
}
|
||||
public componentDidMount() {
|
||||
window.addEventListener('resize', this.throttledScreenWidthUpdate);
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
public componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.throttledScreenWidthUpdate);
|
||||
}
|
||||
public render() {
|
||||
return (
|
||||
<div id="landing" className="clearfix" style={{color: colors.grey800}}>
|
||||
<DocumentTitle title="0x Protocol"/>
|
||||
<TopBar
|
||||
blockchainIsLoaded={false}
|
||||
location={this.props.location}
|
||||
isNightVersion={true}
|
||||
style={{backgroundColor: CUSTOM_HERO_BACKGROUND_COLOR, position: 'relative'}}
|
||||
/>
|
||||
{this.renderHero()}
|
||||
{this.renderProjects()}
|
||||
{this.renderTokenizationSection()}
|
||||
{this.renderProtocolSection()}
|
||||
{this.renderInfoBoxes()}
|
||||
{this.renderBuildingBlocksSection()}
|
||||
{this.renderUseCases()}
|
||||
{this.renderCallToAction()}
|
||||
<Footer location={this.props.location} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderHero() {
|
||||
const isSmallScreen = this.state.screenWidth === ScreenWidths.SM;
|
||||
const buttonLabelStyle: React.CSSProperties = {
|
||||
textTransform: 'none',
|
||||
fontSize: isSmallScreen ? 12 : 14,
|
||||
fontWeight: 400,
|
||||
};
|
||||
const lightButtonStyle: React.CSSProperties = {
|
||||
borderRadius: 6,
|
||||
border: '1px solid #D8D8D8',
|
||||
lineHeight: '33px',
|
||||
height: 38,
|
||||
};
|
||||
const left = 'col lg-col-7 md-col-7 col-12 lg-pt4 md-pt4 sm-pt0 mt1 lg-pl4 md-pl4 sm-pl0 sm-px3 sm-center';
|
||||
return (
|
||||
<div
|
||||
className="clearfix py4"
|
||||
style={{backgroundColor: CUSTOM_HERO_BACKGROUND_COLOR}}
|
||||
>
|
||||
<div
|
||||
className="mx-auto max-width-4 clearfix"
|
||||
>
|
||||
<div className="lg-pt4 md-pt4 sm-pt2 lg-pb4 md-pb4 lg-my4 md-my4 sm-mt2 sm-mb4 clearfix">
|
||||
<div className="col lg-col-5 md-col-5 col-12 sm-center">
|
||||
<img
|
||||
src="/images/landing/hero_chip_image.png"
|
||||
height={isSmallScreen ? 300 : 395}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={left}
|
||||
style={{color: 'white'}}
|
||||
>
|
||||
<div style={{paddingLeft: isSmallScreen ? 0 : 12}}>
|
||||
<div
|
||||
className="sm-pb2"
|
||||
style={{fontFamily: 'Roboto Mono', fontSize: isSmallScreen ? 26 : 34}}
|
||||
>
|
||||
Powering decentralized exchange
|
||||
</div>
|
||||
<div
|
||||
className="pt2 h5 sm-mx-auto"
|
||||
style={{maxWidth: 446, fontFamily: 'Roboto Mono', lineHeight: 1.7, fontWeight: 300}}
|
||||
>
|
||||
0x is an open, permissionless protocol allowing for ERC20 tokens to
|
||||
be traded on the Ethereum blockchain.
|
||||
</div>
|
||||
<div className="pt3 clearfix sm-mx-auto" style={{maxWidth: 342}}>
|
||||
<div className="lg-pr2 md-pr2 col col-6 sm-center">
|
||||
<Link to={WebsitePaths.ZeroExJs} className="text-decoration-none">
|
||||
<RaisedButton
|
||||
style={{borderRadius: 6, minWidth: 157.36}}
|
||||
buttonStyle={{borderRadius: 6}}
|
||||
labelStyle={buttonLabelStyle}
|
||||
label="Build on 0x"
|
||||
onClick={_.noop}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col col-6 sm-center">
|
||||
<a
|
||||
href={constants.ZEROEX_CHAT_URL}
|
||||
target="_blank"
|
||||
className="text-decoration-none"
|
||||
>
|
||||
<RaisedButton
|
||||
style={{borderRadius: 6, minWidth: 150}}
|
||||
buttonStyle={lightButtonStyle}
|
||||
labelColor="white"
|
||||
backgroundColor={CUSTOM_HERO_BACKGROUND_COLOR}
|
||||
labelStyle={buttonLabelStyle}
|
||||
label="Join the community"
|
||||
onClick={_.noop}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderProjects() {
|
||||
const isSmallScreen = this.state.screenWidth === ScreenWidths.SM;
|
||||
const isMediumScreen = this.state.screenWidth === ScreenWidths.MD;
|
||||
const projectList = _.map(projects, (project: Project, i: number) => {
|
||||
const colWidth = isSmallScreen ? 3 : isMediumScreen ? 4 : 2 - (i % 2);
|
||||
return (
|
||||
<div
|
||||
key={`project-${project.logoFileName}`}
|
||||
className={`col col-${colWidth} center`}
|
||||
>
|
||||
<div>
|
||||
<a
|
||||
href={project.projectUrl}
|
||||
target="_blank"
|
||||
className="text-decoration-none"
|
||||
>
|
||||
<img
|
||||
src={`/images/landing/project_logos/${project.logoFileName}`}
|
||||
height={isSmallScreen ? 60 : 92}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
const titleStyle: React.CSSProperties = {
|
||||
fontFamily: 'Roboto Mono',
|
||||
color: '#A4A4A4',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: 300,
|
||||
letterSpacing: 3,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className="clearfix py4"
|
||||
style={{backgroundColor: CUSTOM_PROJECTS_BACKGROUND_COLOR}}
|
||||
>
|
||||
<div className="mx-auto max-width-4 clearfix sm-px3">
|
||||
<div
|
||||
className="h4 pb3 md-pl3 sm-pl2"
|
||||
style={titleStyle}
|
||||
>
|
||||
Projects building on 0x
|
||||
</div>
|
||||
<div className="clearfix">
|
||||
{projectList}
|
||||
</div>
|
||||
<div
|
||||
className="pt3 mx-auto center"
|
||||
style={{color: CUSTOM_GRAY_TEXT, fontFamily: 'Roboto Mono', maxWidth: 300, fontSize: 14}}
|
||||
>
|
||||
view the{' '}
|
||||
<Link
|
||||
to={`${WebsitePaths.Wiki}#List-of-Projects-Using-0x-Protocol`}
|
||||
className="text-decoration-none underline"
|
||||
style={{color: CUSTOM_GRAY_TEXT}}
|
||||
>
|
||||
full list
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderTokenizationSection() {
|
||||
const isSmallScreen = this.state.screenWidth === ScreenWidths.SM;
|
||||
return (
|
||||
<div
|
||||
className="clearfix lg-py4 md-py4 sm-pb4 sm-pt2"
|
||||
style={{backgroundColor: CUSTOM_WHITE_BACKGROUND}}
|
||||
>
|
||||
<div className="mx-auto max-width-4 py4 clearfix">
|
||||
{isSmallScreen &&
|
||||
this.renderTokenCloud()
|
||||
}
|
||||
<div className="col lg-col-6 md-col-6 col-12">
|
||||
<div className="mx-auto" style={{maxWidth: 385, paddingTop: 7}}>
|
||||
<div
|
||||
className="lg-h1 md-h1 sm-h2 sm-center sm-pt3"
|
||||
style={{fontFamily: 'Roboto Mono'}}
|
||||
>
|
||||
The world's value is becoming tokenized
|
||||
</div>
|
||||
<div
|
||||
className="pb2 lg-pt2 md-pt2 sm-pt3 sm-px3 h5 sm-center"
|
||||
style={{fontFamily: 'Roboto Mono', lineHeight: 1.7}}
|
||||
>
|
||||
{isSmallScreen ?
|
||||
<span>
|
||||
The Ethereum blockchain is an open, borderless financial system that represents
|
||||
a wide variety of assets as cryptographic tokens. In the future, most digital
|
||||
assets and goods will be tokenized.
|
||||
</span> :
|
||||
<div>
|
||||
<div>
|
||||
The Ethereum blockchain is an open, borderless
|
||||
financial system that represents
|
||||
</div>
|
||||
<div>
|
||||
a wide variety of assets as cryptographic tokens.
|
||||
In the future, most digital assets and goods will be tokenized.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className="flex pt1 sm-px3">
|
||||
{this.renderAssetTypes()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!isSmallScreen &&
|
||||
this.renderTokenCloud()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderProtocolSection() {
|
||||
const isSmallScreen = this.state.screenWidth === ScreenWidths.SM;
|
||||
return (
|
||||
<div
|
||||
className="clearfix lg-py4 md-py4 sm-pt4"
|
||||
style={{backgroundColor: CUSTOM_HERO_BACKGROUND_COLOR}}
|
||||
>
|
||||
<div className="mx-auto max-width-4 lg-py4 md-py4 sm-pt4 clearfix">
|
||||
<div className="col lg-col-6 md-col-6 col-12 sm-center">
|
||||
<img
|
||||
src="/images/landing/relayer_diagram.png"
|
||||
height={isSmallScreen ? 326 : 426}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col lg-col-6 md-col-6 col-12 lg-pr3 md-pr3 sm-mx-auto"
|
||||
style={{color: CUSTOM_WHITE_TEXT, paddingTop: 8, maxWidth: isSmallScreen ? 'none' : 445}}
|
||||
>
|
||||
<div
|
||||
className="lg-h1 md-h1 sm-h2 pb1 sm-pt3 sm-center"
|
||||
style={{fontFamily: 'Roboto Mono'}}
|
||||
>
|
||||
<div>
|
||||
Off-chain order relay
|
||||
</div>
|
||||
<div>
|
||||
On-chain settlement
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pb2 pt2 h5 sm-center sm-px3 sm-mx-auto"
|
||||
style={{fontFamily: 'Roboto Mono', lineHeight: 1.7, fontWeight: 300, maxWidth: 445}}
|
||||
>
|
||||
In 0x protocol, orders are transported off-chain, massively reducing gas
|
||||
costs and eliminating blockchain bloat. Relayers help broadcast orders and
|
||||
collect a fee each time they facilitate a trade. Anyone can build a relayer.
|
||||
</div>
|
||||
<div
|
||||
className="pt3 sm-mx-auto sm-px3"
|
||||
style={{color: CUSTOM_GRAY_TEXT, maxWidth: isSmallScreen ? 412 : 'none'}}
|
||||
>
|
||||
<div className="flex" style={{fontSize: 18}}>
|
||||
<div
|
||||
className="lg-h4 md-h4 sm-h5"
|
||||
style={{letterSpacing: isSmallScreen ? 1 : 3, fontFamily: 'Roboto Mono'}}
|
||||
>
|
||||
RELAYERS BUILDING ON 0X
|
||||
</div>
|
||||
<div className="h5" style={{marginLeft: isSmallScreen ? 26 : 49}}>
|
||||
<Link
|
||||
to={`${WebsitePaths.Wiki}#List-of-Projects-Using-0x-Protocol`}
|
||||
className="text-decoration-none underline"
|
||||
style={{color: CUSTOM_GRAY_TEXT, fontFamily: 'Roboto Mono'}}
|
||||
>
|
||||
view all
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg-flex md-flex sm-clearfix pt3" style={{opacity: 0.4}}>
|
||||
<div className="col col-4 sm-center">
|
||||
<img
|
||||
src="/images/landing/ethfinex.png"
|
||||
style={{height: isSmallScreen ? 85 : 107}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col col-4 center"
|
||||
>
|
||||
<img
|
||||
src="/images/landing/radar_relay.png"
|
||||
style={{height: isSmallScreen ? 85 : 107}}
|
||||
/>
|
||||
</div>
|
||||
<div className="col col-4 sm-center" style={{textAlign: 'right'}}>
|
||||
<img
|
||||
src="/images/landing/paradex.png"
|
||||
style={{height: isSmallScreen ? 85 : 107}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderBuildingBlocksSection() {
|
||||
const isSmallScreen = this.state.screenWidth === ScreenWidths.SM;
|
||||
const underlineStyle: React.CSSProperties = {
|
||||
height: isSmallScreen ? 18 : 23,
|
||||
lineHeight: 'none',
|
||||
borderBottom: '2px solid #979797',
|
||||
};
|
||||
const descriptionStyle: React.CSSProperties = {
|
||||
fontFamily: 'Roboto Mono',
|
||||
lineHeight: isSmallScreen ? 1.5 : 2,
|
||||
fontWeight: 300,
|
||||
fontSize: 15,
|
||||
maxWidth: isSmallScreen ? 375 : 'none',
|
||||
};
|
||||
const callToActionStyle: React.CSSProperties = {
|
||||
fontFamily: 'Roboto Mono',
|
||||
fontSize: 15,
|
||||
fontWeight: 300,
|
||||
maxWidth: isSmallScreen ? 375 : 441,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className="clearfix lg-pt4 md-pt4"
|
||||
style={{backgroundColor: CUSTOM_HERO_BACKGROUND_COLOR}}
|
||||
>
|
||||
<div className="mx-auto max-width-4 lg-pt4 md-pt4 lg-mb4 md-mb4 sm-mb2 clearfix">
|
||||
{isSmallScreen &&
|
||||
this.renderBlockChipImage()
|
||||
}
|
||||
<div
|
||||
className="col lg-col-6 md-col-6 col-12 lg-pr3 md-pr3 sm-px3"
|
||||
style={{color: CUSTOM_WHITE_TEXT}}
|
||||
>
|
||||
<div
|
||||
className="pb1 lg-pt4 md-pt4 sm-pt3 lg-h1 md-h1 sm-h2 sm-px3 sm-center"
|
||||
style={{fontFamily: 'Roboto Mono'}}
|
||||
>
|
||||
A building block for dApps
|
||||
</div>
|
||||
<div
|
||||
className="pb3 pt2 sm-mx-auto sm-center"
|
||||
style={descriptionStyle}
|
||||
>
|
||||
0x protocol is a pluggable building block for dApps that require exchange functionality.
|
||||
Join the many developers that are already using 0x in their web applications and smart
|
||||
contracts.
|
||||
</div>
|
||||
<div
|
||||
className="sm-mx-auto sm-center"
|
||||
style={callToActionStyle}
|
||||
>
|
||||
Learn how in our{' '}
|
||||
<Link
|
||||
to={WebsitePaths.ZeroExJs}
|
||||
className="text-decoration-none underline"
|
||||
style={{color: CUSTOM_WHITE_TEXT, fontFamily: 'Roboto Mono'}}
|
||||
>
|
||||
0x.js
|
||||
</Link>
|
||||
{' '}and{' '}
|
||||
<Link
|
||||
to={WebsitePaths.SmartContracts}
|
||||
className="text-decoration-none underline"
|
||||
style={{color: CUSTOM_WHITE_TEXT, fontFamily: 'Roboto Mono'}}
|
||||
>
|
||||
smart contract
|
||||
</Link>
|
||||
{' '}docs
|
||||
</div>
|
||||
</div>
|
||||
{!isSmallScreen &&
|
||||
this.renderBlockChipImage()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderBlockChipImage() {
|
||||
const isSmallScreen = this.state.screenWidth === ScreenWidths.SM;
|
||||
return (
|
||||
<div className="col lg-col-6 md-col-6 col-12 sm-center">
|
||||
<img
|
||||
src="/images/landing/0x_chips.png"
|
||||
height={isSmallScreen ? 240 : 368}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderTokenCloud() {
|
||||
const isSmallScreen = this.state.screenWidth === ScreenWidths.SM;
|
||||
return (
|
||||
<div className="col lg-col-6 md-col-6 col-12 center">
|
||||
<img
|
||||
src="/images/landing/tokenized_world.png"
|
||||
height={isSmallScreen ? 280 : 364.5}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderAssetTypes() {
|
||||
const isSmallScreen = this.state.screenWidth === ScreenWidths.SM;
|
||||
const assetTypes: AssetType[] = [
|
||||
{
|
||||
title: 'Currency',
|
||||
imageUrl: '/images/landing/currency.png',
|
||||
},
|
||||
{
|
||||
title: 'Traditional assets',
|
||||
imageUrl: '/images/landing/stocks.png',
|
||||
style: {paddingLeft: isSmallScreen ? 41 : 56, paddingRight: isSmallScreen ? 41 : 56},
|
||||
},
|
||||
{
|
||||
title: 'Digital goods',
|
||||
imageUrl: '/images/landing/digital_goods.png',
|
||||
},
|
||||
];
|
||||
const assets = _.map(assetTypes, (assetType: AssetType) => {
|
||||
const style = _.isUndefined(assetType.style) ? {} : assetType.style;
|
||||
return (
|
||||
<div
|
||||
key={`asset-${assetType.title}`}
|
||||
className="center"
|
||||
style={{opacity: 0.8, ...style}}
|
||||
>
|
||||
<div>
|
||||
<img
|
||||
src={assetType.imageUrl}
|
||||
height="80"
|
||||
/>
|
||||
</div>
|
||||
<div style={{fontFamily: 'Roboto Mono', fontSize: 13.5, fontWeight: 400, opacity: 0.75}}>
|
||||
{assetType.title}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return assets;
|
||||
}
|
||||
private renderLink(label: string, path: string, color: string, style?: React.CSSProperties) {
|
||||
return (
|
||||
<div
|
||||
style={{borderBottom: `1px solid ${color}`, paddingBottom: 1, height: 20, lineHeight: 1.7, ...style}}
|
||||
>
|
||||
<Link
|
||||
to={path}
|
||||
className="text-decoration-none"
|
||||
style={{color, fontFamily: 'Roboto Mono'}}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderInfoBoxes() {
|
||||
const isSmallScreen = this.state.screenWidth === ScreenWidths.SM;
|
||||
const boxStyle: React.CSSProperties = {
|
||||
maxWidth: 252,
|
||||
height: 386,
|
||||
backgroundColor: '#F9F9F9',
|
||||
borderRadius: 5,
|
||||
padding: '10px 24px 24px',
|
||||
};
|
||||
const boxes = _.map(boxContents, (boxContent: BoxContent) => {
|
||||
return (
|
||||
<div
|
||||
key={`box-${boxContent.title}`}
|
||||
className="col lg-col-4 md-col-4 col-12 sm-pb4"
|
||||
>
|
||||
<div
|
||||
className={`center sm-mx-auto ${!isSmallScreen && boxContent.classNames}`}
|
||||
style={boxStyle}
|
||||
>
|
||||
<div>
|
||||
<img src={boxContent.imageUrl} style={{height: 210}} />
|
||||
</div>
|
||||
<div
|
||||
className="h3"
|
||||
style={{color: 'black', fontFamily: 'Roboto Mono'}}
|
||||
>
|
||||
{boxContent.title}
|
||||
</div>
|
||||
<div
|
||||
className="pt2 pb2"
|
||||
style={{fontFamily: 'Roboto Mono', fontSize: 14}}
|
||||
>
|
||||
{boxContent.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className="clearfix"
|
||||
style={{backgroundColor: CUSTOM_HERO_BACKGROUND_COLOR}}
|
||||
>
|
||||
<div
|
||||
className="mx-auto py4 sm-mt2 clearfix"
|
||||
style={{maxWidth: '60em'}}
|
||||
>
|
||||
{boxes}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderUseCases() {
|
||||
const isSmallScreen = this.state.screenWidth === ScreenWidths.SM;
|
||||
const isMediumScreen = this.state.screenWidth === ScreenWidths.MD;
|
||||
|
||||
const useCases: UseCase[] = [
|
||||
{
|
||||
imageUrl: '/images/landing/governance_icon.png',
|
||||
type: 'Decentralized governance',
|
||||
description: 'Decentralized organizations use tokens to represent ownership and \
|
||||
guide their governance logic. 0x allows decentralized organizations \
|
||||
to seamlessly and safely trade ownership for startup capital.',
|
||||
projectIconUrls: ['/images/landing/aragon.png'],
|
||||
classNames: 'lg-px2 md-px2',
|
||||
},
|
||||
{
|
||||
imageUrl: '/images/landing/prediction_market_icon.png',
|
||||
type: 'Prediction markets',
|
||||
description: 'Decentralized prediction market platforms generate sets of tokens that \
|
||||
represent a financial stake in the outcomes of real-world events. 0x allows \
|
||||
these tokens to be instantly tradable.',
|
||||
projectIconUrls: ['/images/landing/augur.png'],
|
||||
classNames: 'lg-px2 md-px2',
|
||||
},
|
||||
{
|
||||
imageUrl: '/images/landing/stable_tokens_icon.png',
|
||||
type: 'Stable tokens',
|
||||
description: 'Novel economic constructs such as stable coins require efficient, liquid \
|
||||
markets to succeed. 0x will facilitate the underlying economic mechanisms \
|
||||
that allow these tokens to remain stable.',
|
||||
projectIconUrls: ['/images/landing/maker.png'],
|
||||
classNames: 'lg-px2 md-px2',
|
||||
},
|
||||
{
|
||||
imageUrl: '/images/landing/loans_icon.png',
|
||||
type: 'Decentralized loans',
|
||||
description: 'Efficient lending requires liquid markets where investors can buy and re-sell loans. \
|
||||
0x enables an ecosystem of lenders to self-organize and efficiently determine \
|
||||
market prices for all outstanding loans.',
|
||||
projectIconUrls: ['/images/landing/dharma.png', '/images/landing/lendroid.png'],
|
||||
classNames: 'lg-pr2 md-pr2 lg-col-6 md-col-6',
|
||||
style: {width: 291, float: 'right', marginTop: !isSmallScreen ? 38 : 0},
|
||||
},
|
||||
{
|
||||
imageUrl: '/images/landing/fund_management_icon.png',
|
||||
type: 'Fund management',
|
||||
description: 'Decentralized fund management limits fund managers to investing in pre-agreed \
|
||||
upon asset classes. Embedding 0x into fund management smart contracts enables \
|
||||
them to enforce these security constraints.',
|
||||
projectIconUrls: ['/images/landing/melonport.png'],
|
||||
classNames: 'lg-pl2 md-pl2 lg-col-6 md-col-6',
|
||||
style: {width: 291, marginTop: !isSmallScreen ? 38 : 0},
|
||||
},
|
||||
];
|
||||
|
||||
const cases = _.map(useCases, (useCase: UseCase) => {
|
||||
const style = _.isUndefined(useCase.style) || isSmallScreen ? {} : useCase.style;
|
||||
const useCaseBoxStyle = {
|
||||
color: '#A2A2A2',
|
||||
border: '1px solid #565656',
|
||||
borderRadius: 4,
|
||||
maxWidth: isSmallScreen ? 375 : 'none',
|
||||
...style,
|
||||
};
|
||||
const typeStyle: React.CSSProperties = {
|
||||
color: '#EBEBEB',
|
||||
fontSize: 13,
|
||||
textTransform: 'uppercase',
|
||||
fontFamily: 'Roboto Mono',
|
||||
fontWeight: 300,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
key={`useCase-${useCase.type}`}
|
||||
className={`col lg-col-4 md-col-4 col-12 sm-pt3 sm-px3 sm-pb3 ${useCase.classNames}`}
|
||||
>
|
||||
<div
|
||||
className="relative p2 pb2 sm-mx-auto"
|
||||
style={useCaseBoxStyle}
|
||||
>
|
||||
<div
|
||||
className="absolute center"
|
||||
style={{top: -35, width: 'calc(100% - 32px)'}}
|
||||
>
|
||||
<img src={useCase.imageUrl} style={{height: 50}} />
|
||||
</div>
|
||||
<div className="pt2 center" style={typeStyle}>
|
||||
{useCase.type}
|
||||
</div>
|
||||
<div
|
||||
className="pt2"
|
||||
style={{lineHeight: 1.5, fontSize: 14, overflow: 'hidden', height: 104}}
|
||||
>
|
||||
{useCase.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div
|
||||
className="clearfix pb4 lg-pt2 md-pt2 sm-pt4"
|
||||
style={{backgroundColor: CUSTOM_HERO_BACKGROUND_COLOR}}
|
||||
>
|
||||
<div
|
||||
className="mx-auto pb4 pt3 mt1 sm-mt2 clearfix"
|
||||
style={{maxWidth: '67em'}}
|
||||
>
|
||||
{cases}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderCallToAction() {
|
||||
const isSmallScreen = this.state.screenWidth === ScreenWidths.SM;
|
||||
const buttonLabelStyle: React.CSSProperties = {
|
||||
textTransform: 'none',
|
||||
fontSize: 15,
|
||||
fontWeight: 400,
|
||||
};
|
||||
const lightButtonStyle: React.CSSProperties = {
|
||||
borderRadius: 6,
|
||||
border: '1px solid #a0a0a0',
|
||||
lineHeight: '33px',
|
||||
height: 49,
|
||||
};
|
||||
const callToActionClassNames = 'col lg-col-8 md-col-8 col-12 lg-pr3 md-pr3 \
|
||||
lg-right-align md-right-align sm-center sm-px3 h4';
|
||||
return (
|
||||
<div
|
||||
className="clearfix pb4"
|
||||
style={{backgroundColor: CUSTOM_HERO_BACKGROUND_COLOR}}
|
||||
>
|
||||
<div
|
||||
className="mx-auto max-width-4 pb4 mb3 clearfix"
|
||||
>
|
||||
<div
|
||||
className={callToActionClassNames}
|
||||
style={{fontFamily: 'Roboto Mono', color: 'white', lineHeight: isSmallScreen ? 1.7 : 3}}
|
||||
>
|
||||
Get started on building the decentralized future
|
||||
</div>
|
||||
<div className="col lg-col-4 md-col-4 col-12 sm-center sm-pt2">
|
||||
<Link to={WebsitePaths.ZeroExJs} className="text-decoration-none">
|
||||
<RaisedButton
|
||||
style={{borderRadius: 6, minWidth: 150}}
|
||||
buttonStyle={lightButtonStyle}
|
||||
labelColor={colors.white}
|
||||
backgroundColor={CUSTOM_HERO_BACKGROUND_COLOR}
|
||||
labelStyle={buttonLabelStyle}
|
||||
label="Build on 0x"
|
||||
onClick={_.noop}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private updateScreenWidth() {
|
||||
const newScreenWidth = utils.getScreenWidth();
|
||||
if (newScreenWidth !== this.state.screenWidth) {
|
||||
this.setState({
|
||||
screenWidth: newScreenWidth,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
46
packages/website/ts/pages/not_found.tsx
Normal file
46
packages/website/ts/pages/not_found.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import {Styles} from 'ts/types';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {Footer} from 'ts/components/footer';
|
||||
import {TopBar} from 'ts/components/top_bar';
|
||||
|
||||
export interface NotFoundProps {
|
||||
location: Location;
|
||||
}
|
||||
|
||||
interface NotFoundState {}
|
||||
|
||||
const styles: Styles = {
|
||||
thin: {
|
||||
fontWeight: 100,
|
||||
},
|
||||
};
|
||||
|
||||
export class NotFound extends React.Component<NotFoundProps, NotFoundState> {
|
||||
public render() {
|
||||
return (
|
||||
<div>
|
||||
<TopBar
|
||||
blockchainIsLoaded={false}
|
||||
location={this.props.location}
|
||||
/>
|
||||
<div className="mx-auto max-width-4 py4">
|
||||
<div className="center py4">
|
||||
<div className="py4">
|
||||
<div className="py4">
|
||||
<h1 style={{...styles.thin}}>404 Not Found</h1>
|
||||
<div className="py1">
|
||||
<div className="py3">
|
||||
Hm... looks like we couldn't find what you are looking for.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer location={this.props.location} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
98
packages/website/ts/pages/shared/anchor_title.tsx
Normal file
98
packages/website/ts/pages/shared/anchor_title.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import * as React from 'react';
|
||||
import {Styles, HeaderSizes} from 'ts/types';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {Link as ScrollLink} from 'react-scroll';
|
||||
|
||||
const headerSizeToScrollOffset: {[headerSize: string]: number} = {
|
||||
h2: -20,
|
||||
h3: 0,
|
||||
};
|
||||
|
||||
interface AnchorTitleProps {
|
||||
title: string|React.ReactNode;
|
||||
id: string;
|
||||
headerSize: HeaderSizes;
|
||||
shouldShowAnchor: boolean;
|
||||
}
|
||||
|
||||
interface AnchorTitleState {
|
||||
isHovering: boolean;
|
||||
}
|
||||
|
||||
const styles: Styles = {
|
||||
anchor: {
|
||||
fontSize: 20,
|
||||
transform: 'rotate(45deg)',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
headers: {
|
||||
WebkitMarginStart: 0,
|
||||
WebkitMarginEnd: 0,
|
||||
fontWeight: 'bold',
|
||||
display: 'block',
|
||||
},
|
||||
h1: {
|
||||
fontSize: '1.8em',
|
||||
WebkitMarginBefore: '0.83em',
|
||||
WebkitMarginAfter: '0.83em',
|
||||
},
|
||||
h2: {
|
||||
fontSize: '1.5em',
|
||||
WebkitMarginBefore: '0.83em',
|
||||
WebkitMarginAfter: '0.83em',
|
||||
},
|
||||
h3: {
|
||||
fontSize: '1.17em',
|
||||
WebkitMarginBefore: '1em',
|
||||
WebkitMarginAfter: '1em',
|
||||
},
|
||||
};
|
||||
|
||||
export class AnchorTitle extends React.Component<AnchorTitleProps, AnchorTitleState> {
|
||||
constructor(props: AnchorTitleProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isHovering: false,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
let opacity = 0;
|
||||
if (this.props.shouldShowAnchor) {
|
||||
if (this.state.isHovering) {
|
||||
opacity = 0.6;
|
||||
} else {
|
||||
opacity = 1;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div className="relative flex" style={{...styles[this.props.headerSize], ...styles.headers}}>
|
||||
<div
|
||||
className="inline-block"
|
||||
style={{paddingRight: 4}}
|
||||
>
|
||||
{this.props.title}
|
||||
</div>
|
||||
<ScrollLink
|
||||
to={this.props.id}
|
||||
offset={headerSizeToScrollOffset[this.props.headerSize]}
|
||||
duration={constants.DOCS_SCROLL_DURATION_MS}
|
||||
containerId={constants.DOCS_CONTAINER_ID}
|
||||
>
|
||||
<i
|
||||
className="zmdi zmdi-link"
|
||||
onClick={utils.setUrlHash.bind(utils, this.props.id)}
|
||||
style={{...styles.anchor, opacity}}
|
||||
onMouseOver={this.setHoverState.bind(this, true)}
|
||||
onMouseOut={this.setHoverState.bind(this, false)}
|
||||
/>
|
||||
</ScrollLink>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private setHoverState(isHovering: boolean) {
|
||||
this.setState({
|
||||
isHovering,
|
||||
});
|
||||
}
|
||||
}
|
||||
20
packages/website/ts/pages/shared/markdown_code_block.tsx
Normal file
20
packages/website/ts/pages/shared/markdown_code_block.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import * as HighLight from 'react-highlight';
|
||||
|
||||
interface MarkdownCodeBlockProps {
|
||||
literal: string;
|
||||
language: string;
|
||||
}
|
||||
|
||||
export function MarkdownCodeBlock(props: MarkdownCodeBlockProps) {
|
||||
return (
|
||||
<span style={{fontSize: 16}}>
|
||||
<HighLight
|
||||
className={props.language || 'js'}
|
||||
>
|
||||
{props.literal}
|
||||
</HighLight>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
77
packages/website/ts/pages/shared/markdown_section.tsx
Normal file
77
packages/website/ts/pages/shared/markdown_section.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import * as ReactMarkdown from 'react-markdown';
|
||||
import {Element as ScrollElement} from 'react-scroll';
|
||||
import {AnchorTitle} from 'ts/pages/shared/anchor_title';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {MarkdownCodeBlock} from 'ts/pages/shared/markdown_code_block';
|
||||
import RaisedButton from 'material-ui/RaisedButton';
|
||||
import {HeaderSizes} from 'ts/types';
|
||||
|
||||
interface MarkdownSectionProps {
|
||||
sectionName: string;
|
||||
markdownContent: string;
|
||||
headerSize?: HeaderSizes;
|
||||
githubLink?: string;
|
||||
}
|
||||
|
||||
interface MarkdownSectionState {
|
||||
shouldShowAnchor: boolean;
|
||||
}
|
||||
|
||||
export class MarkdownSection extends React.Component<MarkdownSectionProps, MarkdownSectionState> {
|
||||
public static defaultProps: Partial<MarkdownSectionProps> = {
|
||||
headerSize: HeaderSizes.H3,
|
||||
};
|
||||
constructor(props: MarkdownSectionProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
shouldShowAnchor: false,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const sectionName = this.props.sectionName;
|
||||
const id = utils.getIdFromName(sectionName);
|
||||
return (
|
||||
<div
|
||||
className="pt2 pr3 md-pl2 sm-pl3 overflow-hidden"
|
||||
onMouseOver={this.setAnchorVisibility.bind(this, true)}
|
||||
onMouseOut={this.setAnchorVisibility.bind(this, false)}
|
||||
>
|
||||
<ScrollElement name={id}>
|
||||
<div className="clearfix">
|
||||
<div className="col lg-col-8 md-col-8 sm-col-12">
|
||||
<span style={{textTransform: 'capitalize'}}>
|
||||
<AnchorTitle
|
||||
headerSize={this.props.headerSize}
|
||||
title={sectionName}
|
||||
id={id}
|
||||
shouldShowAnchor={this.state.shouldShowAnchor}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="col col-4 sm-hide xs-hide py2 right-align">
|
||||
{!_.isUndefined(this.props.githubLink) &&
|
||||
<RaisedButton
|
||||
href={this.props.githubLink}
|
||||
target="_blank"
|
||||
label="Edit on Github"
|
||||
icon={<i className="zmdi zmdi-github" style={{fontSize: 23}} />}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<ReactMarkdown
|
||||
source={this.props.markdownContent}
|
||||
renderers={{CodeBlock: MarkdownCodeBlock}}
|
||||
/>
|
||||
</ScrollElement>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private setAnchorVisibility(shouldShowAnchor: boolean) {
|
||||
this.setState({
|
||||
shouldShowAnchor,
|
||||
});
|
||||
}
|
||||
}
|
||||
163
packages/website/ts/pages/shared/nested_sidebar_menu.tsx
Normal file
163
packages/website/ts/pages/shared/nested_sidebar_menu.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import MenuItem from 'material-ui/MenuItem';
|
||||
import {colors} from 'material-ui/styles';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {VersionDropDown} from 'ts/pages/shared/version_drop_down';
|
||||
import {ZeroExJsDocSections, Styles, MenuSubsectionsBySection, Docs} from 'ts/types';
|
||||
import {typeDocUtils} from 'ts/utils/typedoc_utils';
|
||||
import {Link as ScrollLink} from 'react-scroll';
|
||||
|
||||
interface NestedSidebarMenuProps {
|
||||
topLevelMenu: {[topLevel: string]: string[]};
|
||||
menuSubsectionsBySection: MenuSubsectionsBySection;
|
||||
shouldDisplaySectionHeaders?: boolean;
|
||||
onMenuItemClick?: () => void;
|
||||
selectedVersion?: string;
|
||||
versions?: string[];
|
||||
doc?: Docs;
|
||||
isSectionHeaderClickable?: boolean;
|
||||
}
|
||||
|
||||
interface NestedSidebarMenuState {}
|
||||
|
||||
const styles: Styles = {
|
||||
menuItemWithHeaders: {
|
||||
minHeight: 0,
|
||||
},
|
||||
menuItemWithoutHeaders: {
|
||||
minHeight: 48,
|
||||
},
|
||||
menuItemInnerDivWithHeaders: {
|
||||
lineHeight: 2,
|
||||
},
|
||||
};
|
||||
|
||||
export class NestedSidebarMenu extends React.Component<NestedSidebarMenuProps, NestedSidebarMenuState> {
|
||||
public static defaultProps: Partial<NestedSidebarMenuProps> = {
|
||||
shouldDisplaySectionHeaders: true,
|
||||
onMenuItemClick: _.noop,
|
||||
};
|
||||
public render() {
|
||||
const navigation = _.map(this.props.topLevelMenu, (menuItems: string[], sectionName: string) => {
|
||||
const finalSectionName = sectionName.replace(/-/g, ' ');
|
||||
if (this.props.shouldDisplaySectionHeaders) {
|
||||
const id = utils.getIdFromName(sectionName);
|
||||
return (
|
||||
<div
|
||||
key={`section-${sectionName}`}
|
||||
className="py1"
|
||||
>
|
||||
<ScrollLink
|
||||
to={id}
|
||||
offset={-20}
|
||||
duration={constants.DOCS_SCROLL_DURATION_MS}
|
||||
containerId={constants.DOCS_CONTAINER_ID}
|
||||
>
|
||||
<div
|
||||
style={{color: colors.grey500, cursor: 'pointer'}}
|
||||
className="pb1"
|
||||
>
|
||||
{finalSectionName.toUpperCase()}
|
||||
</div>
|
||||
</ScrollLink>
|
||||
{this.renderMenuItems(menuItems)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key={`section-${sectionName}`} >
|
||||
{this.renderMenuItems(menuItems)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
{!_.isUndefined(this.props.versions) &&
|
||||
!_.isUndefined(this.props.selectedVersion) &&
|
||||
!_.isUndefined(this.props.doc) &&
|
||||
<VersionDropDown
|
||||
selectedVersion={this.props.selectedVersion}
|
||||
versions={this.props.versions}
|
||||
doc={this.props.doc}
|
||||
/>
|
||||
}
|
||||
{navigation}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderMenuItems(menuItemNames: string[]): React.ReactNode[] {
|
||||
const menuItemStyles = this.props.shouldDisplaySectionHeaders ?
|
||||
styles.menuItemWithHeaders :
|
||||
styles.menuItemWithoutHeaders;
|
||||
const menuItemInnerDivStyles = this.props.shouldDisplaySectionHeaders ?
|
||||
styles.menuItemInnerDivWithHeaders : {};
|
||||
const menuItems = _.map(menuItemNames, menuItemName => {
|
||||
const id = utils.getIdFromName(menuItemName);
|
||||
return (
|
||||
<div key={menuItemName}>
|
||||
<ScrollLink
|
||||
key={`menuItem-${menuItemName}`}
|
||||
to={id}
|
||||
offset={-10}
|
||||
duration={constants.DOCS_SCROLL_DURATION_MS}
|
||||
containerId={constants.DOCS_CONTAINER_ID}
|
||||
>
|
||||
<MenuItem
|
||||
onTouchTap={this.onMenuItemClick.bind(this, menuItemName)}
|
||||
style={menuItemStyles}
|
||||
innerDivStyle={menuItemInnerDivStyles}
|
||||
>
|
||||
<span style={{textTransform: 'capitalize'}}>
|
||||
{menuItemName}
|
||||
</span>
|
||||
</MenuItem>
|
||||
</ScrollLink>
|
||||
{this.renderMenuItemSubsections(menuItemName)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return menuItems;
|
||||
}
|
||||
private renderMenuItemSubsections(menuItemName: string): React.ReactNode {
|
||||
if (_.isUndefined(this.props.menuSubsectionsBySection[menuItemName])) {
|
||||
return null;
|
||||
}
|
||||
return this.renderMenuSubsectionsBySection(menuItemName, this.props.menuSubsectionsBySection[menuItemName]);
|
||||
}
|
||||
private renderMenuSubsectionsBySection(menuItemName: string, entityNames: string[]): React.ReactNode {
|
||||
return (
|
||||
<ul style={{margin: 0, listStyleType: 'none', paddingLeft: 0}} key={menuItemName}>
|
||||
{_.map(entityNames, entityName => {
|
||||
const id = utils.getIdFromName(entityName);
|
||||
return (
|
||||
<li key={`menuItem-${entityName}`}>
|
||||
<ScrollLink
|
||||
to={id}
|
||||
offset={0}
|
||||
duration={constants.DOCS_SCROLL_DURATION_MS}
|
||||
containerId={constants.DOCS_CONTAINER_ID}
|
||||
onTouchTap={this.onMenuItemClick.bind(this, entityName)}
|
||||
>
|
||||
<MenuItem
|
||||
onTouchTap={this.onMenuItemClick.bind(this, menuItemName)}
|
||||
style={{minHeight: 35}}
|
||||
innerDivStyle={{paddingLeft: 36, fontSize: 14, lineHeight: '35px'}}
|
||||
>
|
||||
{entityName}
|
||||
</MenuItem>
|
||||
</ScrollLink>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
private onMenuItemClick(menuItemName: string): void {
|
||||
const id = utils.getIdFromName(menuItemName);
|
||||
utils.setUrlHash(id);
|
||||
this.props.onMenuItemClick();
|
||||
}
|
||||
}
|
||||
50
packages/website/ts/pages/shared/section_header.tsx
Normal file
50
packages/website/ts/pages/shared/section_header.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import * as React from 'react';
|
||||
import {Element as ScrollElement} from 'react-scroll';
|
||||
import {AnchorTitle} from 'ts/pages/shared/anchor_title';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {HeaderSizes} from 'ts/types';
|
||||
|
||||
interface SectionHeaderProps {
|
||||
sectionName: string;
|
||||
headerSize?: HeaderSizes;
|
||||
}
|
||||
|
||||
interface SectionHeaderState {
|
||||
shouldShowAnchor: boolean;
|
||||
}
|
||||
|
||||
export class SectionHeader extends React.Component<SectionHeaderProps, SectionHeaderState> {
|
||||
public static defaultProps: Partial<SectionHeaderProps> = {
|
||||
headerSize: HeaderSizes.H2,
|
||||
};
|
||||
constructor(props: SectionHeaderProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
shouldShowAnchor: false,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
const sectionName = this.props.sectionName.replace(/-/g, ' ');
|
||||
const id = utils.getIdFromName(sectionName);
|
||||
return (
|
||||
<div
|
||||
onMouseOver={this.setAnchorVisibility.bind(this, true)}
|
||||
onMouseOut={this.setAnchorVisibility.bind(this, false)}
|
||||
>
|
||||
<ScrollElement name={id}>
|
||||
<AnchorTitle
|
||||
headerSize={this.props.headerSize}
|
||||
title={<span style={{textTransform: 'capitalize'}}>{sectionName}</span>}
|
||||
id={id}
|
||||
shouldShowAnchor={this.state.shouldShowAnchor}
|
||||
/>
|
||||
</ScrollElement>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private setAnchorVisibility(shouldShowAnchor: boolean) {
|
||||
this.setState({
|
||||
shouldShowAnchor,
|
||||
});
|
||||
}
|
||||
}
|
||||
46
packages/website/ts/pages/shared/version_drop_down.tsx
Normal file
46
packages/website/ts/pages/shared/version_drop_down.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import MenuItem from 'material-ui/MenuItem';
|
||||
import DropDownMenu from 'material-ui/DropDownMenu';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {Docs} from 'ts/types';
|
||||
|
||||
interface VersionDropDownProps {
|
||||
selectedVersion: string;
|
||||
versions: string[];
|
||||
doc: Docs;
|
||||
}
|
||||
|
||||
interface VersionDropDownState {}
|
||||
|
||||
export class VersionDropDown extends React.Component<VersionDropDownProps, VersionDropDownState> {
|
||||
public render() {
|
||||
return (
|
||||
<div className="mx-auto" style={{width: 120}}>
|
||||
<DropDownMenu
|
||||
maxHeight={300}
|
||||
value={this.props.selectedVersion}
|
||||
onChange={this.updateSelectedVersion.bind(this)}
|
||||
>
|
||||
{this.renderDropDownItems()}
|
||||
</DropDownMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderDropDownItems() {
|
||||
const items = _.map(this.props.versions, version => {
|
||||
return (
|
||||
<MenuItem
|
||||
key={version}
|
||||
value={version}
|
||||
primaryText={`v${version}`}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return items;
|
||||
}
|
||||
private updateSelectedVersion(e: any, index: number, value: string) {
|
||||
const docPath = constants.docToPath[this.props.doc];
|
||||
window.location.href = `${docPath}/${value}${window.location.hash}`;
|
||||
}
|
||||
}
|
||||
210
packages/website/ts/pages/wiki/wiki.tsx
Normal file
210
packages/website/ts/pages/wiki/wiki.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import DocumentTitle = require('react-document-title');
|
||||
import {colors} from 'material-ui/styles';
|
||||
import CircularProgress from 'material-ui/CircularProgress';
|
||||
import {
|
||||
scroller,
|
||||
} from 'react-scroll';
|
||||
import {Styles, Article, ArticlesBySection} from 'ts/types';
|
||||
import {TopBar} from 'ts/components/top_bar';
|
||||
import {HeaderSizes, WebsitePaths} from 'ts/types';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {constants} from 'ts/utils/constants';
|
||||
import {configs} from 'ts/utils/configs';
|
||||
import {NestedSidebarMenu} from 'ts/pages/shared/nested_sidebar_menu';
|
||||
import {SectionHeader} from 'ts/pages/shared/section_header';
|
||||
import {MarkdownSection} from 'ts/pages/shared/markdown_section';
|
||||
|
||||
const WIKI_NOT_READY_BACKOUT_TIMEOUT_MS = 5000;
|
||||
|
||||
export interface WikiProps {
|
||||
source: string;
|
||||
location: Location;
|
||||
}
|
||||
|
||||
interface WikiState {
|
||||
articlesBySection: ArticlesBySection;
|
||||
}
|
||||
|
||||
const styles: Styles = {
|
||||
mainContainers: {
|
||||
position: 'absolute',
|
||||
top: 60,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
overflowZ: 'hidden',
|
||||
overflowY: 'scroll',
|
||||
minHeight: 'calc(100vh - 60px)',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
},
|
||||
menuContainer: {
|
||||
borderColor: colors.grey300,
|
||||
maxWidth: 330,
|
||||
marginLeft: 20,
|
||||
},
|
||||
};
|
||||
|
||||
export class Wiki extends React.Component<WikiProps, WikiState> {
|
||||
private wikiBackoffTimeoutId: number;
|
||||
constructor(props: WikiProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
articlesBySection: undefined,
|
||||
};
|
||||
}
|
||||
public componentWillMount() {
|
||||
this.fetchArticlesBySectionAsync();
|
||||
}
|
||||
public componentWillUnmount() {
|
||||
clearTimeout(this.wikiBackoffTimeoutId);
|
||||
}
|
||||
public render() {
|
||||
const menuSubsectionsBySection = _.isUndefined(this.state.articlesBySection)
|
||||
? {}
|
||||
: this.getMenuSubsectionsBySection(this.state.articlesBySection);
|
||||
return (
|
||||
<div>
|
||||
<DocumentTitle title="0x Protocol Wiki"/>
|
||||
<TopBar
|
||||
blockchainIsLoaded={false}
|
||||
location={this.props.location}
|
||||
menuSubsectionsBySection={menuSubsectionsBySection}
|
||||
shouldFullWidth={true}
|
||||
/>
|
||||
{_.isUndefined(this.state.articlesBySection) ?
|
||||
<div
|
||||
className="col col-12"
|
||||
style={styles.mainContainers}
|
||||
>
|
||||
<div
|
||||
className="relative sm-px2 sm-pt2 sm-m1"
|
||||
style={{height: 122, top: '50%', transform: 'translateY(-50%)'}}
|
||||
>
|
||||
<div className="center pb2">
|
||||
<CircularProgress size={40} thickness={5} />
|
||||
</div>
|
||||
<div className="center pt2" style={{paddingBottom: 11}}>Loading wiki...</div>
|
||||
</div>
|
||||
</div> :
|
||||
<div
|
||||
className="mx-auto flex"
|
||||
style={{color: colors.grey800, height: 43}}
|
||||
>
|
||||
<div className="relative col md-col-3 lg-col-3 lg-pl0 md-pl1 sm-hide xs-hide">
|
||||
<div
|
||||
className="border-right absolute pt2"
|
||||
style={{...styles.menuContainer, ...styles.mainContainers}}
|
||||
>
|
||||
<NestedSidebarMenu
|
||||
topLevelMenu={menuSubsectionsBySection}
|
||||
menuSubsectionsBySection={menuSubsectionsBySection}
|
||||
isSectionHeaderClickable={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative col lg-col-9 md-col-9 sm-col-12 col-12">
|
||||
<div
|
||||
id="documentation"
|
||||
style={styles.mainContainers}
|
||||
className="absolute"
|
||||
>
|
||||
<div id="0xProtocolWiki" />
|
||||
<h1 className="md-pl2 sm-pl3">
|
||||
<a href={constants.GITHUB_WIKI_URL} target="_blank">
|
||||
0x Protocol Wiki
|
||||
</a>
|
||||
</h1>
|
||||
<div id="wiki">
|
||||
{this.renderWikiArticles()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private renderWikiArticles(): React.ReactNode {
|
||||
const sectionNames = _.keys(this.state.articlesBySection);
|
||||
const sections = _.map(sectionNames, sectionName => this.renderSection(sectionName));
|
||||
return sections;
|
||||
}
|
||||
private renderSection(sectionName: string) {
|
||||
const articles = this.state.articlesBySection[sectionName];
|
||||
const renderedArticles = _.map(articles, (article: Article) => {
|
||||
const githubLink = `${constants.GITHUB_WIKI_URL}/edit/master/${sectionName}/${article.fileName}`;
|
||||
return (
|
||||
<div key={`markdown-section-${article.title}`}>
|
||||
<MarkdownSection
|
||||
sectionName={article.title}
|
||||
markdownContent={article.content}
|
||||
headerSize={HeaderSizes.H2}
|
||||
githubLink={githubLink}
|
||||
/>
|
||||
<div className="mb4 mt3 p3 center" style={{backgroundColor: '#f9f5ef'}}>
|
||||
See a way to make this article better?{' '}
|
||||
<a
|
||||
href={githubLink}
|
||||
target="_blank"
|
||||
>
|
||||
Edit here →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div
|
||||
key={`section-${sectionName}`}
|
||||
className="py2 pr3 md-pl2 sm-pl3"
|
||||
>
|
||||
<SectionHeader sectionName={sectionName} headerSize={HeaderSizes.H1} />
|
||||
{renderedArticles}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private scrollToHash(): void {
|
||||
const hashWithPrefix = this.props.location.hash;
|
||||
let hash = hashWithPrefix.slice(1);
|
||||
if (_.isEmpty(hash)) {
|
||||
hash = '0xProtocolWiki'; // scroll to the top
|
||||
}
|
||||
|
||||
scroller.scrollTo(hash, {duration: 0, offset: 0, containerId: 'documentation'});
|
||||
}
|
||||
private async fetchArticlesBySectionAsync(): Promise<void> {
|
||||
const endpoint = `${configs.BACKEND_BASE_URL}${WebsitePaths.Wiki}`;
|
||||
const response = await fetch(endpoint);
|
||||
if (response.status === constants.HTTP_NO_CONTENT_STATUS_CODE) {
|
||||
// We need to backoff and try fetching again later
|
||||
this.wikiBackoffTimeoutId = window.setTimeout(() => {
|
||||
this.fetchArticlesBySectionAsync();
|
||||
}, WIKI_NOT_READY_BACKOUT_TIMEOUT_MS);
|
||||
return;
|
||||
}
|
||||
if (response.status !== 200) {
|
||||
// TODO: Show the user an error message when the wiki fail to load
|
||||
const errMsg = await response.text();
|
||||
utils.consoleLog(`Failed to load wiki: ${response.status} ${errMsg}`);
|
||||
return;
|
||||
}
|
||||
const articlesBySection = await response.json();
|
||||
this.setState({
|
||||
articlesBySection,
|
||||
}, () => {
|
||||
this.scrollToHash();
|
||||
});
|
||||
}
|
||||
private getMenuSubsectionsBySection(articlesBySection: ArticlesBySection) {
|
||||
const sectionNames = _.keys(articlesBySection);
|
||||
const menuSubsectionsBySection: {[section: string]: string[]} = {};
|
||||
for (const sectionName of sectionNames) {
|
||||
const articles = articlesBySection[sectionName];
|
||||
const articleNames = _.map(articles, article => article.title);
|
||||
menuSubsectionsBySection[sectionName] = articleNames;
|
||||
}
|
||||
return menuSubsectionsBySection;
|
||||
}
|
||||
}
|
||||
244
packages/website/ts/redux/dispatcher.ts
Normal file
244
packages/website/ts/redux/dispatcher.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import {Dispatch} from 'redux';
|
||||
import {State} from 'ts/redux/reducer';
|
||||
import {
|
||||
Direction,
|
||||
Side,
|
||||
AssetToken,
|
||||
BlockchainErrs,
|
||||
Token,
|
||||
SignatureData,
|
||||
Fill,
|
||||
Order,
|
||||
ActionTypes,
|
||||
ScreenWidths,
|
||||
ProviderType,
|
||||
TokenStateByAddress,
|
||||
} from 'ts/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
export class Dispatcher {
|
||||
private dispatch: Dispatch<State>;
|
||||
constructor(dispatch: Dispatch<State>) {
|
||||
this.dispatch = dispatch;
|
||||
}
|
||||
// Portal
|
||||
public resetState() {
|
||||
this.dispatch({
|
||||
type: ActionTypes.RESET_STATE,
|
||||
});
|
||||
}
|
||||
public updateNodeVersion(nodeVersion: string) {
|
||||
this.dispatch({
|
||||
data: nodeVersion,
|
||||
type: ActionTypes.UPDATE_NODE_VERSION,
|
||||
});
|
||||
}
|
||||
public updateScreenWidth(screenWidth: ScreenWidths) {
|
||||
this.dispatch({
|
||||
data: screenWidth,
|
||||
type: ActionTypes.UPDATE_SCREEN_WIDTH,
|
||||
});
|
||||
}
|
||||
public swapAssetTokenSymbols() {
|
||||
this.dispatch({
|
||||
type: ActionTypes.SWAP_ASSET_TOKENS,
|
||||
});
|
||||
}
|
||||
public updateGenerateOrderStep(direction: Direction) {
|
||||
this.dispatch({
|
||||
data: direction,
|
||||
type: ActionTypes.UPDATE_GENERATE_ORDER_STEP,
|
||||
});
|
||||
}
|
||||
public updateOrderSalt(salt: BigNumber) {
|
||||
this.dispatch({
|
||||
data: salt,
|
||||
type: ActionTypes.UPDATE_ORDER_SALT,
|
||||
});
|
||||
}
|
||||
public updateUserSuppliedOrderCache(order: Order) {
|
||||
this.dispatch({
|
||||
data: order,
|
||||
type: ActionTypes.UPDATE_USER_SUPPLIED_ORDER_CACHE,
|
||||
});
|
||||
}
|
||||
public updateShouldBlockchainErrDialogBeOpen(shouldBeOpen: boolean) {
|
||||
this.dispatch({
|
||||
data: shouldBeOpen,
|
||||
type: ActionTypes.UPDATE_SHOULD_BLOCKCHAIN_ERR_DIALOG_BE_OPEN,
|
||||
});
|
||||
}
|
||||
public updateChosenAssetToken(side: Side, token: AssetToken) {
|
||||
this.dispatch({
|
||||
data: {
|
||||
side,
|
||||
token,
|
||||
},
|
||||
type: ActionTypes.UPDATE_CHOSEN_ASSET_TOKEN,
|
||||
});
|
||||
}
|
||||
public updateChosenAssetTokenAddress(side: Side, address: string) {
|
||||
this.dispatch({
|
||||
data: {
|
||||
address,
|
||||
side,
|
||||
},
|
||||
type: ActionTypes.UPDATE_CHOSEN_ASSET_TOKEN_ADDRESS,
|
||||
});
|
||||
}
|
||||
public updateOrderTakerAddress(address: string) {
|
||||
this.dispatch({
|
||||
data: address,
|
||||
type: ActionTypes.UPDATE_ORDER_TAKER_ADDRESS,
|
||||
});
|
||||
}
|
||||
public updateUserAddress(address: string) {
|
||||
this.dispatch({
|
||||
data: address,
|
||||
type: ActionTypes.UPDATE_USER_ADDRESS,
|
||||
});
|
||||
}
|
||||
public updateOrderExpiry(unixTimestampSec: BigNumber) {
|
||||
this.dispatch({
|
||||
data: unixTimestampSec,
|
||||
type: ActionTypes.UPDATE_ORDER_EXPIRY,
|
||||
});
|
||||
}
|
||||
public encounteredBlockchainError(err: BlockchainErrs) {
|
||||
this.dispatch({
|
||||
data: err,
|
||||
type: ActionTypes.BLOCKCHAIN_ERR_ENCOUNTERED,
|
||||
});
|
||||
}
|
||||
public updateBlockchainIsLoaded(isLoaded: boolean) {
|
||||
this.dispatch({
|
||||
data: isLoaded,
|
||||
type: ActionTypes.UPDATE_BLOCKCHAIN_IS_LOADED,
|
||||
});
|
||||
}
|
||||
public addTokenToTokenByAddress(token: Token) {
|
||||
this.dispatch({
|
||||
data: token,
|
||||
type: ActionTypes.ADD_TOKEN_TO_TOKEN_BY_ADDRESS,
|
||||
});
|
||||
}
|
||||
public removeTokenToTokenByAddress(token: Token) {
|
||||
this.dispatch({
|
||||
data: token,
|
||||
type: ActionTypes.REMOVE_TOKEN_TO_TOKEN_BY_ADDRESS,
|
||||
});
|
||||
}
|
||||
public clearTokenByAddress() {
|
||||
this.dispatch({
|
||||
type: ActionTypes.CLEAR_TOKEN_BY_ADDRESS,
|
||||
});
|
||||
}
|
||||
public updateTokenByAddress(tokens: Token[]) {
|
||||
this.dispatch({
|
||||
data: tokens,
|
||||
type: ActionTypes.UPDATE_TOKEN_BY_ADDRESS,
|
||||
});
|
||||
}
|
||||
public updateTokenStateByAddress(tokenStateByAddress: TokenStateByAddress) {
|
||||
this.dispatch({
|
||||
data: tokenStateByAddress,
|
||||
type: ActionTypes.UPDATE_TOKEN_STATE_BY_ADDRESS,
|
||||
});
|
||||
}
|
||||
public removeFromTokenStateByAddress(tokenAddress: string) {
|
||||
this.dispatch({
|
||||
data: tokenAddress,
|
||||
type: ActionTypes.REMOVE_FROM_TOKEN_STATE_BY_ADDRESS,
|
||||
});
|
||||
}
|
||||
public replaceTokenAllowanceByAddress(address: string, allowance: BigNumber) {
|
||||
this.dispatch({
|
||||
data: {
|
||||
address,
|
||||
allowance,
|
||||
},
|
||||
type: ActionTypes.REPLACE_TOKEN_ALLOWANCE_BY_ADDRESS,
|
||||
});
|
||||
}
|
||||
public replaceTokenBalanceByAddress(address: string, balance: BigNumber) {
|
||||
this.dispatch({
|
||||
data: {
|
||||
address,
|
||||
balance,
|
||||
},
|
||||
type: ActionTypes.REPLACE_TOKEN_BALANCE_BY_ADDRESS,
|
||||
});
|
||||
}
|
||||
public updateTokenBalanceByAddress(address: string, balanceDelta: BigNumber) {
|
||||
this.dispatch({
|
||||
data: {
|
||||
address,
|
||||
balanceDelta,
|
||||
},
|
||||
type: ActionTypes.UPDATE_TOKEN_BALANCE_BY_ADDRESS,
|
||||
});
|
||||
}
|
||||
public updateSignatureData(signatureData: SignatureData) {
|
||||
this.dispatch({
|
||||
data: signatureData,
|
||||
type: ActionTypes.UPDATE_ORDER_SIGNATURE_DATA,
|
||||
});
|
||||
}
|
||||
public updateUserEtherBalance(balance: BigNumber) {
|
||||
this.dispatch({
|
||||
data: balance,
|
||||
type: ActionTypes.UPDATE_USER_ETHER_BALANCE,
|
||||
});
|
||||
}
|
||||
public updateNetworkId(networkId: number) {
|
||||
this.dispatch({
|
||||
data: networkId,
|
||||
type: ActionTypes.UPDATE_NETWORK_ID,
|
||||
});
|
||||
}
|
||||
public updateOrderFillAmount(amount: BigNumber) {
|
||||
this.dispatch({
|
||||
data: amount,
|
||||
type: ActionTypes.UPDATE_ORDER_FILL_AMOUNT,
|
||||
});
|
||||
}
|
||||
|
||||
// Docs
|
||||
public updateCurrentDocsVersion(version: string) {
|
||||
this.dispatch({
|
||||
data: version,
|
||||
type: ActionTypes.UPDATE_LIBRARY_VERSION,
|
||||
});
|
||||
}
|
||||
public updateAvailableDocVersions(versions: string[]) {
|
||||
this.dispatch({
|
||||
data: versions,
|
||||
type: ActionTypes.UPDATE_AVAILABLE_LIBRARY_VERSIONS,
|
||||
});
|
||||
}
|
||||
|
||||
// Shared
|
||||
public showFlashMessage(msg: string|React.ReactNode) {
|
||||
this.dispatch({
|
||||
data: msg,
|
||||
type: ActionTypes.SHOW_FLASH_MESSAGE,
|
||||
});
|
||||
}
|
||||
public hideFlashMessage() {
|
||||
this.dispatch({
|
||||
type: ActionTypes.HIDE_FLASH_MESSAGE,
|
||||
});
|
||||
}
|
||||
public updateProviderType(providerType: ProviderType) {
|
||||
this.dispatch({
|
||||
type: ActionTypes.UPDATE_PROVIDER_TYPE,
|
||||
data: providerType,
|
||||
});
|
||||
}
|
||||
public updateInjectedProviderName(injectedProviderName: string) {
|
||||
this.dispatch({
|
||||
type: ActionTypes.UPDATE_INJECTED_PROVIDER_NAME,
|
||||
data: injectedProviderName,
|
||||
});
|
||||
}
|
||||
}
|
||||
363
packages/website/ts/redux/reducer.ts
Normal file
363
packages/website/ts/redux/reducer.ts
Normal file
@@ -0,0 +1,363 @@
|
||||
import * as _ from 'lodash';
|
||||
import {ZeroEx} from '0x.js';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {utils} from 'ts/utils/utils';
|
||||
import {
|
||||
GenerateOrderSteps,
|
||||
Side,
|
||||
SideToAssetToken,
|
||||
Direction,
|
||||
BlockchainErrs,
|
||||
SignatureData,
|
||||
TokenByAddress,
|
||||
TokenStateByAddress,
|
||||
Order,
|
||||
Action,
|
||||
ActionTypes,
|
||||
ScreenWidths,
|
||||
ProviderType,
|
||||
TokenState,
|
||||
} from 'ts/types';
|
||||
|
||||
// Instead of defaulting the docs version to an empty string, we pre-populate it with
|
||||
// a valid version value. This does not need to be updated however, since onLoad, it
|
||||
// is always replaced with a value retrieved from our S3 bucket.
|
||||
const DEFAULT_DOCS_VERSION = '0.0.0';
|
||||
|
||||
export interface State {
|
||||
// Portal
|
||||
blockchainErr: BlockchainErrs;
|
||||
blockchainIsLoaded: boolean;
|
||||
generateOrderStep: GenerateOrderSteps;
|
||||
networkId: number;
|
||||
orderExpiryTimestamp: BigNumber;
|
||||
orderFillAmount: BigNumber;
|
||||
orderTakerAddress: string;
|
||||
orderSignatureData: SignatureData;
|
||||
orderSalt: BigNumber;
|
||||
nodeVersion: string;
|
||||
screenWidth: ScreenWidths;
|
||||
shouldBlockchainErrDialogBeOpen: boolean;
|
||||
sideToAssetToken: SideToAssetToken;
|
||||
tokenByAddress: TokenByAddress;
|
||||
tokenStateByAddress: TokenStateByAddress;
|
||||
userAddress: string;
|
||||
userEtherBalance: BigNumber;
|
||||
// Note: cache of supplied orderJSON in fill order step. Do not use for anything else.
|
||||
userSuppliedOrderCache: Order;
|
||||
|
||||
// Docs
|
||||
docsVersion: string;
|
||||
availableDocVersions: string[];
|
||||
|
||||
// Shared
|
||||
flashMessage: string|React.ReactNode;
|
||||
providerType: ProviderType;
|
||||
injectedProviderName: string;
|
||||
};
|
||||
|
||||
const INITIAL_STATE: State = {
|
||||
// Portal
|
||||
blockchainErr: '',
|
||||
blockchainIsLoaded: false,
|
||||
generateOrderStep: GenerateOrderSteps.ChooseAssets,
|
||||
networkId: undefined,
|
||||
orderExpiryTimestamp: utils.initialOrderExpiryUnixTimestampSec(),
|
||||
orderFillAmount: undefined,
|
||||
orderSignatureData: {
|
||||
hash: '',
|
||||
r: '',
|
||||
s: '',
|
||||
v: 27,
|
||||
},
|
||||
orderTakerAddress: '',
|
||||
orderSalt: ZeroEx.generatePseudoRandomSalt(),
|
||||
nodeVersion: undefined,
|
||||
screenWidth: utils.getScreenWidth(),
|
||||
shouldBlockchainErrDialogBeOpen: false,
|
||||
sideToAssetToken: {
|
||||
[Side.deposit]: {},
|
||||
[Side.receive]: {},
|
||||
},
|
||||
tokenByAddress: {},
|
||||
tokenStateByAddress: {},
|
||||
userAddress: '',
|
||||
userEtherBalance: new BigNumber(0),
|
||||
userSuppliedOrderCache: undefined,
|
||||
|
||||
// Docs
|
||||
docsVersion: DEFAULT_DOCS_VERSION,
|
||||
availableDocVersions: [DEFAULT_DOCS_VERSION],
|
||||
|
||||
// Shared
|
||||
flashMessage: undefined,
|
||||
providerType: ProviderType.INJECTED,
|
||||
injectedProviderName: '',
|
||||
};
|
||||
|
||||
export function reducer(state: State = INITIAL_STATE, action: Action) {
|
||||
switch (action.type) {
|
||||
// Portal
|
||||
case ActionTypes.RESET_STATE:
|
||||
return INITIAL_STATE;
|
||||
|
||||
case ActionTypes.UPDATE_ORDER_SALT: {
|
||||
return _.assign({}, state, {
|
||||
orderSalt: action.data,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_NODE_VERSION: {
|
||||
return _.assign({}, state, {
|
||||
nodeVersion: action.data,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_ORDER_FILL_AMOUNT: {
|
||||
return _.assign({}, state, {
|
||||
orderFillAmount: action.data,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_SHOULD_BLOCKCHAIN_ERR_DIALOG_BE_OPEN: {
|
||||
return _.assign({}, state, {
|
||||
shouldBlockchainErrDialogBeOpen: action.data,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_USER_ETHER_BALANCE: {
|
||||
return _.assign({}, state, {
|
||||
userEtherBalance: action.data,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_USER_SUPPLIED_ORDER_CACHE: {
|
||||
return _.assign({}, state, {
|
||||
userSuppliedOrderCache: action.data,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.CLEAR_TOKEN_BY_ADDRESS: {
|
||||
return _.assign({}, state, {
|
||||
tokenByAddress: {},
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.ADD_TOKEN_TO_TOKEN_BY_ADDRESS: {
|
||||
const newTokenByAddress = state.tokenByAddress;
|
||||
newTokenByAddress[action.data.address] = action.data;
|
||||
return _.assign({}, state, {
|
||||
tokenByAddress: newTokenByAddress,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.REMOVE_TOKEN_TO_TOKEN_BY_ADDRESS: {
|
||||
const newTokenByAddress = state.tokenByAddress;
|
||||
delete newTokenByAddress[action.data.address];
|
||||
return _.assign({}, state, {
|
||||
tokenByAddress: newTokenByAddress,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_TOKEN_BY_ADDRESS: {
|
||||
const tokenByAddress = state.tokenByAddress;
|
||||
const tokens = action.data;
|
||||
_.each(tokens, token => {
|
||||
const updatedToken = _.assign({}, tokenByAddress[token.address], token);
|
||||
tokenByAddress[token.address] = updatedToken;
|
||||
});
|
||||
return _.assign({}, state, {
|
||||
tokenByAddress,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_TOKEN_STATE_BY_ADDRESS: {
|
||||
const tokenStateByAddress = state.tokenStateByAddress;
|
||||
const updatedTokenStateByAddress = action.data;
|
||||
_.each(updatedTokenStateByAddress, (tokenState: TokenState, address: string) => {
|
||||
const updatedTokenState = _.assign({}, tokenStateByAddress[address], tokenState);
|
||||
tokenStateByAddress[address] = updatedTokenState;
|
||||
});
|
||||
return _.assign({}, state, {
|
||||
tokenStateByAddress,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.REMOVE_FROM_TOKEN_STATE_BY_ADDRESS: {
|
||||
const tokenStateByAddress = state.tokenStateByAddress;
|
||||
const tokenAddress = action.data;
|
||||
delete tokenStateByAddress[tokenAddress];
|
||||
return _.assign({}, state, {
|
||||
tokenStateByAddress,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.REPLACE_TOKEN_ALLOWANCE_BY_ADDRESS: {
|
||||
const tokenStateByAddress = state.tokenStateByAddress;
|
||||
const allowance = action.data.allowance;
|
||||
const tokenAddress = action.data.address;
|
||||
tokenStateByAddress[tokenAddress] = _.assign({}, tokenStateByAddress[tokenAddress], {
|
||||
allowance,
|
||||
});
|
||||
return _.assign({}, state, {
|
||||
tokenStateByAddress,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.REPLACE_TOKEN_BALANCE_BY_ADDRESS: {
|
||||
const tokenStateByAddress = state.tokenStateByAddress;
|
||||
const balance = action.data.balance;
|
||||
const tokenAddress = action.data.address;
|
||||
tokenStateByAddress[tokenAddress] = _.assign({}, tokenStateByAddress[tokenAddress], {
|
||||
balance,
|
||||
});
|
||||
return _.assign({}, state, {
|
||||
tokenStateByAddress,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_TOKEN_BALANCE_BY_ADDRESS: {
|
||||
const tokenStateByAddress = state.tokenStateByAddress;
|
||||
const balanceDelta = action.data.balanceDelta;
|
||||
const tokenAddress = action.data.address;
|
||||
const currBalance = tokenStateByAddress[tokenAddress].balance;
|
||||
tokenStateByAddress[tokenAddress] = _.assign({}, tokenStateByAddress[tokenAddress], {
|
||||
balance: currBalance.plus(balanceDelta),
|
||||
});
|
||||
return _.assign({}, state, {
|
||||
tokenStateByAddress,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_ORDER_SIGNATURE_DATA: {
|
||||
return _.assign({}, state, {
|
||||
orderSignatureData: action.data,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_SCREEN_WIDTH: {
|
||||
return _.assign({}, state, {
|
||||
screenWidth: action.data,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_BLOCKCHAIN_IS_LOADED: {
|
||||
return _.assign({}, state, {
|
||||
blockchainIsLoaded: action.data,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.BLOCKCHAIN_ERR_ENCOUNTERED: {
|
||||
return _.assign({}, state, {
|
||||
blockchainErr: action.data,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_NETWORK_ID: {
|
||||
return _.assign({}, state, {
|
||||
networkId: action.data,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_GENERATE_ORDER_STEP: {
|
||||
const direction = action.data;
|
||||
let nextGenerateOrderStep = state.generateOrderStep;
|
||||
if (direction === Direction.forward) {
|
||||
nextGenerateOrderStep += 1;
|
||||
} else if (state.generateOrderStep !== 0) {
|
||||
nextGenerateOrderStep -= 1;
|
||||
}
|
||||
return _.assign({}, state, {
|
||||
generateOrderStep: nextGenerateOrderStep,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_CHOSEN_ASSET_TOKEN: {
|
||||
const newSideToAssetToken = _.assign({}, state.sideToAssetToken, {
|
||||
[action.data.side]: action.data.token,
|
||||
});
|
||||
return _.assign({}, state, {
|
||||
sideToAssetToken: newSideToAssetToken,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_CHOSEN_ASSET_TOKEN_ADDRESS: {
|
||||
const newAssetToken = state.sideToAssetToken[action.data.side];
|
||||
newAssetToken.address = action.data.address;
|
||||
const newSideToAssetToken = _.assign({}, state.sideToAssetToken, {
|
||||
[action.data.side]: newAssetToken,
|
||||
});
|
||||
return _.assign({}, state, {
|
||||
sideToAssetToken: newSideToAssetToken,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.SWAP_ASSET_TOKENS: {
|
||||
const newSideToAssetToken = _.assign({}, state.sideToAssetToken, {
|
||||
[Side.deposit]: state.sideToAssetToken[Side.receive],
|
||||
[Side.receive]: state.sideToAssetToken[Side.deposit],
|
||||
});
|
||||
return _.assign({}, state, {
|
||||
sideToAssetToken: newSideToAssetToken,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_ORDER_EXPIRY: {
|
||||
return _.assign({}, state, {
|
||||
orderExpiryTimestamp: action.data,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_ORDER_TAKER_ADDRESS: {
|
||||
return _.assign({}, state, {
|
||||
orderTakerAddress: action.data,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_USER_ADDRESS: {
|
||||
return _.assign({}, state, {
|
||||
userAddress: action.data,
|
||||
});
|
||||
}
|
||||
|
||||
// Docs
|
||||
case ActionTypes.UPDATE_LIBRARY_VERSION: {
|
||||
return _.assign({}, state, {
|
||||
docsVersion: action.data,
|
||||
});
|
||||
}
|
||||
case ActionTypes.UPDATE_AVAILABLE_LIBRARY_VERSIONS: {
|
||||
return _.assign({}, state, {
|
||||
availableDocVersions: action.data,
|
||||
});
|
||||
}
|
||||
|
||||
// Shared
|
||||
case ActionTypes.SHOW_FLASH_MESSAGE: {
|
||||
return _.assign({}, state, {
|
||||
flashMessage: action.data,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.HIDE_FLASH_MESSAGE: {
|
||||
return _.assign({}, state, {
|
||||
flashMessage: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_PROVIDER_TYPE: {
|
||||
return _.assign({}, state, {
|
||||
providerType: action.data,
|
||||
});
|
||||
}
|
||||
|
||||
case ActionTypes.UPDATE_INJECTED_PROVIDER_NAME: {
|
||||
return _.assign({}, state, {
|
||||
injectedProviderName: action.data,
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
24
packages/website/ts/schemas/order_schema.ts
Normal file
24
packages/website/ts/schemas/order_schema.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export const orderSchema = {
|
||||
id: '/Order',
|
||||
properties: {
|
||||
maker: {$ref: '/OrderTaker'},
|
||||
taker: {$ref: '/OrderTaker'},
|
||||
salt: {type: 'string'},
|
||||
signature: {$ref: '/SignatureData'},
|
||||
expiration: {type: 'string'},
|
||||
feeRecipient: {type: 'string'},
|
||||
exchangeContract: {type: 'string'},
|
||||
networkId: {type: 'number'},
|
||||
},
|
||||
required: [
|
||||
'maker',
|
||||
'taker',
|
||||
'salt',
|
||||
'signature',
|
||||
'expiration',
|
||||
'feeRecipient',
|
||||
'exchangeContract',
|
||||
'networkId',
|
||||
],
|
||||
type: 'object',
|
||||
};
|
||||
11
packages/website/ts/schemas/order_taker_schema.ts
Normal file
11
packages/website/ts/schemas/order_taker_schema.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const orderTakerSchema = {
|
||||
id: '/OrderTaker',
|
||||
properties: {
|
||||
address: {type: 'string'},
|
||||
token: {$ref: '/Token'},
|
||||
amount: {type: 'string'},
|
||||
feeAmount: {type: 'string'},
|
||||
},
|
||||
required: ['address', 'token', 'amount', 'feeAmount'],
|
||||
type: 'object',
|
||||
};
|
||||
11
packages/website/ts/schemas/signature_data_schema.ts
Normal file
11
packages/website/ts/schemas/signature_data_schema.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const signatureDataSchema = {
|
||||
id: '/SignatureData',
|
||||
properties: {
|
||||
hash: {type: 'string'},
|
||||
r: {type: 'string'},
|
||||
s: {type: 'string'},
|
||||
v: {type: 'number'},
|
||||
},
|
||||
required: ['hash', 'r', 's', 'v'],
|
||||
type: 'object',
|
||||
};
|
||||
11
packages/website/ts/schemas/token_schema.ts
Normal file
11
packages/website/ts/schemas/token_schema.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const tokenSchema = {
|
||||
id: '/Token',
|
||||
properties: {
|
||||
name: {type: 'string'},
|
||||
symbol: {type: 'string'},
|
||||
decimals: {type: 'number'},
|
||||
address: {type: 'string'},
|
||||
},
|
||||
required: ['name', 'symbol', 'decimals', 'address'],
|
||||
type: 'object',
|
||||
};
|
||||
19
packages/website/ts/schemas/validator.ts
Normal file
19
packages/website/ts/schemas/validator.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import {Validator, Schema as JSONSchema} from 'jsonschema';
|
||||
import {signatureDataSchema} from 'ts/schemas/signature_data_schema';
|
||||
import {orderSchema} from 'ts/schemas/order_schema';
|
||||
import {tokenSchema} from 'ts/schemas/token_schema';
|
||||
import {orderTakerSchema} from 'ts/schemas/order_taker_schema';
|
||||
|
||||
export class SchemaValidator {
|
||||
private validator: Validator;
|
||||
constructor() {
|
||||
this.validator = new Validator();
|
||||
this.validator.addSchema(signatureDataSchema as JSONSchema, signatureDataSchema.id);
|
||||
this.validator.addSchema(tokenSchema as JSONSchema, tokenSchema.id);
|
||||
this.validator.addSchema(orderTakerSchema as JSONSchema, orderTakerSchema.id);
|
||||
this.validator.addSchema(orderSchema as JSONSchema, orderSchema.id);
|
||||
}
|
||||
public validate(instance: object, schema: Schema) {
|
||||
return this.validator.validate(instance, schema);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user