Add website to mono repo, update packages to align with existing sub-packages, use new subscribeAsync 0x.js method

This commit is contained in:
Fabio Berger
2017-11-21 14:03:08 -06:00
parent 037f466e1f
commit 3660ba28d7
292 changed files with 26118 additions and 100 deletions

View 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;
});
}
}

View 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>
);
}
}

View File

@@ -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();
}
}

View 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;
}
}

View File

@@ -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>
);
}

View 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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>
);
}

View 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,
});
}
}

View 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: [],
});
}
}

View 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>
);
}
}

View 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>
);
}

View File

@@ -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>
);
}
}

View File

@@ -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>
);
}
}
}

View 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>
);
}
}
}

View 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);
}
}

View File

@@ -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);
}
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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>
);
}
}
}

View 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);
}
}

View 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);
}
}

View 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;
}
}

View File

@@ -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);
}
}

View 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>
);
}
}
}

View 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,
});
}
}

View 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;
}
}

View 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);
}
}

View 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>
);
}
}

View 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,
});
}
}

View 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,
});
}
}

View 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();
}
}

View 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>
);
}
}

View 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>
);
}
}

View 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;
}
}

View File

@@ -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>
);
}
}

View 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>
);
}

View 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,
});
}
}

View 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));
}
}

View 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,
});
}
}

View 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>
);
};

View 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"
/>
);
}

View 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>
);
}

View 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();
}
}

View 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>
);
};

View 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>
);
}
}

View 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>
);
};

View 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,
});
}
}
}

View 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,
});
}
}
}

View 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>
);
}
}

View 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,
});
}
}

View 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>
);
}
}

View 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>
);
};

View 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>
);
};

View 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,
});
}
}

View 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>
);
}
}

View 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>
);
}
}

View 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);

View 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);

View File

@@ -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);

View 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
View 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;
}};
}

View 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'),
);

View 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}
/>
);
};
};

View 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);
},
};

View 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);
},
};

View 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');
},
};

View 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>
);
});
}
}

View 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>
);
}

View 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>
);
};

View 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>
);
}

View 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>
);
}

View 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,
});
}
}

View 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>
);
}

View 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,
});
}
}

View 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;
}

View File

@@ -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();
});
}
}

View 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>
);
}

View 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>
);
}

View 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,
});
}
}

View File

@@ -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();
});
}
}

View 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 blockchains 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 protocols 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 protocols 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
Ethereums rapidly evolving technology stack, decentralized governance is
needed because 0x protocols 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 protocols 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 protocols 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 teams 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 industrys 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;
}
}

View 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,
});
}
}

View 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,
});
}
}
}

View 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>
);
}
}

View 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,
});
}
}

View 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>
);
}

View 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,
});
}
}

View 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();
}
}

View 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,
});
}
}

View 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}`;
}
}

View 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;
}
}

View 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,
});
}
}

View 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;
}
}

View 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',
};

View 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',
};

View 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',
};

View 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',
};

View 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