Merge pull request #351 from 0xProject/feature/portal-ledger-support

Portal Ledger Support, Lazy-loading token balances/allowances
This commit is contained in:
Fabio Berger 2018-01-30 21:27:21 +01:00 committed by GitHub
commit 1feac1a308
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 1206 additions and 647 deletions

View File

@ -36,7 +36,6 @@
"find-versions": "^2.0.0",
"is-mobile": "^0.2.2",
"jsonschema": "^1.2.0",
"ledgerco": "0xProject/ledger-node-js-api",
"less": "^2.7.2",
"lodash": "^4.17.4",
"material-ui": "^0.17.1",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 888 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

View File

@ -37,10 +37,10 @@ import {
EtherscanLinkSuffixes,
ProviderType,
Side,
SideToAssetToken,
SignatureData,
Token,
TokenByAddress,
TokenStateByAddress,
} from 'ts/types';
import { configs } from 'ts/utils/configs';
import { constants } from 'ts/utils/constants';
@ -54,6 +54,7 @@ import FilterSubprovider = require('web3-provider-engine/subproviders/filters');
import * as MintableArtifacts from '../contracts/Mintable.json';
const BLOCK_NUMBER_BACK_TRACK = 50;
const GWEI_IN_WEI = 1000000000;
export class Blockchain {
public networkId: number;
@ -64,8 +65,9 @@ export class Blockchain {
private _exchangeAddress: string;
private _userAddress: string;
private _cachedProvider: Web3.Provider;
private _cachedProviderNetworkId: number;
private _ledgerSubprovider: LedgerWalletSubprovider;
private _zrxPollIntervalId: NodeJS.Timer;
private _defaultGasPrice: BigNumber;
private static async _onPageLoadAsync(): Promise<void> {
if (document.readyState === 'complete') {
return; // Already loaded
@ -111,7 +113,7 @@ export class Blockchain {
// injected into their browser.
provider = new ProviderEngine();
provider.addProvider(new FilterSubprovider());
const networkId = configs.IS_MAINNET_ENABLED ? constants.NETWORK_ID_MAINNET : constants.NETWORK_ID_TESTNET;
const networkId = configs.IS_MAINNET_ENABLED ? constants.NETWORK_ID_MAINNET : constants.NETWORK_ID_KOVAN;
provider.addProvider(new RedundantRPCSubprovider(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId]));
provider.start();
}
@ -121,6 +123,10 @@ export class Blockchain {
constructor(dispatcher: Dispatcher, isSalePage: boolean = false) {
this._dispatcher = dispatcher;
this._userAddress = '';
const defaultGasPrice = GWEI_IN_WEI * 30;
this._defaultGasPrice = new BigNumber(defaultGasPrice);
// tslint:disable-next-line:no-floating-promises
this._updateDefaultGasPriceAsync();
// tslint:disable-next-line:no-floating-promises
this._onPageLoadInitFireAndForgetAsync();
}
@ -133,14 +139,14 @@ export class Blockchain {
} else if (this.networkId !== newNetworkId) {
this.networkId = newNetworkId;
this._dispatcher.encounteredBlockchainError(BlockchainErrs.NoError);
await this._fetchTokenInformationAsync();
await this.fetchTokenInformationAsync();
await this._rehydrateStoreWithContractEvents();
}
}
public async userAddressUpdatedFireAndForgetAsync(newUserAddress: string) {
if (this._userAddress !== newUserAddress) {
this._userAddress = newUserAddress;
await this._fetchTokenInformationAsync();
await this.fetchTokenInformationAsync();
await this._rehydrateStoreWithContractEvents();
}
}
@ -180,84 +186,96 @@ export class Blockchain {
}
this._ledgerSubprovider.setPathIndex(pathIndex);
}
public async providerTypeUpdatedFireAndForgetAsync(providerType: ProviderType) {
public async updateProviderToLedgerAsync(networkId: number) {
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();
const ledgerWalletConfigs = {
networkId: this.networkId,
ledgerEthereumClientFactoryAsync: ledgerEthereumBrowserClientFactoryAsync,
};
this._ledgerSubprovider = new LedgerSubprovider(ledgerWalletConfigs);
provider.addProvider(this._ledgerSubprovider);
provider.addProvider(new FilterSubprovider());
const networkId = configs.IS_MAINNET_ENABLED
? constants.NETWORK_ID_MAINNET
: constants.NETWORK_ID_TESTNET;
provider.addProvider(new RedundantRPCSubprovider(configs.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);
this._zeroEx.setProvider(provider, networkId);
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);
this._zeroEx.setProvider(provider, this.networkId);
await this._postInstantiationOrUpdatingProviderZeroExAsync();
delete this._ledgerSubprovider;
delete this._cachedProvider;
break;
}
default:
throw utils.spawnSwitchErr('providerType', providerType);
const isU2FSupported = await utils.isU2FSupportedAsync();
if (!isU2FSupported) {
throw new Error('Cannot update providerType to LEDGER without U2F support');
}
await this._fetchTokenInformationAsync();
// Cache injected provider so that we can switch the user back to it easily
if (_.isUndefined(this._cachedProvider)) {
this._cachedProvider = this._web3Wrapper.getProviderObj();
this._cachedProviderNetworkId = this.networkId;
}
this._web3Wrapper.destroy();
this._userAddress = '';
this._dispatcher.updateUserAddress(''); // Clear old userAddress
const provider = new ProviderEngine();
const ledgerWalletConfigs = {
networkId,
ledgerEthereumClientFactoryAsync: ledgerEthereumBrowserClientFactoryAsync,
};
this._ledgerSubprovider = new LedgerSubprovider(ledgerWalletConfigs);
provider.addProvider(this._ledgerSubprovider);
provider.addProvider(new FilterSubprovider());
provider.addProvider(new RedundantRPCSubprovider(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId]));
provider.start();
this.networkId = networkId;
this._dispatcher.updateNetworkId(this.networkId);
const shouldPollUserAddress = false;
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress);
this._zeroEx.setProvider(provider, this.networkId);
await this._postInstantiationOrUpdatingProviderZeroExAsync();
this._web3Wrapper.startEmittingNetworkConnectionAndUserBalanceState();
this._dispatcher.updateProviderType(ProviderType.Ledger);
}
public async updateProviderToInjectedAsync() {
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
if (_.isUndefined(this._cachedProvider)) {
return; // Going from injected to injected, so we noop
}
this._web3Wrapper.destroy();
const provider = this._cachedProvider;
this.networkId = this._cachedProviderNetworkId;
const shouldPollUserAddress = true;
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress);
this._userAddress = await this._web3Wrapper.getFirstAccountIfExistsAsync();
this._zeroEx.setProvider(provider, this.networkId);
await this._postInstantiationOrUpdatingProviderZeroExAsync();
await this.fetchTokenInformationAsync();
this._web3Wrapper.startEmittingNetworkConnectionAndUserBalanceState();
this._dispatcher.updateProviderType(ProviderType.Injected);
delete this._ledgerSubprovider;
delete this._cachedProvider;
}
public async setProxyAllowanceAsync(token: Token, amountInBaseUnits: BigNumber): Promise<void> {
utils.assert(this.isValidAddress(token.address), BlockchainCallErrs.TokenAddressIsInvalid);
utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses);
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
this._showFlashMessageIfLedger();
const txHash = await this._zeroEx.token.setProxyAllowanceAsync(
token.address,
this._userAddress,
amountInBaseUnits,
{
gasPrice: this._defaultGasPrice,
},
);
await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
const allowance = amountInBaseUnits;
this._dispatcher.replaceTokenAllowanceByAddress(token.address, allowance);
}
public async transferAsync(token: Token, toAddress: string, amountInBaseUnits: BigNumber): Promise<void> {
this._showFlashMessageIfLedger();
const txHash = await this._zeroEx.token.transferAsync(
token.address,
this._userAddress,
toAddress,
amountInBaseUnits,
{
gasPrice: this._defaultGasPrice,
},
);
await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
const etherScanLinkIfExists = utils.getEtherScanLinkIfExists(txHash, this.networkId, EtherscanLinkSuffixes.Tx);
@ -309,11 +327,15 @@ export class Blockchain {
const shouldThrowOnInsufficientBalanceOrAllowance = true;
this._showFlashMessageIfLedger();
const txHash = await this._zeroEx.exchange.fillOrderAsync(
signedOrder,
fillTakerTokenAmount,
shouldThrowOnInsufficientBalanceOrAllowance,
this._userAddress,
{
gasPrice: this._defaultGasPrice,
},
);
const receipt = await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
const logs: Array<LogWithDecodedArgs<ExchangeContractEventArgs>> = receipt.logs as any;
@ -324,7 +346,10 @@ export class Blockchain {
return filledTakerTokenAmount;
}
public async cancelOrderAsync(signedOrder: SignedOrder, cancelTakerTokenAmount: BigNumber): Promise<BigNumber> {
const txHash = await this._zeroEx.exchange.cancelOrderAsync(signedOrder, cancelTakerTokenAmount);
this._showFlashMessageIfLedger();
const txHash = await this._zeroEx.exchange.cancelOrderAsync(signedOrder, cancelTakerTokenAmount, {
gasPrice: this._defaultGasPrice,
});
const receipt = await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
const logs: Array<LogWithDecodedArgs<ExchangeContractEventArgs>> = receipt.logs as any;
this._zeroEx.exchange.throwLogErrorsAsErrors(logs);
@ -368,22 +393,25 @@ export class Blockchain {
const [currBalance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, token.address);
this._zrxPollIntervalId = intervalUtils.setAsyncExcludingInterval(
async () => {
const [balance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, token.address);
if (!balance.eq(currBalance)) {
this._dispatcher.replaceTokenBalanceByAddress(token.address, balance);
intervalUtils.clearAsyncExcludingInterval(this._zrxPollIntervalId);
delete this._zrxPollIntervalId;
}
},
5000,
(err: Error) => {
utils.consoleLog(`Polling tokenBalance failed: ${err}`);
intervalUtils.clearAsyncExcludingInterval(this._zrxPollIntervalId);
delete this._zrxPollIntervalId;
},
);
const newTokenBalancePromise = new Promise((resolve: (balance: BigNumber) => void, reject) => {
const tokenPollInterval = intervalUtils.setAsyncExcludingInterval(
async () => {
const [balance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, token.address);
if (!balance.eq(currBalance)) {
intervalUtils.clearAsyncExcludingInterval(tokenPollInterval);
resolve(balance);
}
},
5000,
(err: Error) => {
utils.consoleLog(`Polling tokenBalance failed: ${err}`);
intervalUtils.clearAsyncExcludingInterval(tokenPollInterval);
reject(err);
},
);
});
return newTokenBalancePromise;
}
public async signOrderHashAsync(orderHash: string): Promise<SignatureData> {
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
@ -393,7 +421,21 @@ export class Blockchain {
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);
this._showFlashMessageIfLedger();
const nodeVersion = await this._web3Wrapper.getNodeVersionAsync();
const isParityNode = utils.isParityNode(nodeVersion);
const isTestRpc = utils.isTestRpc(nodeVersion);
const isLedgerSigner = !_.isUndefined(this._ledgerSubprovider);
let shouldAddPersonalMessagePrefix = true;
if ((isParityNode && !isLedgerSigner) || isTestRpc || isLedgerSigner) {
shouldAddPersonalMessagePrefix = false;
}
const ecSignature = await this._zeroEx.signOrderHashAsync(
orderHash,
makerAddress,
shouldAddPersonalMessagePrefix,
);
const signatureData = _.extend({}, ecSignature, {
hash: orderHash,
});
@ -404,11 +446,11 @@ export class Blockchain {
utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses);
const mintableContract = await this._instantiateContractIfExistsAsync(MintableArtifacts, token.address);
this._showFlashMessageIfLedger();
await mintableContract.mint(constants.MINT_AMOUNT, {
from: this._userAddress,
gasPrice: this._defaultGasPrice,
});
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);
@ -418,14 +460,20 @@ export class Blockchain {
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses);
const txHash = await this._zeroEx.etherToken.depositAsync(etherTokenAddress, amount, this._userAddress);
this._showFlashMessageIfLedger();
const txHash = await this._zeroEx.etherToken.depositAsync(etherTokenAddress, amount, this._userAddress, {
gasPrice: this._defaultGasPrice,
});
await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
}
public async convertWrappedEthTokensToEthAsync(etherTokenAddress: string, amount: BigNumber): Promise<void> {
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses);
const txHash = await this._zeroEx.etherToken.withdrawAsync(etherTokenAddress, amount, this._userAddress);
this._showFlashMessageIfLedger();
const txHash = await this._zeroEx.etherToken.withdrawAsync(etherTokenAddress, amount, this._userAddress, {
gasPrice: this._defaultGasPrice,
});
await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
}
public async doesContractExistAtAddressAsync(address: string) {
@ -451,22 +499,6 @@ export class Blockchain {
}
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();
@ -479,10 +511,59 @@ export class Blockchain {
this._web3Wrapper.updatePrevUserAddress(newUserAddress);
}
public destroy() {
intervalUtils.clearAsyncExcludingInterval(this._zrxPollIntervalId);
this._web3Wrapper.destroy();
this._stopWatchingExchangeLogFillEvents();
}
public async fetchTokenInformationAsync() {
utils.assert(
!_.isUndefined(this.networkId),
'Cannot call fetchTokenInformationAsync if disconnected from Ethereum node',
);
this._dispatcher.updateBlockchainIsLoaded(false);
const tokenRegistryTokensByAddress = await this._getTokenRegistryTokensByAddressAsync();
const trackedTokensByAddress = trackedTokenStorage.getTrackedTokensByAddress(this._userAddress, this.networkId);
const tokenRegistryTokens = _.values(tokenRegistryTokensByAddress);
if (_.isEmpty(trackedTokensByAddress)) {
_.each(configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, symbol => {
const token = _.find(tokenRegistryTokens, t => t.symbol === symbol);
token.isTracked = true;
trackedTokensByAddress[token.address] = token;
});
_.each(trackedTokensByAddress, (token: Token, address: string) => {
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(trackedTokensByAddress, (trackedToken: Token, address: string) => {
if (!_.isUndefined(tokenRegistryTokensByAddress[address])) {
tokenRegistryTokensByAddress[address].isTracked = true;
}
});
}
const allTokensByAddress = {
...tokenRegistryTokensByAddress,
...trackedTokensByAddress,
};
const allTokens = _.values(allTokensByAddress);
const mostPopularTradingPairTokens: Token[] = [
_.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[0] }),
_.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[1] }),
];
const sideToAssetToken: SideToAssetToken = {
[Side.Deposit]: {
address: mostPopularTradingPairTokens[0].address,
},
[Side.Receive]: {
address: mostPopularTradingPairTokens[1].address,
},
};
this._dispatcher.batchDispatch(allTokensByAddress, this.networkId, this._userAddress, sideToAssetToken);
this._dispatcher.updateBlockchainIsLoaded(true);
}
private async _showEtherScanLinkAndAwaitTransactionMinedAsync(
txHash: string,
): Promise<TransactionReceiptWithDecodedLogs> {
@ -665,17 +746,23 @@ export class Blockchain {
}
const provider = await Blockchain._getProviderAsync(injectedWeb3, networkIdIfExists);
const networkId = !_.isUndefined(networkIdIfExists)
this.networkId = !_.isUndefined(networkIdIfExists)
? networkIdIfExists
: configs.IS_MAINNET_ENABLED ? constants.NETWORK_ID_MAINNET : constants.NETWORK_ID_TESTNET;
: configs.IS_MAINNET_ENABLED ? constants.NETWORK_ID_MAINNET : constants.NETWORK_ID_KOVAN;
this._dispatcher.updateNetworkId(this.networkId);
const zeroExConfigs = {
networkId,
networkId: this.networkId,
};
this._zeroEx = new ZeroEx(provider, zeroExConfigs);
this._updateProviderName(injectedWeb3);
const shouldPollUserAddress = true;
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, networkId, shouldPollUserAddress);
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress);
await this._postInstantiationOrUpdatingProviderZeroExAsync();
this._userAddress = await this._web3Wrapper.getFirstAccountIfExistsAsync();
this._dispatcher.updateUserAddress(this._userAddress);
await this.fetchTokenInformationAsync();
this._web3Wrapper.startEmittingNetworkConnectionAndUserBalanceState();
await this._rehydrateStoreWithContractEvents();
}
// This method should always be run after instantiating or updating the provider
// of the ZeroEx instance.
@ -690,60 +777,6 @@ export class Blockchain {
: constants.PROVIDER_NAME_PUBLIC;
this._dispatcher.updateInjectedProviderName(providerName);
}
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.DEFAULT_TRACKED_TOKEN_SYMBOLS, 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.DEFAULT_TRACKED_TOKEN_SYMBOLS[0] }),
_.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[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();
@ -779,4 +812,20 @@ export class Blockchain {
}
}
}
private _showFlashMessageIfLedger() {
if (!_.isUndefined(this._ledgerSubprovider)) {
this._dispatcher.showFlashMessage('Confirm the transaction on your Ledger Nano S');
}
}
private async _updateDefaultGasPriceAsync() {
const endpoint = `${configs.BACKEND_BASE_URL}/eth_gas_station`;
const response = await fetch(endpoint);
if (response.status !== 200) {
return; // noop and we keep hard-coded default
}
const gasInfo = await response.json();
const gasPriceInGwei = new BigNumber(gasInfo.average / 10);
const gasPriceInWei = gasPriceInGwei.mul(1000000000);
this._defaultGasPrice = gasPriceInWei;
}
} // tslint:disable:max-file-line-count

View File

@ -3,7 +3,7 @@ import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import * as React from 'react';
import { Blockchain } from 'ts/blockchain';
import { BlockchainErrs } from 'ts/types';
import { BlockchainErrs, Networks } from 'ts/types';
import { colors } from 'ts/utils/colors';
import { configs } from 'ts/utils/configs';
import { constants } from 'ts/utils/constants';
@ -129,7 +129,7 @@ export class BlockchainErrDialog extends React.Component<BlockchainErrDialogProp
<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.NETWORK_ID_TESTNET})
{Networks.Kovan} testnet (network Id: {constants.NETWORK_ID_KOVAN})
{configs.IS_MAINNET_ENABLED
? ` or ${constants.MAINNET_NAME} (network Id: ${constants.NETWORK_ID_MAINNET}).`
: `.`}

View File

@ -2,38 +2,55 @@ import { BigNumber } from '@0xproject/utils';
import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import * as React from 'react';
import { Blockchain } from 'ts/blockchain';
import { EthAmountInput } from 'ts/components/inputs/eth_amount_input';
import { TokenAmountInput } from 'ts/components/inputs/token_amount_input';
import { Side, Token, TokenState } from 'ts/types';
import { Side, Token } from 'ts/types';
import { colors } from 'ts/utils/colors';
interface EthWethConversionDialogProps {
blockchain: Blockchain;
userAddress: string;
networkId: number;
direction: Side;
onComplete: (direction: Side, value: BigNumber) => void;
onCancelled: () => void;
isOpen: boolean;
token: Token;
tokenState: TokenState;
etherBalance: BigNumber;
lastForceTokenStateRefetch: number;
}
interface EthWethConversionDialogState {
value?: BigNumber;
shouldShowIncompleteErrs: boolean;
hasErrors: boolean;
isEthTokenBalanceLoaded: boolean;
ethTokenBalance: BigNumber;
}
export class EthWethConversionDialog extends React.Component<
EthWethConversionDialogProps,
EthWethConversionDialogState
> {
private _isUnmounted: boolean;
constructor() {
super();
this._isUnmounted = false;
this.state = {
shouldShowIncompleteErrs: false,
hasErrors: false,
isEthTokenBalanceLoaded: false,
ethTokenBalance: new BigNumber(0),
};
}
public componentWillMount() {
// tslint:disable-next-line:no-floating-promises
this._fetchEthTokenBalanceAsync();
}
public componentWillUnmount() {
this._isUnmounted = true;
}
public render() {
const convertDialogActions = [
<FlatButton key="cancel" label="Cancel" onTouchTap={this._onCancel.bind(this)} />,
@ -72,8 +89,11 @@ export class EthWethConversionDialog extends React.Component<
<div className="pt2 mx-auto" style={{ width: 245 }}>
{this.props.direction === Side.Receive ? (
<TokenAmountInput
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
blockchain={this.props.blockchain}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
token={this.props.token}
tokenState={this.props.tokenState}
shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs}
shouldCheckBalance={true}
shouldCheckAllowance={false}
@ -93,19 +113,20 @@ export class EthWethConversionDialog extends React.Component<
)}
<div className="pt1" style={{ fontSize: 12 }}>
<div className="left">1 ETH = 1 WETH</div>
{this.props.direction === Side.Receive && (
<div
className="right"
onClick={this._onMaxClick.bind(this)}
style={{
color: colors.darkBlue,
textDecoration: 'underline',
cursor: 'pointer',
}}
>
Max
</div>
)}
{this.props.direction === Side.Receive &&
this.state.isEthTokenBalanceLoaded && (
<div
className="right"
onClick={this._onMaxClick.bind(this)}
style={{
color: colors.darkBlue,
textDecoration: 'underline',
cursor: 'pointer',
}}
>
Max
</div>
)}
</div>
</div>
</div>
@ -132,7 +153,7 @@ export class EthWethConversionDialog extends React.Component<
}
private _onMaxClick() {
this.setState({
value: this.props.tokenState.balance,
value: this.state.ethTokenBalance,
});
}
private _onValueChange(isValid: boolean, amount?: BigNumber) {
@ -160,4 +181,16 @@ export class EthWethConversionDialog extends React.Component<
});
this.props.onCancelled();
}
private async _fetchEthTokenBalanceAsync() {
const [balance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
this.props.userAddress,
this.props.token.address,
);
if (!this._isUnmounted) {
this.setState({
isEthTokenBalanceLoaded: true,
ethTokenBalance: balance,
});
}
}
}

View File

@ -7,8 +7,10 @@ import TextField from 'material-ui/TextField';
import * as React from 'react';
import ReactTooltip = require('react-tooltip');
import { Blockchain } from 'ts/blockchain';
import { NetworkDropDown } from 'ts/components/dropdowns/network_drop_down';
import { LifeCycleRaisedButton } from 'ts/components/ui/lifecycle_raised_button';
import { Dispatcher } from 'ts/redux/dispatcher';
import { ProviderType } from 'ts/types';
import { colors } from 'ts/utils/colors';
import { configs } from 'ts/utils/configs';
import { constants } from 'ts/utils/constants';
@ -27,27 +29,33 @@ interface LedgerConfigDialogProps {
dispatcher: Dispatcher;
blockchain: Blockchain;
networkId: number;
providerType: ProviderType;
}
interface LedgerConfigDialogState {
didConnectFail: boolean;
connectionErrMsg: string;
stepIndex: LedgerSteps;
userAddresses: string[];
addressBalances: BigNumber[];
derivationPath: string;
derivationErrMsg: string;
preferredNetworkId: number;
}
export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, LedgerConfigDialogState> {
constructor(props: LedgerConfigDialogProps) {
super(props);
const derivationPathIfExists = props.blockchain.getLedgerDerivationPathIfExists();
this.state = {
didConnectFail: false,
connectionErrMsg: '',
stepIndex: LedgerSteps.CONNECT,
userAddresses: [],
addressBalances: [],
derivationPath: configs.DEFAULT_DERIVATION_PATH,
derivationPath: _.isUndefined(derivationPathIfExists)
? configs.DEFAULT_DERIVATION_PATH
: derivationPathIfExists,
derivationErrMsg: '',
preferredNetworkId: props.networkId,
};
}
public render() {
@ -74,19 +82,28 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
);
}
private _renderConnectStep() {
const networkIds = _.values(constants.NETWORK_ID_BY_NAME);
return (
<div>
<div className="h4 pt3">Follow these instructions before proceeding:</div>
<ol>
<ol className="mb0">
<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">Verify that "Browser Support" AND "Contract Data" are 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>
<li>Choose your desired network:</li>
</ol>
<div className="pb2">
<NetworkDropDown
updateSelectedNetwork={this._onSelectedNetworkUpdated.bind(this)}
selectedNetworkId={this.state.preferredNetworkId}
avialableNetworkIds={networkIds}
/>
</div>
<div className="center pb3">
<LifeCycleRaisedButton
isPrimary={true}
@ -95,9 +112,9 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
labelComplete="Connected!"
onClickAsyncFn={this._onConnectLedgerClickAsync.bind(this, true)}
/>
{this.state.didConnectFail && (
{!_.isEmpty(this.state.connectionErrMsg) && (
<div className="pt2 left-align" style={{ color: colors.red200 }}>
Failed to connect. Follow the instructions and try again.
{this.state.connectionErrMsg}
</div>
)}
</div>
@ -172,7 +189,8 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
}
private _onClose() {
this.setState({
didConnectFail: false,
connectionErrMsg: '',
stepIndex: LedgerSteps.CONNECT,
});
const isOpen = false;
this.props.toggleDialogFn(isOpen);
@ -184,6 +202,8 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
const selectAddressBalance = this.state.addressBalances[selectedRowIndex];
this.props.dispatcher.updateUserAddress(selectedAddress);
this.props.blockchain.updateWeb3WrapperPrevUserAddress(selectedAddress);
// tslint:disable-next-line:no-floating-promises
this.props.blockchain.fetchTokenInformationAsync();
this.props.dispatcher.updateUserEtherBalance(selectAddressBalance);
this.setState({
stepIndex: LedgerSteps.CONNECT,
@ -219,7 +239,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
} catch (err) {
utils.consoleLog(`Ledger error: ${JSON.stringify(err)}`);
this.setState({
didConnectFail: true,
connectionErrMsg: 'Failed to connect. Follow the instructions and try again.',
});
return false;
}
@ -241,6 +261,22 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
});
}
private async _onConnectLedgerClickAsync() {
const isU2FSupported = await utils.isU2FSupportedAsync();
if (!isU2FSupported) {
utils.consoleLog(`U2F not supported in this browser`);
this.setState({
connectionErrMsg: 'U2F not supported by this browser. Try using Chrome.',
});
return false;
}
if (
this.props.providerType !== ProviderType.Ledger ||
(this.props.providerType === ProviderType.Ledger && this.props.networkId !== this.state.preferredNetworkId)
) {
await this.props.blockchain.updateProviderToLedgerAsync(this.state.preferredNetworkId);
}
const didSucceed = await this._fetchAddressesAndBalancesAsync();
if (didSucceed) {
this.setState({
@ -258,4 +294,9 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
}
return userAddresses;
}
private _onSelectedNetworkUpdated(e: any, index: number, networkId: number) {
this.setState({
preferredNetworkId: networkId,
});
}
}

View File

@ -3,16 +3,20 @@ import * as _ from 'lodash';
import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton';
import * as React from 'react';
import { Blockchain } from 'ts/blockchain';
import { AddressInput } from 'ts/components/inputs/address_input';
import { TokenAmountInput } from 'ts/components/inputs/token_amount_input';
import { Token, TokenState } from 'ts/types';
import { Token } from 'ts/types';
interface SendDialogProps {
blockchain: Blockchain;
userAddress: string;
networkId: number;
onComplete: (recipient: string, value: BigNumber) => void;
onCancelled: () => void;
isOpen: boolean;
token: Token;
tokenState: TokenState;
lastForceTokenStateRefetch: number;
}
interface SendDialogState {
@ -66,15 +70,18 @@ export class SendDialog extends React.Component<SendDialogProps, SendDialogState
/>
</div>
<TokenAmountInput
blockchain={this.props.blockchain}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
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}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
/>
</div>
);

View File

@ -82,16 +82,6 @@ export class TrackTokenConfirmationDialog extends React.Component<
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({

View File

@ -0,0 +1,40 @@
import * as _ from 'lodash';
import DropDownMenu from 'material-ui/DropDownMenu';
import MenuItem from 'material-ui/MenuItem';
import * as React from 'react';
import { constants } from 'ts/utils/constants';
interface NetworkDropDownProps {
updateSelectedNetwork: (e: any, index: number, value: number) => void;
selectedNetworkId: number;
avialableNetworkIds: number[];
}
interface NetworkDropDownState {}
export class NetworkDropDown extends React.Component<NetworkDropDownProps, NetworkDropDownState> {
public render() {
return (
<div className="mx-auto" style={{ width: 120 }}>
<DropDownMenu value={this.props.selectedNetworkId} onChange={this.props.updateSelectedNetwork}>
{this._renderDropDownItems()}
</DropDownMenu>
</div>
);
}
private _renderDropDownItems() {
const items = _.map(this.props.avialableNetworkIds, networkId => {
const networkName = constants.NETWORK_NAME_BY_ID[networkId];
const primaryText = (
<div className="flex">
<div className="pr1" style={{ width: 14, paddingTop: 2 }}>
<img src={`/images/network_icons/${networkName.toLowerCase()}.png`} style={{ width: 14 }} />
</div>
<div>{networkName}</div>
</div>
);
return <MenuItem key={networkId} value={networkId} primaryText={primaryText} />;
});
return items;
}
}

View File

@ -6,21 +6,24 @@ import * as React from 'react';
import { Blockchain } from 'ts/blockchain';
import { EthWethConversionDialog } from 'ts/components/dialogs/eth_weth_conversion_dialog';
import { Dispatcher } from 'ts/redux/dispatcher';
import { BlockchainCallErrs, Side, Token, TokenState } from 'ts/types';
import { BlockchainCallErrs, Side, Token } from 'ts/types';
import { constants } from 'ts/utils/constants';
import { errorReporter } from 'ts/utils/error_reporter';
import { utils } from 'ts/utils/utils';
interface EthWethConversionButtonProps {
userAddress: string;
networkId: number;
direction: Side;
ethToken: Token;
ethTokenState: TokenState;
dispatcher: Dispatcher;
blockchain: Blockchain;
userEtherBalance: BigNumber;
isOutdatedWrappedEther: boolean;
onConversionSuccessful?: () => void;
isDisabled?: boolean;
lastForceTokenStateRefetch: number;
refetchEthTokenStateAsync: () => Promise<void>;
}
interface EthWethConversionButtonState {
@ -64,13 +67,16 @@ export class EthWethConversionButton extends React.Component<
onClick={this._toggleConversionDialog.bind(this)}
/>
<EthWethConversionDialog
blockchain={this.props.blockchain}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
direction={this.props.direction}
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}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
/>
</div>
);
@ -86,29 +92,25 @@ export class EthWethConversionButton extends React.Component<
});
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(token.address, value);
const ethAmount = ZeroEx.toUnitAmount(value, constants.DECIMAL_PLACES_ETH);
this.props.dispatcher.showFlashMessage(`Successfully wrapped ${ethAmount.toString()} ETH to WETH`);
balance = balance.plus(value);
} else {
await this.props.blockchain.convertWrappedEthTokensToEthAsync(token.address, value);
const tokenAmount = ZeroEx.toUnitAmount(value, token.decimals);
this.props.dispatcher.showFlashMessage(`Successfully unwrapped ${tokenAmount.toString()} WETH to ETH`);
balance = balance.minus(value);
}
if (!this.props.isOutdatedWrappedEther) {
this.props.dispatcher.replaceTokenBalanceByAddress(token.address, balance);
await this.props.refetchEthTokenStateAsync();
}
this.props.onConversionSuccessful();
} catch (err) {
const errMsg = `${err}`;
if (_.includes(errMsg, BlockchainCallErrs.UserHasNoAssociatedAddresses)) {
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
} else if (!_.includes(errMsg, 'User denied transaction')) {
} else if (!utils.didUserDenyWeb3Request(errMsg)) {
utils.consoleLog(`Unexpected error encountered: ${err}`);
utils.consoleLog(err.stack);
const errorMsg =

View File

@ -16,7 +16,6 @@ import {
Token,
TokenByAddress,
TokenState,
TokenStateByAddress,
} from 'ts/types';
import { colors } from 'ts/utils/colors';
import { configs } from 'ts/utils/configs';
@ -41,19 +40,23 @@ interface EthWrappersProps {
blockchain: Blockchain;
dispatcher: Dispatcher;
tokenByAddress: TokenByAddress;
tokenStateByAddress: TokenStateByAddress;
userAddress: string;
userEtherBalance: BigNumber;
lastForceTokenStateRefetch: number;
}
interface EthWrappersState {
ethTokenState: TokenState;
isWethStateLoaded: boolean;
outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded;
outdatedWETHStateByAddress: OutdatedWETHStateByAddress;
}
export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersState> {
private _isUnmounted: boolean;
constructor(props: EthWrappersProps) {
super(props);
this._isUnmounted = false;
const outdatedWETHAddresses = this._getOutdatedWETHAddresses();
const outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded = {};
const outdatedWETHStateByAddress: OutdatedWETHStateByAddress = {};
@ -67,18 +70,34 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
this.state = {
outdatedWETHAddressToIsStateLoaded,
outdatedWETHStateByAddress,
isWethStateLoaded: false,
ethTokenState: {
balance: new BigNumber(0),
allowance: new BigNumber(0),
},
};
}
public componentWillReceiveProps(nextProps: EthWrappersProps) {
if (
nextProps.userAddress !== this.props.userAddress ||
nextProps.networkId !== this.props.networkId ||
nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch
) {
// tslint:disable-next-line:no-floating-promises
this._fetchWETHStateAsync();
}
}
public componentDidMount() {
window.scrollTo(0, 0);
// tslint:disable-next-line:no-floating-promises
this._fetchOutdatedWETHStateAsync();
this._fetchWETHStateAsync();
}
public componentWillUnmount() {
this._isUnmounted = true;
}
public render() {
const tokens = _.values(this.props.tokenByAddress);
const etherToken = _.find(tokens, { symbol: 'WETH' });
const etherTokenState = this.props.tokenStateByAddress[etherToken.address];
const wethBalance = ZeroEx.toUnitAmount(etherTokenState.balance, constants.DECIMAL_PLACES_ETH);
const etherToken = this._getEthToken();
const wethBalance = ZeroEx.toUnitAmount(this.state.ethTokenState.balance, constants.DECIMAL_PLACES_ETH);
const isBidirectional = true;
const etherscanUrl = utils.getEtherScanLinkIfExists(
etherToken.address,
@ -136,10 +155,13 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
</TableRowColumn>
<TableRowColumn>
<EthWethConversionButton
refetchEthTokenStateAsync={this._refetchEthTokenStateAsync.bind(this)}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
isOutdatedWrappedEther={false}
direction={Side.Deposit}
ethToken={etherToken}
ethTokenState={etherTokenState}
dispatcher={this.props.dispatcher}
blockchain={this.props.blockchain}
userEtherBalance={this.props.userEtherBalance}
@ -150,13 +172,23 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
<TableRowColumn className="py1">
{this._renderTokenLink(tokenLabel, etherscanUrl)}
</TableRowColumn>
<TableRowColumn>{wethBalance.toFixed(PRECISION)} WETH</TableRowColumn>
<TableRowColumn>
{this.state.isWethStateLoaded ? (
`${wethBalance.toFixed(PRECISION)} WETH`
) : (
<i className="zmdi zmdi-spinner zmdi-hc-spin" />
)}
</TableRowColumn>
<TableRowColumn>
<EthWethConversionButton
refetchEthTokenStateAsync={this._refetchEthTokenStateAsync.bind(this)}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
isOutdatedWrappedEther={false}
direction={Side.Receive}
isDisabled={!this.state.isWethStateLoaded}
ethToken={etherToken}
ethTokenState={etherTokenState}
dispatcher={this.props.dispatcher}
blockchain={this.props.blockchain}
userEtherBalance={this.props.userEtherBalance}
@ -190,7 +222,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
</TableRow>
</TableHeader>
<TableBody displayRowCheckbox={false}>
{this._renderOutdatedWeths(etherToken, etherTokenState)}
{this._renderOutdatedWeths(etherToken, this.state.ethTokenState)}
</TableBody>
</Table>
</div>
@ -269,6 +301,10 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
</TableRowColumn>
<TableRowColumn>
<EthWethConversionButton
refetchEthTokenStateAsync={this._refetchEthTokenStateAsync.bind(this)}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
isDisabled={!isStateLoaded}
isOutdatedWrappedEther={true}
direction={Side.Receive}
@ -338,7 +374,14 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
},
});
}
private async _fetchOutdatedWETHStateAsync() {
private async _fetchWETHStateAsync() {
const tokens = _.values(this.props.tokenByAddress);
const wethToken = _.find(tokens, token => token.symbol === 'WETH');
const [wethBalance, wethAllowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
this.props.userAddress,
wethToken.address,
);
const outdatedWETHAddresses = this._getOutdatedWETHAddresses();
const outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded = {};
const outdatedWETHStateByAddress: OutdatedWETHStateByAddress = {};
@ -353,10 +396,17 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
};
outdatedWETHAddressToIsStateLoaded[address] = true;
}
this.setState({
outdatedWETHStateByAddress,
outdatedWETHAddressToIsStateLoaded,
});
if (!this._isUnmounted) {
this.setState({
outdatedWETHStateByAddress,
outdatedWETHAddressToIsStateLoaded,
ethTokenState: {
balance: wethBalance,
allowance: wethAllowance,
},
isWethStateLoaded: true,
});
}
}
private _getOutdatedWETHAddresses(): string[] {
const outdatedWETHAddresses = _.compact(
@ -371,4 +421,22 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
);
return outdatedWETHAddresses;
}
private _getEthToken() {
const tokens = _.values(this.props.tokenByAddress);
const etherToken = _.find(tokens, { symbol: 'WETH' });
return etherToken;
}
private async _refetchEthTokenStateAsync() {
const etherToken = this._getEthToken();
const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
this.props.userAddress,
etherToken.address,
);
this.setState({
ethTokenState: {
balance,
allowance,
},
});
}
} // tslint:disable:max-file-line-count

View File

@ -19,7 +19,7 @@ import { VisualOrder } from 'ts/components/visual_order';
import { Dispatcher } from 'ts/redux/dispatcher';
import { orderSchema } from 'ts/schemas/order_schema';
import { SchemaValidator } from 'ts/schemas/validator';
import { AlertTypes, BlockchainErrs, Order, Token, TokenByAddress, TokenStateByAddress, WebsitePaths } from 'ts/types';
import { AlertTypes, BlockchainErrs, Order, Token, TokenByAddress, WebsitePaths } from 'ts/types';
import { colors } from 'ts/utils/colors';
import { constants } from 'ts/utils/constants';
import { errorReporter } from 'ts/utils/error_reporter';
@ -33,9 +33,9 @@ interface FillOrderProps {
networkId: number;
userAddress: string;
tokenByAddress: TokenByAddress;
tokenStateByAddress: TokenStateByAddress;
initialOrder: Order;
dispatcher: Dispatcher;
lastForceTokenStateRefetch: number;
}
interface FillOrderState {
@ -59,8 +59,10 @@ interface FillOrderState {
export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
private _validator: SchemaValidator;
private _isUnmounted: boolean;
constructor(props: FillOrderProps) {
super(props);
this._isUnmounted = false;
this.state = {
globalErrMsg: '',
didOrderValidationRun: false,
@ -90,6 +92,9 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
public componentDidMount() {
window.scrollTo(0, 0);
}
public componentWillUnmount() {
this._isUnmounted = true;
}
public render() {
return (
<div className="clearfix lg-px4 md-px4 sm-px2" style={{ minHeight: 600 }}>
@ -185,7 +190,6 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
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 = {
@ -249,14 +253,17 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
{!isUserMaker && (
<div className="clearfix mx-auto relative" style={{ width: 235, height: 108 }}>
<TokenAmountInput
blockchain={this.props.blockchain}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
label="Fill amount"
onChange={this._onFillAmountChange.bind(this)}
shouldShowIncompleteErrs={false}
token={fillToken}
tokenState={fillTokenState}
amount={fillAssetToken.amount}
shouldCheckBalance={true}
shouldCheckAllowance={true}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
/>
<div
className="absolute sm-hide xs-hide"
@ -454,12 +461,14 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
if (!_.isEmpty(orderJSON)) {
orderJSONErrMsg = 'Submitted order JSON is not valid JSON';
}
this.setState({
didOrderValidationRun: true,
orderJSON,
orderJSONErrMsg,
parsedOrder,
});
if (!this._isUnmounted) {
this.setState({
didOrderValidationRun: true,
orderJSON,
orderJSONErrMsg,
parsedOrder,
});
}
return;
}
@ -556,11 +565,8 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
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);
// After fill completes, let's force fetch the token balances
this.props.dispatcher.forceTokenStateRefetch();
this.setState({
isFilling: false,
didFillOrderSucceed: true,
@ -573,7 +579,7 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
isFilling: false,
});
const errMsg = `${err}`;
if (_.includes(errMsg, 'User denied transaction signature')) {
if (utils.didUserDenyWeb3Request(errMsg)) {
return;
}
globalErrMsg = 'Failed to fill order, please refresh and try again';
@ -653,7 +659,7 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
isCancelling: false,
});
const errMsg = `${err}`;
if (_.includes(errMsg, 'User denied transaction signature')) {
if (utils.didUserDenyWeb3Request(errMsg)) {
return;
}
globalErrMsg = 'Failed to cancel order, please refresh and try again';

View File

@ -8,7 +8,7 @@ import { TrackTokenConfirmation } from 'ts/components/track_token_confirmation';
import { TokenIcon } from 'ts/components/ui/token_icon';
import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage';
import { Dispatcher } from 'ts/redux/dispatcher';
import { DialogConfigs, Token, TokenByAddress, TokenState, TokenVisibility } from 'ts/types';
import { DialogConfigs, Token, TokenByAddress, TokenVisibility } from 'ts/types';
const TOKEN_ICON_DIMENSION = 100;
const TILE_DIMENSION = 146;
@ -223,10 +223,7 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt
assetView: AssetViews.NEW_TOKEN_FORM,
});
}
private _onNewTokenSubmitted(newToken: Token, newTokenState: TokenState) {
this.props.dispatcher.updateTokenStateByAddress({
[newToken.address]: newTokenState,
});
private _onNewTokenSubmitted(newToken: Token) {
trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newToken);
this.props.dispatcher.addTokenToTokenByAddress(newToken);
this.setState({
@ -256,15 +253,6 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt
newTokenEntry.isTracked = true;
trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newTokenEntry);
const [balance, allowance] = await this.props.blockchain.getCurrentUserTokenBalanceAndAllowanceAsync(
token.address,
);
this.props.dispatcher.updateTokenStateByAddress({
[token.address]: {
balance,
allowance,
},
});
this.props.dispatcher.updateTokenByAddress([newTokenEntry]);
this.setState({
isAddingTokenToTracked: false,

View File

@ -27,7 +27,6 @@ import {
SignatureData,
Token,
TokenByAddress,
TokenStateByAddress,
} from 'ts/types';
import { colors } from 'ts/utils/colors';
import { errorReporter } from 'ts/utils/error_reporter';
@ -53,7 +52,7 @@ interface GenerateOrderFormProps {
orderSalt: BigNumber;
sideToAssetToken: SideToAssetToken;
tokenByAddress: TokenByAddress;
tokenStateByAddress: TokenStateByAddress;
lastForceTokenStateRefetch: number;
}
interface GenerateOrderFormState {
@ -80,10 +79,8 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G
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> \
@ -110,9 +107,12 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G
tokenByAddress={this.props.tokenByAddress}
/>
<TokenAmountInput
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
blockchain={this.props.blockchain}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
label="Sell amount"
token={depositToken}
tokenState={depositTokenState}
amount={this.props.sideToAssetToken[Side.Deposit].amount}
onChange={this._onTokenAmountChange.bind(this, depositToken, Side.Deposit)}
shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs}
@ -139,9 +139,12 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G
tokenByAddress={this.props.tokenByAddress}
/>
<TokenAmountInput
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
blockchain={this.props.blockchain}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
label="Receive amount"
token={receiveToken}
tokenState={receiveTokenState}
amount={this.props.sideToAssetToken[Side.Receive].amount}
onChange={this._onTokenAmountChange.bind(this, receiveToken, Side.Receive)}
shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs}
@ -242,8 +245,10 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G
// 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 [debitBalance, debitAllowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
this.props.userAddress,
debitToken.address,
);
const receiveAmount = this.props.sideToAssetToken[Side.Receive].amount;
if (
!_.isUndefined(debitToken.amount) &&

View File

@ -1,4 +1,3 @@
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import TextField from 'material-ui/TextField';
import * as React from 'react';
@ -7,13 +6,13 @@ 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 { AlertTypes, Token, TokenByAddress, TokenState } from 'ts/types';
import { AlertTypes, Token, TokenByAddress } from 'ts/types';
import { colors } from 'ts/utils/colors';
interface NewTokenFormProps {
blockchain: Blockchain;
tokenByAddress: TokenByAddress;
onNewTokenSubmitted: (token: Token, tokenState: TokenState) => void;
onNewTokenSubmitted: (token: Token) => void;
}
interface NewTokenFormState {
@ -110,13 +109,9 @@ export class NewTokenForm extends React.Component<NewTokenFormProps, NewTokenFor
}
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,
);
await this.props.blockchain.getCurrentUserTokenBalanceAndAllowanceAsync(this.state.address);
} catch (err) {
hasBalanceAllowanceErr = true;
}
@ -155,11 +150,7 @@ export class NewTokenForm extends React.Component<NewTokenFormProps, NewTokenFor
isTracked: true,
isRegistered: false,
};
const newTokenState: TokenState = {
balance,
allowance,
};
this.props.onNewTokenSubmitted(newToken, newTokenState);
this.props.onNewTokenSubmitted(newToken);
}
private _onTokenNameChanged(e: any, name: string) {
let nameErrText = '';

View File

@ -17,6 +17,8 @@ interface AllowanceToggleProps {
token: Token;
tokenState: TokenState;
userAddress: string;
isDisabled: boolean;
refetchTokenStateAsync: () => Promise<void>;
}
interface AllowanceToggleState {
@ -45,7 +47,7 @@ export class AllowanceToggle extends React.Component<AllowanceToggleProps, Allow
<div className="flex">
<div>
<Toggle
disabled={this.state.isSpinnerVisible}
disabled={this.state.isSpinnerVisible || this.props.isDisabled}
toggled={this._isAllowanceSet()}
onToggle={this._onToggleAllowanceAsync.bind(this)}
/>
@ -73,12 +75,13 @@ export class AllowanceToggle extends React.Component<AllowanceToggleProps, Allow
}
try {
await this.props.blockchain.setProxyAllowanceAsync(this.props.token, newAllowanceAmountInBaseUnits);
await this.props.refetchTokenStateAsync();
} catch (err) {
this.setState({
isSpinnerVisible: false,
});
const errMsg = `${err}`;
if (_.includes(errMsg, 'User denied transaction')) {
if (utils.didUserDenyWeb3Request(errMsg)) {
return;
}
utils.consoleLog(`Unexpected error encountered: ${err}`);

View File

@ -18,6 +18,7 @@ interface BalanceBoundedInputProps {
validate?: (amount: BigNumber) => InputErrMsg;
onVisitBalancesPageClick?: () => void;
shouldHideVisitBalancesLink?: boolean;
isDisabled?: boolean;
}
interface BalanceBoundedInputState {
@ -29,6 +30,7 @@ export class BalanceBoundedInput extends React.Component<BalanceBoundedInputProp
public static defaultProps: Partial<BalanceBoundedInputProps> = {
shouldShowIncompleteErrs: false,
shouldHideVisitBalancesLink: false,
isDisabled: false,
};
constructor(props: BalanceBoundedInputProps) {
super(props);
@ -88,6 +90,7 @@ export class BalanceBoundedInput extends React.Component<BalanceBoundedInputProp
hintText={<span style={{ textTransform: 'capitalize' }}>amount</span>}
onChange={this._onValueChange.bind(this)}
underlineStyle={{ width: 'calc(100% + 50px)' }}
disabled={this.props.isDisabled}
/>
);
}
@ -100,7 +103,7 @@ export class BalanceBoundedInput extends React.Component<BalanceBoundedInputProp
},
() => {
const isValid = _.isUndefined(errMsg);
if (utils.isNumeric(amountString)) {
if (utils.isNumeric(amountString) && !_.includes(amountString, '-')) {
this.props.onChange(isValid, new BigNumber(amountString));
} else {
this.props.onChange(isValid);

View File

@ -3,13 +3,16 @@ import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import * as React from 'react';
import { Link } from 'react-router-dom';
import { Blockchain } from 'ts/blockchain';
import { BalanceBoundedInput } from 'ts/components/inputs/balance_bounded_input';
import { InputErrMsg, Token, TokenState, ValidatedBigNumberCallback, WebsitePaths } from 'ts/types';
import { InputErrMsg, Token, ValidatedBigNumberCallback, WebsitePaths } from 'ts/types';
import { colors } from 'ts/utils/colors';
interface TokenAmountInputProps {
userAddress: string;
networkId: number;
blockchain: Blockchain;
token: Token;
tokenState: TokenState;
label?: string;
amount?: BigNumber;
shouldShowIncompleteErrs: boolean;
@ -17,11 +20,45 @@ interface TokenAmountInputProps {
shouldCheckAllowance: boolean;
onChange: ValidatedBigNumberCallback;
onVisitBalancesPageClick?: () => void;
lastForceTokenStateRefetch: number;
}
interface TokenAmountInputState {}
interface TokenAmountInputState {
balance: BigNumber;
allowance: BigNumber;
isBalanceAndAllowanceLoaded: boolean;
}
export class TokenAmountInput extends React.Component<TokenAmountInputProps, TokenAmountInputState> {
private _isUnmounted: boolean;
constructor(props: TokenAmountInputProps) {
super(props);
this._isUnmounted = false;
const defaultAmount = new BigNumber(0);
this.state = {
balance: defaultAmount,
allowance: defaultAmount,
isBalanceAndAllowanceLoaded: false,
};
}
public componentWillMount() {
// tslint:disable-next-line:no-floating-promises
this._fetchBalanceAndAllowanceAsync(this.props.token.address, this.props.userAddress);
}
public componentWillUnmount() {
this._isUnmounted = true;
}
public componentWillReceiveProps(nextProps: TokenAmountInputProps) {
if (
nextProps.userAddress !== this.props.userAddress ||
nextProps.networkId !== this.props.networkId ||
nextProps.token.address !== this.props.token.address ||
nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch
) {
// tslint:disable-next-line:no-floating-promises
this._fetchBalanceAndAllowanceAsync(nextProps.token.address, nextProps.userAddress);
}
}
public render() {
const amount = this.props.amount
? ZeroEx.toUnitAmount(this.props.amount, this.props.token.decimals)
@ -32,12 +69,13 @@ export class TokenAmountInput extends React.Component<TokenAmountInputProps, Tok
<BalanceBoundedInput
label={this.props.label}
amount={amount}
balance={ZeroEx.toUnitAmount(this.props.tokenState.balance, this.props.token.decimals)}
balance={ZeroEx.toUnitAmount(this.state.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}
isDisabled={!this.state.isBalanceAndAllowanceLoaded}
/>
<div style={{ paddingTop: hasLabel ? 39 : 14 }}>{this.props.token.symbol}</div>
</div>
@ -51,7 +89,7 @@ export class TokenAmountInput extends React.Component<TokenAmountInputProps, Tok
this.props.onChange(isValid, baseUnitAmount);
}
private _validate(amount: BigNumber): InputErrMsg {
if (this.props.shouldCheckAllowance && amount.gt(this.props.tokenState.allowance)) {
if (this.props.shouldCheckAllowance && amount.gt(this.state.allowance)) {
return (
<span>
Insufficient allowance.{' '}
@ -67,4 +105,20 @@ export class TokenAmountInput extends React.Component<TokenAmountInputProps, Tok
return undefined;
}
}
private async _fetchBalanceAndAllowanceAsync(tokenAddress: string, userAddress: string) {
this.setState({
isBalanceAndAllowanceLoaded: false,
});
const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
userAddress,
tokenAddress,
);
if (!this._isUnmounted) {
this.setState({
balance,
allowance,
isBalanceAndAllowanceLoaded: true,
});
}
}
}

View File

@ -1,11 +1,13 @@
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import CircularProgress from 'material-ui/CircularProgress';
import Paper from 'material-ui/Paper';
import * as React from 'react';
import * as DocumentTitle from 'react-document-title';
import { Route, Switch } from 'react-router-dom';
import { Blockchain } from 'ts/blockchain';
import { BlockchainErrDialog } from 'ts/components/dialogs/blockchain_err_dialog';
import { LedgerConfigDialog } from 'ts/components/dialogs/ledger_config_dialog';
import { PortalDisclaimerDialog } from 'ts/components/dialogs/portal_disclaimer_dialog';
import { WrappedEthSectionNoticeDialog } from 'ts/components/dialogs/wrapped_eth_section_notice_dialog';
import { EthWrappers } from 'ts/components/eth_wrappers';
@ -13,25 +15,15 @@ import { FillOrder } from 'ts/components/fill_order';
import { Footer } from 'ts/components/footer';
import { PortalMenu } from 'ts/components/portal_menu';
import { TokenBalances } from 'ts/components/token_balances';
import { TopBar } from 'ts/components/top_bar';
import { TopBar } from 'ts/components/top_bar/top_bar';
import { TradeHistory } from 'ts/components/trade_history/trade_history';
import { FlashMessage } from 'ts/components/ui/flash_message';
import { Loading } from 'ts/components/ui/loading';
import { GenerateOrderForm } from 'ts/containers/generate_order_form';
import { localStorage } from 'ts/local_storage/local_storage';
import { Dispatcher } from 'ts/redux/dispatcher';
import { orderSchema } from 'ts/schemas/order_schema';
import { SchemaValidator } from 'ts/schemas/validator';
import {
BlockchainErrs,
HashData,
Order,
ScreenWidths,
Token,
TokenByAddress,
TokenStateByAddress,
WebsitePaths,
} from 'ts/types';
import { BlockchainErrs, HashData, Order, ProviderType, ScreenWidths, TokenByAddress, WebsitePaths } from 'ts/types';
import { colors } from 'ts/utils/colors';
import { configs } from 'ts/utils/configs';
import { constants } from 'ts/utils/constants';
@ -46,18 +38,20 @@ export interface PortalAllProps {
blockchainIsLoaded: boolean;
dispatcher: Dispatcher;
hashData: HashData;
injectedProviderName: string;
networkId: number;
nodeVersion: string;
orderFillAmount: BigNumber;
providerType: ProviderType;
screenWidth: ScreenWidths;
tokenByAddress: TokenByAddress;
tokenStateByAddress: TokenStateByAddress;
userEtherBalance: BigNumber;
userAddress: string;
shouldBlockchainErrDialogBeOpen: boolean;
userSuppliedOrderCache: Order;
location: Location;
flashMessage?: string | React.ReactNode;
lastForceTokenStateRefetch: number;
}
interface PortalAllState {
@ -67,6 +61,7 @@ interface PortalAllState {
prevPathname: string;
isDisclaimerDialogOpen: boolean;
isWethNoticeDialogOpen: boolean;
isLedgerDialogOpen: boolean;
}
export class Portal extends React.Component<PortalAllProps, PortalAllState> {
@ -96,6 +91,7 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
prevPathname: this.props.location.pathname,
isDisclaimerDialogOpen: !hasAcceptedDisclaimer,
isWethNoticeDialogOpen: !hasAlreadyDismissedWethNotice && isViewingBalances,
isLedgerDialogOpen: false,
};
}
public componentDidMount() {
@ -125,11 +121,6 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
if (nextProps.userAddress !== this.state.prevUserAddress) {
// tslint:disable-next-line:no-floating-promises
this._blockchain.userAddressUpdatedFireAndForgetAsync(nextProps.userAddress);
if (!_.isEmpty(nextProps.userAddress) && nextProps.blockchainIsLoaded) {
const tokens = _.values(nextProps.tokenByAddress);
// tslint:disable-next-line:no-floating-promises
this._updateBalanceAndAllowanceWithLoadingScreenAsync(tokens);
}
this.setState({
prevUserAddress: nextProps.userAddress,
});
@ -167,8 +158,14 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
<DocumentTitle title="0x Portal DApp" />
<TopBar
userAddress={this.props.userAddress}
networkId={this.props.networkId}
injectedProviderName={this.props.injectedProviderName}
onToggleLedgerDialog={this.onToggleLedgerDialog.bind(this)}
dispatcher={this.props.dispatcher}
providerType={this.props.providerType}
blockchainIsLoaded={this.props.blockchainIsLoaded}
location={this.props.location}
blockchain={this._blockchain}
/>
<div id="portal" className="mx-auto max-width-4" style={{ width: '100%' }}>
<Paper className="mb3 mt2">
@ -215,7 +212,19 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
/>
</Switch>
) : (
<Loading />
<div className="pt4 sm-px2 sm-pt2 sm-m1" style={{ height: 500 }}>
<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 Portal...
</div>
</div>
</div>
)}
</div>
</div>
@ -239,11 +248,26 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
onToggleDialog={this._onPortalDisclaimerAccepted.bind(this)}
/>
<FlashMessage dispatcher={this.props.dispatcher} flashMessage={this.props.flashMessage} />
{this.props.blockchainIsLoaded && (
<LedgerConfigDialog
providerType={this.props.providerType}
networkId={this.props.networkId}
blockchain={this._blockchain}
dispatcher={this.props.dispatcher}
toggleDialogFn={this.onToggleLedgerDialog.bind(this)}
isOpen={this.state.isLedgerDialogOpen}
/>
)}
</div>
<Footer />
<Footer />;
</div>
);
}
public onToggleLedgerDialog() {
this.setState({
isLedgerDialogOpen: !this.state.isLedgerDialogOpen,
});
}
private _renderEthWrapper() {
return (
<EthWrappers
@ -251,9 +275,9 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
blockchain={this._blockchain}
dispatcher={this.props.dispatcher}
tokenByAddress={this.props.tokenByAddress}
tokenStateByAddress={this.props.tokenStateByAddress}
userAddress={this.props.userAddress}
userEtherBalance={this.props.userEtherBalance}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
/>
);
}
@ -267,6 +291,8 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
);
}
private _renderTokenBalances() {
const allTokens = _.values(this.props.tokenByAddress);
const trackedTokens = _.filter(allTokens, t => t.isTracked);
return (
<TokenBalances
blockchain={this._blockchain}
@ -275,10 +301,11 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
dispatcher={this.props.dispatcher}
screenWidth={this.props.screenWidth}
tokenByAddress={this.props.tokenByAddress}
tokenStateByAddress={this.props.tokenStateByAddress}
trackedTokens={trackedTokens}
userAddress={this.props.userAddress}
userEtherBalance={this.props.userEtherBalance}
networkId={this.props.networkId}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
/>
);
}
@ -296,8 +323,8 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
networkId={this.props.networkId}
userAddress={this.props.userAddress}
tokenByAddress={this.props.tokenByAddress}
tokenStateByAddress={this.props.tokenStateByAddress}
dispatcher={this.props.dispatcher}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
/>
);
}
@ -353,9 +380,4 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
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

@ -5,16 +5,19 @@ import * as React from 'react';
import { Blockchain } from 'ts/blockchain';
import { SendDialog } from 'ts/components/dialogs/send_dialog';
import { Dispatcher } from 'ts/redux/dispatcher';
import { BlockchainCallErrs, Token, TokenState } from 'ts/types';
import { BlockchainCallErrs, Token } from 'ts/types';
import { errorReporter } from 'ts/utils/error_reporter';
import { utils } from 'ts/utils/utils';
interface SendButtonProps {
userAddress: string;
networkId: number;
token: Token;
tokenState: TokenState;
dispatcher: Dispatcher;
blockchain: Blockchain;
onError: () => void;
lastForceTokenStateRefetch: number;
refetchTokenStateAsync: (tokenAddress: string) => Promise<void>;
}
interface SendButtonState {
@ -42,11 +45,14 @@ export class SendButton extends React.Component<SendButtonProps, SendButtonState
onClick={this._toggleSendDialog.bind(this)}
/>
<SendDialog
blockchain={this.props.blockchain}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
isOpen={this.state.isSendDialogVisible}
onComplete={this._onSendAmountSelectedAsync.bind(this)}
onCancelled={this._toggleSendDialog.bind(this)}
token={this.props.token}
tokenState={this.props.tokenState}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
/>
</div>
);
@ -62,18 +68,15 @@ export class SendButton extends React.Component<SendButtonProps, SendButtonState
});
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);
await this.props.refetchTokenStateAsync(token.address);
} catch (err) {
const errMsg = `${err}`;
if (_.includes(errMsg, BlockchainCallErrs.UserHasNoAssociatedAddresses)) {
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
return;
} else if (!_.includes(errMsg, 'User denied transaction')) {
} else if (!utils.didUserDenyWeb3Request(errMsg)) {
utils.consoleLog(`Unexpected error encountered: ${err}`);
utils.consoleLog(err.stack);
this.props.onError();

View File

@ -27,11 +27,11 @@ import {
BlockchainCallErrs,
BlockchainErrs,
EtherscanLinkSuffixes,
Networks,
ScreenWidths,
Styles,
Token,
TokenByAddress,
TokenStateByAddress,
TokenVisibility,
} from 'ts/types';
import { colors } from 'ts/utils/colors';
@ -58,6 +58,14 @@ const styles: Styles = {
},
};
interface TokenStateByAddress {
[address: string]: {
balance: BigNumber;
allowance: BigNumber;
isLoaded: boolean;
};
}
interface TokenBalancesProps {
blockchain: Blockchain;
blockchainErr: BlockchainErrs;
@ -65,10 +73,11 @@ interface TokenBalancesProps {
dispatcher: Dispatcher;
screenWidth: ScreenWidths;
tokenByAddress: TokenByAddress;
tokenStateByAddress: TokenStateByAddress;
trackedTokens: Token[];
userAddress: string;
userEtherBalance: BigNumber;
networkId: number;
lastForceTokenStateRefetch: number;
}
interface TokenBalancesState {
@ -76,14 +85,17 @@ interface TokenBalancesState {
isBalanceSpinnerVisible: boolean;
isDharmaDialogVisible: boolean;
isZRXSpinnerVisible: boolean;
currentZrxBalance?: BigNumber;
isTokenPickerOpen: boolean;
isAddingToken: boolean;
trackedTokenStateByAddress: TokenStateByAddress;
}
export class TokenBalances extends React.Component<TokenBalancesProps, TokenBalancesState> {
private _isUnmounted: boolean;
public constructor(props: TokenBalancesProps) {
super(props);
this._isUnmounted = false;
const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(props.trackedTokens);
this.state = {
errorType: undefined,
isBalanceSpinnerVisible: false,
@ -91,8 +103,17 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
isDharmaDialogVisible: DharmaLoanFrame.isAuthTokenPresent(),
isTokenPickerOpen: false,
isAddingToken: false,
trackedTokenStateByAddress: initialTrackedTokenStateByAddress,
};
}
public componentWillMount() {
const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
// tslint:disable-next-line:no-floating-promises
this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
}
public componentWillUnmount() {
this._isUnmounted = true;
}
public componentWillReceiveProps(nextProps: TokenBalancesProps) {
if (nextProps.userEtherBalance !== this.props.userEtherBalance) {
if (this.state.isBalanceSpinnerVisible) {
@ -103,18 +124,36 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
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, constants.DECIMAL_PLACES_ZRX);
this.props.dispatcher.showFlashMessage(`Received ${receiveAmountInUnits.toString(10)} Kovan ZRX`);
if (
nextProps.userAddress !== this.props.userAddress ||
nextProps.networkId !== this.props.networkId ||
nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch
) {
const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
// tslint:disable-next-line:no-floating-promises
this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
}
if (!_.isEqual(nextProps.trackedTokens, this.props.trackedTokens)) {
const newTokens = _.difference(nextProps.trackedTokens, this.props.trackedTokens);
const newTokenAddresses = _.map(newTokens, token => token.address);
// Add placeholder entry for this token to the state, since fetching the
// balance/allowance is asynchronous
const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
for (const tokenAddress of newTokenAddresses) {
trackedTokenStateByAddress[tokenAddress] = {
balance: new BigNumber(0),
allowance: new BigNumber(0),
isLoaded: false,
};
}
this.setState({
isZRXSpinnerVisible: false,
currentZrxBalance: undefined,
trackedTokenStateByAddress,
});
// Fetch the actual balance/allowance.
// tslint:disable-next-line:no-floating-promises
this._fetchBalancesAndAllowancesAsync(newTokenAddresses);
}
}
public componentDidMount() {
@ -137,13 +176,13 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
onTouchTap={this._onDharmaDialogToggle.bind(this, false)}
/>,
];
const isTestNetwork = this.props.networkId === constants.NETWORK_ID_TESTNET;
const isKovanTestNetwork = this.props.networkId === constants.NETWORK_ID_KOVAN;
const dharmaButtonColumnStyle = {
paddingLeft: 3,
display: isTestNetwork ? 'table-cell' : 'none',
display: isKovanTestNetwork ? 'table-cell' : 'none',
};
const stubColumnStyle = {
display: isTestNetwork ? 'none' : 'table-cell',
display: isKovanTestNetwork ? 'none' : 'table-cell',
};
const allTokenRowHeight = _.size(this.props.tokenByAddress) * TOKEN_TABLE_ROW_HEIGHT;
const tokenTableHeight =
@ -162,10 +201,10 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
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>
<h3>{isKovanTestNetwork ? 'Test ether' : 'Ether'}</h3>
<Divider />
<div className="pt2 pb2">
{isTestNetwork
{isKovanTestNetwork
? '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. \
@ -177,12 +216,12 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
<TableHeaderColumn>Currency</TableHeaderColumn>
<TableHeaderColumn>Balance</TableHeaderColumn>
<TableRowColumn className="sm-hide xs-hide" style={stubColumnStyle} />
{isTestNetwork && (
{isKovanTestNetwork && (
<TableHeaderColumn style={{ paddingLeft: 3 }}>
{isSmallScreen ? 'Faucet' : 'Request from faucet'}
</TableHeaderColumn>
)}
{isTestNetwork && (
{isKovanTestNetwork && (
<TableHeaderColumn style={dharmaButtonColumnStyle}>
{isSmallScreen ? 'Loan' : 'Request Dharma loan'}
<HelpTooltip style={{ paddingLeft: 4 }} explanation={dharmaLoanExplanation} />
@ -204,7 +243,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
)}
</TableRowColumn>
<TableRowColumn className="sm-hide xs-hide" style={stubColumnStyle} />
{isTestNetwork && (
{isKovanTestNetwork && (
<TableRowColumn style={{ paddingLeft: 3 }}>
<LifeCycleRaisedButton
labelReady="Request"
@ -214,7 +253,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
/>
</TableRowColumn>
)}
{isTestNetwork && (
{isKovanTestNetwork && (
<TableRowColumn style={dharmaButtonColumnStyle}>
<RaisedButton
label="Request"
@ -228,7 +267,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
</Table>
<div className="clearfix" style={{ paddingBottom: 1 }}>
<div className="col col-10">
<h3 className="pt2">{isTestNetwork ? 'Test tokens' : 'Tokens'}</h3>
<h3 className="pt2">{isKovanTestNetwork ? 'Test tokens' : 'Tokens'}</h3>
</div>
<div className="col col-1 pt3 align-right">
<FloatingActionButton mini={true} zDepth={0} onClick={this._onAddTokenClicked.bind(this)}>
@ -243,7 +282,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
</div>
<Divider />
<div className="pt2 pb2">
{isTestNetwork
{isKovanTestNetwork
? "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>
@ -303,8 +342,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
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 trackedTokens = this.props.trackedTokens;
const trackedTokensStartingWithEtherToken = trackedTokens.sort(
firstBy((t: Token) => t.symbol !== ETHER_TOKEN_SYMBOL)
.thenBy((t: Token) => t.symbol !== ZRX_TOKEN_SYMBOL)
@ -317,7 +355,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
return tableRows;
}
private _renderTokenRow(tokenColSpan: number, actionPaddingX: number, token: Token) {
const tokenState = this.props.tokenStateByAddress[token.address];
const tokenState = this.state.trackedTokenStateByAddress[token.address];
const tokenLink = utils.getEtherScanLinkIfExists(
token.address,
this.props.networkId,
@ -338,13 +376,19 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
)}
</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>
)}
{tokenState.isLoaded ? (
<span>
{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>
)}
</span>
) : (
<i className="zmdi zmdi-spinner zmdi-hc-spin" />
)}
</TableRowColumn>
<TableRowColumn>
<AllowanceToggle
@ -354,6 +398,8 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
tokenState={tokenState}
onErrorOccurred={this._onErrorOccurred.bind(this)}
userAddress={this.props.userAddress}
isDisabled={!tokenState.isLoaded}
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)}
/>
</TableRowColumn>
<TableRowColumn style={{ paddingLeft: actionPaddingX, paddingRight: actionPaddingX }}>
@ -366,7 +412,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
/>
)}
{token.symbol === ZRX_TOKEN_SYMBOL &&
this.props.networkId === constants.NETWORK_ID_TESTNET && (
this.props.networkId === constants.NETWORK_ID_KOVAN && (
<LifeCycleRaisedButton
labelReady="Request"
labelLoading="Sending..."
@ -383,11 +429,14 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
}}
>
<SendButton
userAddress={this.props.userAddress}
networkId={this.props.networkId}
blockchain={this.props.blockchain}
dispatcher={this.props.dispatcher}
token={token}
tokenState={tokenState}
onError={this._onSendFailed.bind(this)}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)}
/>
</TableRowColumn>
)}
@ -414,7 +463,6 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
} 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`);
@ -449,9 +497,9 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
case BalanceErrs.incorrectNetworkForFaucet:
return (
<div>
Our faucet can only send test Ether to addresses on the {constants.TESTNET_NAME} testnet
(networkId {constants.NETWORK_ID_TESTNET}). Please make sure you are connected to the{' '}
{constants.TESTNET_NAME} testnet and try requesting ether again.
Our faucet can only send test Ether to addresses on the {Networks.Kovan} testnet (networkId{' '}
{constants.NETWORK_ID_KOVAN}). Please make sure you are connected to the {Networks.Kovan}{' '}
testnet and try requesting ether again.
</div>
);
@ -510,6 +558,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
private async _onMintTestTokensAsync(token: Token): Promise<boolean> {
try {
await this.props.blockchain.mintTestTokensAsync(token);
await this._refetchTokenStateAsync(token.address);
const amount = ZeroEx.toUnitAmount(constants.MINT_AMOUNT, token.decimals);
this.props.dispatcher.showFlashMessage(`Successfully minted ${amount.toString(10)} ${token.symbol}`);
return true;
@ -519,7 +568,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
return false;
}
if (_.includes(errMsg, 'User denied transaction')) {
if (utils.didUserDenyWeb3Request(errMsg)) {
return false;
}
utils.consoleLog(`Unexpected error encountered: ${err}`);
@ -539,7 +588,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
// 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.NETWORK_ID_TESTNET) {
if (this.props.blockchain.networkId !== constants.NETWORK_ID_KOVAN) {
this.setState({
errorType: BalanceErrs.incorrectNetworkForFaucet,
});
@ -569,15 +618,11 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
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,
});
// tslint:disable-next-line:no-floating-promises
this.props.blockchain.pollTokenBalanceAsync(zrxToken);
this._startPollingZrxBalanceAsync();
}
return true;
}
@ -603,4 +648,65 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
isAddingToken: false,
});
}
private async _startPollingZrxBalanceAsync() {
const tokens = _.values(this.props.tokenByAddress);
const zrxToken = _.find(tokens, t => t.symbol === ZRX_TOKEN_SYMBOL);
// tslint:disable-next-line:no-floating-promises
const balance = await this.props.blockchain.pollTokenBalanceAsync(zrxToken);
const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
trackedTokenStateByAddress[zrxToken.address] = {
...trackedTokenStateByAddress[zrxToken.address],
balance,
};
this.setState({
isZRXSpinnerVisible: false,
});
}
private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]) {
const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
for (const tokenAddress of tokenAddresses) {
const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
this.props.userAddress,
tokenAddress,
);
trackedTokenStateByAddress[tokenAddress] = {
balance,
allowance,
isLoaded: true,
};
}
if (!this._isUnmounted) {
this.setState({
trackedTokenStateByAddress,
});
}
}
private _getInitialTrackedTokenStateByAddress(trackedTokens: Token[]) {
const trackedTokenStateByAddress: TokenStateByAddress = {};
_.each(trackedTokens, token => {
trackedTokenStateByAddress[token.address] = {
balance: new BigNumber(0),
allowance: new BigNumber(0),
isLoaded: false,
};
});
return trackedTokenStateByAddress;
}
private async _refetchTokenStateAsync(tokenAddress: string) {
const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
this.props.userAddress,
tokenAddress,
);
this.setState({
trackedTokenStateByAddress: {
...this.state.trackedTokenStateByAddress,
[tokenAddress]: {
balance,
allowance,
isLoaded: true,
},
},
});
}
} // tslint:disable:max-file-line-count

View File

@ -0,0 +1,148 @@
import * as _ from 'lodash';
import RaisedButton from 'material-ui/RaisedButton';
import * as React from 'react';
import { Blockchain } from 'ts/blockchain';
import { ProviderPicker } from 'ts/components/top_bar/provider_picker';
import { DropDown } from 'ts/components/ui/drop_down';
import { Identicon } from 'ts/components/ui/identicon';
import { Dispatcher } from 'ts/redux/dispatcher';
import { ProviderType } from 'ts/types';
import { colors } from 'ts/utils/colors';
import { constants } from 'ts/utils/constants';
import { utils } from 'ts/utils/utils';
const IDENTICON_DIAMETER = 32;
interface ProviderDisplayProps {
dispatcher: Dispatcher;
userAddress: string;
networkId: number;
injectedProviderName: string;
providerType: ProviderType;
onToggleLedgerDialog: () => void;
blockchain: Blockchain;
}
interface ProviderDisplayState {}
export class ProviderDisplay extends React.Component<ProviderDisplayProps, ProviderDisplayState> {
public render() {
const isAddressAvailable = !_.isEmpty(this.props.userAddress);
const isExternallyInjectedProvider = ProviderType.Injected && this.props.injectedProviderName !== '0x Public';
const displayAddress = isAddressAvailable
? utils.getAddressBeginAndEnd(this.props.userAddress)
: isExternallyInjectedProvider ? 'Account locked' : '0x0000...0000';
// If the "injected" provider is our fallback public node, then we want to
// show the "connect a wallet" message instead of the providerName
const injectedProviderName = isExternallyInjectedProvider
? this.props.injectedProviderName
: 'Connect a wallet';
const providerTitle =
this.props.providerType === ProviderType.Injected ? injectedProviderName : 'Ledger Nano S';
const hoverActiveNode = (
<div className="flex right lg-pr0 md-pr2 sm-pr2" style={{ paddingTop: 16 }}>
<div>
<Identicon address={this.props.userAddress} diameter={IDENTICON_DIAMETER} />
</div>
<div style={{ marginLeft: 12, paddingTop: 1 }}>
<div style={{ fontSize: 12, color: colors.amber800 }}>{providerTitle}</div>
<div style={{ fontSize: 14 }}>{displayAddress}</div>
</div>
<div
style={{ borderLeft: `1px solid ${colors.grey300}`, marginLeft: 17, paddingTop: 1 }}
className="px2"
>
<i style={{ fontSize: 30, color: colors.grey300 }} className="zmdi zmdi zmdi-chevron-down" />
</div>
</div>
);
const hasInjectedProvider =
this.props.injectedProviderName !== '0x Public' && this.props.providerType === ProviderType.Injected;
const hasLedgerProvider = this.props.providerType === ProviderType.Ledger;
const horizontalPosition = hasInjectedProvider || hasLedgerProvider ? 'left' : 'middle';
return (
<div style={{ width: 'fit-content', height: 48, float: 'right' }}>
<DropDown
hoverActiveNode={hoverActiveNode}
popoverContent={this.renderPopoverContent(hasInjectedProvider, hasLedgerProvider)}
anchorOrigin={{ horizontal: horizontalPosition, vertical: 'bottom' }}
targetOrigin={{ horizontal: horizontalPosition, vertical: 'top' }}
zDepth={1}
/>
</div>
);
}
public renderPopoverContent(hasInjectedProvider: boolean, hasLedgerProvider: boolean) {
if (hasInjectedProvider || hasLedgerProvider) {
return (
<ProviderPicker
dispatcher={this.props.dispatcher}
networkId={this.props.networkId}
injectedProviderName={this.props.injectedProviderName}
providerType={this.props.providerType}
onToggleLedgerDialog={this.props.onToggleLedgerDialog}
blockchain={this.props.blockchain}
/>
);
} else {
// Nothing to connect to, show install/info popover
return (
<div className="px2" style={{ maxWidth: 420 }}>
<div className="center h4 py2" style={{ color: colors.grey700 }}>
Choose a wallet:
</div>
<div className="flex pb3">
<div className="center px2">
<div style={{ color: colors.darkGrey }}>Install a browser wallet</div>
<div className="py2">
<img src="/images/metamask_or_parity.png" width="135" />
</div>
<div>
Use{' '}
<a
href={constants.URL_METAMASK_CHROME_STORE}
target="_blank"
style={{ color: colors.lightBlueA700 }}
>
Metamask
</a>{' '}
or{' '}
<a
href={constants.URL_PARITY_CHROME_STORE}
target="_blank"
style={{ color: colors.lightBlueA700 }}
>
Parity Signer
</a>
</div>
</div>
<div>
<div
className="pl1 ml1"
style={{ borderLeft: `1px solid ${colors.grey300}`, height: 65 }}
/>
<div className="py1">or</div>
<div
className="pl1 ml1"
style={{ borderLeft: `1px solid ${colors.grey300}`, height: 68 }}
/>
</div>
<div className="px2 center">
<div style={{ color: colors.darkGrey }}>Connect to a ledger hardware wallet</div>
<div style={{ paddingTop: 21, paddingBottom: 29 }}>
<img src="/images/ledger_icon.png" style={{ width: 80 }} />
</div>
<div>
<RaisedButton
style={{ width: '100%' }}
label="Use Ledger"
onClick={this.props.onToggleLedgerDialog}
/>
</div>
</div>
</div>
</div>
);
}
}
}

View File

@ -0,0 +1,81 @@
import * as _ from 'lodash';
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
import * as React from 'react';
import { Blockchain } from 'ts/blockchain';
import { Dispatcher } from 'ts/redux/dispatcher';
import { ProviderType } from 'ts/types';
import { colors } from 'ts/utils/colors';
import { constants } from 'ts/utils/constants';
interface ProviderPickerProps {
networkId: number;
injectedProviderName: string;
providerType: ProviderType;
onToggleLedgerDialog: () => void;
dispatcher: Dispatcher;
blockchain: Blockchain;
}
interface ProviderPickerState {}
export class ProviderPicker extends React.Component<ProviderPickerProps, ProviderPickerState> {
public render() {
const isLedgerSelected = this.props.providerType === ProviderType.Ledger;
const menuStyle = {
padding: 10,
paddingTop: 15,
paddingBottom: 15,
};
// Show dropdown with two options
return (
<div style={{ width: 225, overflow: 'hidden' }}>
<RadioButtonGroup name="provider" defaultSelected={this.props.providerType}>
<RadioButton
onClick={this._onProviderRadioChanged.bind(this, ProviderType.Injected)}
style={{ ...menuStyle, backgroundColor: !isLedgerSelected && colors.grey50 }}
value={ProviderType.Injected}
label={this._renderLabel(this.props.injectedProviderName, !isLedgerSelected)}
/>
<RadioButton
onClick={this._onProviderRadioChanged.bind(this, ProviderType.Ledger)}
style={{ ...menuStyle, backgroundColor: isLedgerSelected && colors.grey50 }}
value={ProviderType.Ledger}
label={this._renderLabel('Ledger Nano S', isLedgerSelected)}
/>
</RadioButtonGroup>
</div>
);
}
private _renderLabel(title: string, shouldShowNetwork: boolean) {
const label = (
<div className="flex">
<div style={{ fontSize: 14 }}>{title}</div>
{shouldShowNetwork && this._renderNetwork()}
</div>
);
return label;
}
private _renderNetwork() {
const networkName = constants.NETWORK_NAME_BY_ID[this.props.networkId];
return (
<div className="flex" style={{ marginTop: 1 }}>
<div className="relative" style={{ width: 14, paddingLeft: 14 }}>
<img
src={`/images/network_icons/${networkName.toLowerCase()}.png`}
className="absolute"
style={{ top: 6, width: 10 }}
/>
</div>
<div style={{ color: colors.lightGrey, fontSize: 11 }}>{networkName}</div>
</div>
);
}
private _onProviderRadioChanged(value: string) {
if (value === ProviderType.Ledger) {
this.props.onToggleLedgerDialog();
} else {
// tslint:disable-next-line:no-floating-promises
this.props.blockchain.updateProviderToInjectedAsync();
}
}
}

View File

@ -1,21 +1,31 @@
import * as _ from 'lodash';
import Drawer from 'material-ui/Drawer';
import Menu from 'material-ui/Menu';
import MenuItem from 'material-ui/MenuItem';
import * as React from 'react';
import { Link } from 'react-router-dom';
import ReactTooltip = require('react-tooltip');
import { Blockchain } from 'ts/blockchain';
import { PortalMenu } from 'ts/components/portal_menu';
import { TopBarMenuItem } from 'ts/components/top_bar_menu_item';
import { DropDownMenuItem } from 'ts/components/ui/drop_down_menu_item';
import { ProviderDisplay } from 'ts/components/top_bar/provider_display';
import { TopBarMenuItem } from 'ts/components/top_bar/top_bar_menu_item';
import { DropDown } from 'ts/components/ui/drop_down';
import { Identicon } from 'ts/components/ui/identicon';
import { DocsInfo } from 'ts/pages/documentation/docs_info';
import { NestedSidebarMenu } from 'ts/pages/shared/nested_sidebar_menu';
import { DocsMenu, MenuSubsectionsBySection, Styles, WebsitePaths } from 'ts/types';
import { Dispatcher } from 'ts/redux/dispatcher';
import { DocsMenu, MenuSubsectionsBySection, ProviderType, Styles, WebsitePaths } from 'ts/types';
import { colors } from 'ts/utils/colors';
import { constants } from 'ts/utils/constants';
interface TopBarProps {
userAddress?: string;
networkId?: number;
injectedProviderName?: string;
providerType?: ProviderType;
onToggleLedgerDialog?: () => void;
blockchain?: Blockchain;
dispatcher?: Dispatcher;
blockchainIsLoaded: boolean;
location: Location;
docsVersion?: string;
@ -125,6 +135,15 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
cursor: 'pointer',
paddingTop: 16,
};
const hoverActiveNode = (
<div className="flex relative" style={{ color: menuIconStyle.color }}>
<div style={{ paddingRight: 10 }}>Developers</div>
<div className="absolute" style={{ paddingLeft: 3, right: 3, top: -2 }}>
<i className="zmdi zmdi-caret-right" style={{ fontSize: 22 }} />
</div>
</div>
);
const popoverContent = <Menu style={{ color: colors.darkGrey }}>{developerSectionMenuItems}</Menu>;
return (
<div style={{ ...styles.topBar, ...bottomBorderStyle, ...this.props.style }} className="pb1">
<div className={parentClassNames}>
@ -138,11 +157,12 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
{!this._isViewingPortal() && (
<div className={menuClasses}>
<div className="flex justify-between">
<DropDownMenuItem
title="Developers"
subMenuItems={developerSectionMenuItems}
<DropDown
hoverActiveNode={hoverActiveNode}
popoverContent={popoverContent}
anchorOrigin={{ horizontal: 'middle', vertical: 'bottom' }}
targetOrigin={{ horizontal: 'middle', vertical: 'top' }}
style={styles.menuItem}
isNightVersion={isNightVersion}
/>
<TopBarMenuItem
title="Wiki"
@ -167,10 +187,19 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
</div>
</div>
)}
{this.props.blockchainIsLoaded &&
!_.isEmpty(this.props.userAddress) && (
<div className="col col-5 sm-hide xs-hide">{this._renderUser()}</div>
)}
{this.props.blockchainIsLoaded && (
<div className="sm-hide xs-hide col col-5">
<ProviderDisplay
dispatcher={this.props.dispatcher}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
injectedProviderName={this.props.injectedProviderName}
providerType={this.props.providerType}
onToggleLedgerDialog={this.props.onToggleLedgerDialog}
blockchain={this.props.blockchain}
/>
</div>
)}
<div className={`col ${isFullWidthPage ? 'col-2 pl2' : 'col-1'} md-hide lg-hide`}>
<div style={menuIconStyle}>
<i className="zmdi zmdi-menu" onClick={this._onMenuButtonClick.bind(this)} />

View File

@ -1,36 +1,35 @@
import * as _ from 'lodash';
import Menu from 'material-ui/Menu';
import Popover from 'material-ui/Popover';
import Popover, { PopoverAnimationVertical } from 'material-ui/Popover';
import * as React from 'react';
import { colors } from 'ts/utils/colors';
import { MaterialUIPosition } from 'ts/types';
const CHECK_CLOSE_POPOVER_INTERVAL_MS = 300;
const DEFAULT_STYLE = {
fontSize: 14,
};
interface DropDownMenuItemProps {
title: string;
subMenuItems: React.ReactNode[];
interface DropDownProps {
hoverActiveNode: React.ReactNode;
popoverContent: React.ReactNode;
anchorOrigin: MaterialUIPosition;
targetOrigin: MaterialUIPosition;
style?: React.CSSProperties;
menuItemStyle?: React.CSSProperties;
isNightVersion?: boolean;
zDepth?: number;
}
interface DropDownMenuItemState {
interface DropDownState {
isDropDownOpen: boolean;
anchorEl?: HTMLInputElement;
}
export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, DropDownMenuItemState> {
public static defaultProps: Partial<DropDownMenuItemProps> = {
export class DropDown extends React.Component<DropDownProps, DropDownState> {
public static defaultProps: Partial<DropDownProps> = {
style: DEFAULT_STYLE,
menuItemStyle: DEFAULT_STYLE,
isNightVersion: false,
zDepth: 1,
};
private _isHovering: boolean;
private _popoverCloseCheckIntervalId: number;
constructor(props: DropDownMenuItemProps) {
constructor(props: DropDownProps) {
super(props);
this.state = {
isDropDownOpen: false,
@ -44,30 +43,35 @@ export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, Dro
public componentWillUnmount() {
window.clearInterval(this._popoverCloseCheckIntervalId);
}
public componentWillReceiveProps(nextProps: DropDownProps) {
// HACK: If the popoverContent is updated to a different dimension and the users
// mouse is no longer above it, the dropdown can enter an inconsistent state where
// it believes the user is still hovering over it. In order to remedy this, we
// call hoverOff whenever the dropdown receives updated props. This is a hack
// because it will effectively close the dropdown on any prop update, barring
// dropdowns from having dynamic content.
this._onHoverOff();
}
public render() {
const colorStyle = this.props.isNightVersion ? 'white' : this.props.style.color;
return (
<div
style={{ ...this.props.style, color: colorStyle }}
style={{ ...this.props.style, width: 'fit-content', height: '100%' }}
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>
{this.props.hoverActiveNode}
<Popover
open={this.state.isDropDownOpen}
anchorEl={this.state.anchorEl}
anchorOrigin={{ horizontal: 'middle', vertical: 'bottom' }}
targetOrigin={{ horizontal: 'middle', vertical: 'top' }}
anchorOrigin={this.props.anchorOrigin}
targetOrigin={this.props.targetOrigin}
onRequestClose={this._closePopover.bind(this)}
useLayerForClickAway={false}
animation={PopoverAnimationVertical}
zDepth={this.props.zDepth}
>
<div onMouseEnter={this._onHover.bind(this)} onMouseLeave={this._onHoverOff.bind(this)}>
<Menu style={{ color: colors.grey }}>{this.props.subMenuItems}</Menu>
{this.props.popoverContent}
</div>
</Popover>
</div>
@ -87,7 +91,7 @@ export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, Dro
anchorEl: event.currentTarget,
});
}
private _onHoverOff(event: React.FormEvent<HTMLInputElement>) {
private _onHoverOff() {
this._isHovering = false;
}
private _checkIfShouldClosePopover() {

View File

@ -1,39 +0,0 @@
import * as _ from 'lodash';
import Paper from 'material-ui/Paper';
import * as React from 'react';
import { DefaultPlayer as Video } from 'react-html5video';
import 'react-html5video/dist/styles.css';
import { utils } from 'ts/utils/utils';
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

@ -6,14 +6,7 @@ import { Blockchain } from 'ts/blockchain';
import { GenerateOrderForm as GenerateOrderFormComponent } from 'ts/components/generate_order/generate_order_form';
import { Dispatcher } from 'ts/redux/dispatcher';
import { State } from 'ts/redux/reducer';
import {
BlockchainErrs,
HashData,
SideToAssetToken,
SignatureData,
TokenByAddress,
TokenStateByAddress,
} from 'ts/types';
import { BlockchainErrs, HashData, SideToAssetToken, SignatureData, TokenByAddress } from 'ts/types';
interface GenerateOrderFormProps {
blockchain: Blockchain;
@ -32,7 +25,7 @@ interface ConnectedState {
networkId: number;
sideToAssetToken: SideToAssetToken;
tokenByAddress: TokenByAddress;
tokenStateByAddress: TokenStateByAddress;
lastForceTokenStateRefetch: number;
}
const mapStateToProps = (state: State, ownProps: GenerateOrderFormProps): ConnectedState => ({
@ -45,8 +38,8 @@ const mapStateToProps = (state: State, ownProps: GenerateOrderFormProps): Connec
networkId: state.networkId,
sideToAssetToken: state.sideToAssetToken,
tokenByAddress: state.tokenByAddress,
tokenStateByAddress: state.tokenStateByAddress,
userAddress: state.userAddress,
lastForceTokenStateRefetch: state.lastForceTokenStateRefetch,
});
export const GenerateOrderForm: React.ComponentClass<GenerateOrderFormProps> = connect(mapStateToProps)(

View File

@ -6,18 +6,20 @@ import { Dispatch } from 'redux';
import { Portal as PortalComponent, PortalAllProps as PortalComponentAllProps } from 'ts/components/portal';
import { Dispatcher } from 'ts/redux/dispatcher';
import { State } from 'ts/redux/reducer';
import { BlockchainErrs, HashData, Order, ScreenWidths, Side, TokenByAddress, TokenStateByAddress } from 'ts/types';
import { BlockchainErrs, HashData, Order, ProviderType, ScreenWidths, Side, TokenByAddress } from 'ts/types';
import { constants } from 'ts/utils/constants';
interface ConnectedState {
blockchainErr: BlockchainErrs;
blockchainIsLoaded: boolean;
hashData: HashData;
injectedProviderName: string;
networkId: number;
nodeVersion: string;
orderFillAmount: BigNumber;
providerType: ProviderType;
tokenByAddress: TokenByAddress;
tokenStateByAddress: TokenStateByAddress;
lastForceTokenStateRefetch: number;
userEtherBalance: BigNumber;
screenWidth: ScreenWidths;
shouldBlockchainErrDialogBeOpen: boolean;
@ -57,14 +59,16 @@ const mapStateToProps = (state: State, ownProps: PortalComponentAllProps): Conne
return {
blockchainErr: state.blockchainErr,
blockchainIsLoaded: state.blockchainIsLoaded,
hashData,
injectedProviderName: state.injectedProviderName,
networkId: state.networkId,
nodeVersion: state.nodeVersion,
orderFillAmount: state.orderFillAmount,
hashData,
providerType: state.providerType,
screenWidth: state.screenWidth,
shouldBlockchainErrDialogBeOpen: state.shouldBlockchainErrDialogBeOpen,
tokenByAddress: state.tokenByAddress,
tokenStateByAddress: state.tokenStateByAddress,
lastForceTokenStateRefetch: state.lastForceTokenStateRefetch,
userAddress: state.userAddress,
userEtherBalance: state.userEtherBalance,
userSuppliedOrderCache: state.userSuppliedOrderCache,

View File

@ -10,7 +10,6 @@ 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' {

View File

@ -1,6 +1,6 @@
import * as _ from 'lodash';
import { localStorage } from 'ts/local_storage/local_storage';
import { Token, TrackedTokensByUserAddress } from 'ts/types';
import { Token, TokenByAddress, TrackedTokensByUserAddress } from 'ts/types';
import { configs } from 'ts/utils/configs';
const TRACKED_TOKENS_KEY = 'trackedTokens';
@ -39,18 +39,22 @@ export const trackedTokenStorage = {
const trackedTokensByUserAddress = JSON.parse(trackedTokensJSONString);
return trackedTokensByUserAddress;
},
getTrackedTokensIfExists(userAddress: string, networkId: number): Token[] {
getTrackedTokensByAddress(userAddress: string, networkId: number): TokenByAddress {
const trackedTokensByAddress: TokenByAddress = {};
const trackedTokensJSONString = localStorage.getItemIfExists(TRACKED_TOKENS_KEY);
if (_.isEmpty(trackedTokensJSONString)) {
return undefined;
return trackedTokensByAddress;
}
const trackedTokensByUserAddress = JSON.parse(trackedTokensJSONString);
const trackedTokensByNetworkId = trackedTokensByUserAddress[userAddress];
if (_.isUndefined(trackedTokensByNetworkId)) {
return undefined;
return trackedTokensByAddress;
}
const trackedTokens = trackedTokensByNetworkId[networkId];
return trackedTokens;
_.each(trackedTokens, (trackedToken: Token) => {
trackedTokensByAddress[trackedToken.address] = trackedToken;
});
return trackedTokensByAddress;
},
removeTrackedToken(userAddress: string, networkId: number, tokenAddress: string): void {
const trackedTokensByUserAddress = this.getTrackedTokensByUserAddress();

View File

@ -2,7 +2,7 @@ import * as _ from 'lodash';
import * as React from 'react';
import * as DocumentTitle from 'react-document-title';
import { Footer } from 'ts/components/footer';
import { TopBar } from 'ts/components/top_bar';
import { TopBar } from 'ts/components/top_bar/top_bar';
import { Profile } from 'ts/pages/about/profile';
import { ProfileInfo, Styles } from 'ts/types';
import { colors } from 'ts/utils/colors';

View File

@ -5,7 +5,7 @@ import * as React from 'react';
import DocumentTitle = require('react-document-title');
import { scroller } from 'react-scroll';
import semverSort = require('semver-sort');
import { TopBar } from 'ts/components/top_bar';
import { TopBar } from 'ts/components/top_bar/top_bar';
import { Badge } from 'ts/components/ui/badge';
import { Comment } from 'ts/pages/documentation/comment';
import { DocsInfo } from 'ts/pages/documentation/docs_info';
@ -40,9 +40,9 @@ import { utils } from 'ts/utils/utils';
const SCROLL_TOP_ID = 'docsScrollTop';
const networkNameToColor: { [network: string]: string } = {
[Networks.kovan]: colors.purple,
[Networks.ropsten]: colors.red,
[Networks.mainnet]: colors.turquois,
[Networks.Kovan]: colors.purple,
[Networks.Ropsten]: colors.red,
[Networks.Mainnet]: colors.turquois,
};
export interface DocumentationAllProps {
@ -78,8 +78,10 @@ const styles: Styles = {
};
export class Documentation extends React.Component<DocumentationAllProps, DocumentationState> {
private _isUnmounted: boolean;
constructor(props: DocumentationAllProps) {
super(props);
this._isUnmounted = false;
this.state = {
docAgnosticFormat: undefined,
};
@ -92,6 +94,9 @@ export class Documentation extends React.Component<DocumentationAllProps, Docume
// tslint:disable-next-line:no-floating-promises
this._fetchJSONDocsFireAndForgetAsync(preferredVersionIfExists);
}
public componentWillUnmount() {
this._isUnmounted = true;
}
public render() {
const menuSubsectionsBySection = _.isUndefined(this.state.docAgnosticFormat)
? {}
@ -367,13 +372,15 @@ export class Documentation extends React.Component<DocumentationAllProps, Docume
);
const docAgnosticFormat = this.props.docsInfo.convertToDocAgnosticFormat(versionDocObj as DoxityDocObj);
this.setState(
{
docAgnosticFormat,
},
() => {
this._scrollToHash();
},
);
if (!this._isUnmounted) {
this.setState(
{
docAgnosticFormat,
},
() => {
this._scrollToHash();
},
);
}
}
}

View File

@ -2,7 +2,7 @@ import * as _ from 'lodash';
import * as React from 'react';
import * as DocumentTitle from 'react-document-title';
import { Footer } from 'ts/components/footer';
import { TopBar } from 'ts/components/top_bar';
import { TopBar } from 'ts/components/top_bar/top_bar';
import { Question } from 'ts/pages/faq/question';
import { FAQQuestion, FAQSection, Styles, WebsitePaths } from 'ts/types';
import { colors } from 'ts/utils/colors';

View File

@ -4,7 +4,7 @@ import * as React from 'react';
import DocumentTitle = require('react-document-title');
import { Link } from 'react-router-dom';
import { Footer } from 'ts/components/footer';
import { TopBar } from 'ts/components/top_bar';
import { TopBar } from 'ts/components/top_bar/top_bar';
import { ScreenWidths, WebsitePaths } from 'ts/types';
import { colors } from 'ts/utils/colors';
import { constants } from 'ts/utils/constants';

View File

@ -1,7 +1,7 @@
import * as _ from 'lodash';
import * as React from 'react';
import { Footer } from 'ts/components/footer';
import { TopBar } from 'ts/components/top_bar';
import { TopBar } from 'ts/components/top_bar/top_bar';
import { Styles } from 'ts/types';
export interface NotFoundProps {

View File

@ -3,7 +3,7 @@ import CircularProgress from 'material-ui/CircularProgress';
import * as React from 'react';
import DocumentTitle = require('react-document-title');
import { scroller } from 'react-scroll';
import { TopBar } from 'ts/components/top_bar';
import { TopBar } from 'ts/components/top_bar/top_bar';
import { MarkdownSection } from 'ts/pages/shared/markdown_section';
import { NestedSidebarMenu } from 'ts/pages/shared/nested_sidebar_menu';
import { SectionHeader } from 'ts/pages/shared/section_header';
@ -45,8 +45,10 @@ const styles: Styles = {
export class Wiki extends React.Component<WikiProps, WikiState> {
private _wikiBackoffTimeoutId: number;
private _isUnmounted: boolean;
constructor(props: WikiProps) {
super(props);
this._isUnmounted = false;
this.state = {
articlesBySection: undefined,
};
@ -56,6 +58,7 @@ export class Wiki extends React.Component<WikiProps, WikiState> {
this._fetchArticlesBySectionAsync();
}
public componentWillUnmount() {
this._isUnmounted = true;
clearTimeout(this._wikiBackoffTimeoutId);
}
public render() {
@ -179,14 +182,16 @@ export class Wiki extends React.Component<WikiProps, WikiState> {
return;
}
const articlesBySection = await response.json();
this.setState(
{
articlesBySection,
},
() => {
this._scrollToHash();
},
);
if (!this._isUnmounted) {
this.setState(
{
articlesBySection,
},
() => {
this._scrollToHash();
},
);
}
}
private _getMenuSubsectionsBySection(articlesBySection: ArticlesBySection) {
const sectionNames = _.keys(articlesBySection);

View File

@ -9,9 +9,10 @@ import {
ProviderType,
ScreenWidths,
Side,
SideToAssetToken,
SignatureData,
Token,
TokenStateByAddress,
TokenByAddress,
} from 'ts/types';
export class Dispatcher {
@ -120,9 +121,20 @@ export class Dispatcher {
type: ActionTypes.RemoveTokenFromTokenByAddress,
});
}
public clearTokenByAddress() {
public batchDispatch(
tokenByAddress: TokenByAddress,
networkId: number,
userAddress: string,
sideToAssetToken: SideToAssetToken,
) {
this._dispatch({
type: ActionTypes.ClearTokenByAddress,
data: {
tokenByAddress,
networkId,
userAddress,
sideToAssetToken,
},
type: ActionTypes.BatchDispatch,
});
}
public updateTokenByAddress(tokens: Token[]) {
@ -131,43 +143,9 @@ export class Dispatcher {
type: ActionTypes.UpdateTokenByAddress,
});
}
public updateTokenStateByAddress(tokenStateByAddress: TokenStateByAddress) {
public forceTokenStateRefetch() {
this._dispatch({
data: tokenStateByAddress,
type: ActionTypes.UpdateTokenStateByAddress,
});
}
public removeFromTokenStateByAddress(tokenAddress: string) {
this._dispatch({
data: tokenAddress,
type: ActionTypes.RemoveFromTokenStateByAddress,
});
}
public replaceTokenAllowanceByAddress(address: string, allowance: BigNumber) {
this._dispatch({
data: {
address,
allowance,
},
type: ActionTypes.ReplaceTokenAllowanceByAddress,
});
}
public replaceTokenBalanceByAddress(address: string, balance: BigNumber) {
this._dispatch({
data: {
address,
balance,
},
type: ActionTypes.ReplaceTokenBalanceByAddress,
});
}
public updateTokenBalanceByAddress(address: string, balanceDelta: BigNumber) {
this._dispatch({
data: {
address,
balanceDelta,
},
type: ActionTypes.UpdateTokenBalanceByAddress,
type: ActionTypes.ForceTokenStateRefetch,
});
}
public updateSignatureData(signatureData: SignatureData) {

View File

@ -1,6 +1,7 @@
import { ZeroEx } from '0x.js';
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import * as moment from 'moment';
import {
Action,
ActionTypes,
@ -12,8 +13,6 @@ import {
SideToAssetToken,
SignatureData,
TokenByAddress,
TokenState,
TokenStateByAddress,
} from 'ts/types';
import { utils } from 'ts/utils/utils';
@ -37,7 +36,7 @@ export interface State {
shouldBlockchainErrDialogBeOpen: boolean;
sideToAssetToken: SideToAssetToken;
tokenByAddress: TokenByAddress;
tokenStateByAddress: TokenStateByAddress;
lastForceTokenStateRefetch: number;
userAddress: string;
userEtherBalance: BigNumber;
// Note: cache of supplied orderJSON in fill order step. Do not use for anything else.
@ -76,7 +75,7 @@ const INITIAL_STATE: State = {
[Side.Receive]: {},
},
tokenByAddress: {},
tokenStateByAddress: {},
lastForceTokenStateRefetch: moment().unix(),
userAddress: '',
userEtherBalance: new BigNumber(0),
userSuppliedOrderCache: undefined,
@ -139,13 +138,6 @@ export function reducer(state: State = INITIAL_STATE, action: Action) {
};
}
case ActionTypes.ClearTokenByAddress: {
return {
...state,
tokenByAddress: {},
};
}
case ActionTypes.AddTokenToTokenByAddress: {
const newTokenByAddress = state.tokenByAddress;
newTokenByAddress[action.data.address] = action.data;
@ -180,74 +172,21 @@ export function reducer(state: State = INITIAL_STATE, action: Action) {
};
}
case ActionTypes.UpdateTokenStateByAddress: {
const tokenStateByAddress = state.tokenStateByAddress;
const updatedTokenStateByAddress = action.data;
_.each(updatedTokenStateByAddress, (tokenState: TokenState, address: string) => {
const updatedTokenState = {
...tokenStateByAddress[address],
...tokenState,
};
tokenStateByAddress[address] = updatedTokenState;
});
case ActionTypes.BatchDispatch: {
return {
...state,
tokenStateByAddress,
networkId: action.data.networkId,
userAddress: action.data.userAddress,
sideToAssetToken: action.data.sideToAssetToken,
tokenByAddress: action.data.tokenByAddress,
};
}
case ActionTypes.RemoveFromTokenStateByAddress: {
const tokenStateByAddress = state.tokenStateByAddress;
const tokenAddress = action.data;
delete tokenStateByAddress[tokenAddress];
case ActionTypes.ForceTokenStateRefetch:
return {
...state,
tokenStateByAddress,
lastForceTokenStateRefetch: moment().unix(),
};
}
case ActionTypes.ReplaceTokenAllowanceByAddress: {
const tokenStateByAddress = state.tokenStateByAddress;
const allowance = action.data.allowance;
const tokenAddress = action.data.address;
tokenStateByAddress[tokenAddress] = {
...tokenStateByAddress[tokenAddress],
allowance,
};
return {
...state,
tokenStateByAddress,
};
}
case ActionTypes.ReplaceTokenBalanceByAddress: {
const tokenStateByAddress = state.tokenStateByAddress;
const balance = action.data.balance;
const tokenAddress = action.data.address;
tokenStateByAddress[tokenAddress] = {
...tokenStateByAddress[tokenAddress],
balance,
};
return {
...state,
tokenStateByAddress,
};
}
case ActionTypes.UpdateTokenBalanceByAddress: {
const tokenStateByAddress = state.tokenStateByAddress;
const balanceDelta = action.data.balanceDelta;
const tokenAddress = action.data.address;
const currBalance = tokenStateByAddress[tokenAddress].balance;
tokenStateByAddress[tokenAddress] = {
...tokenStateByAddress[tokenAddress],
balance: currBalance.plus(balanceDelta),
};
return {
...state,
tokenStateByAddress,
};
}
case ActionTypes.UpdateOrderSignatureData: {
return {

View File

@ -25,10 +25,6 @@ export interface TokenState {
balance: BigNumber;
}
export interface TokenStateByAddress {
[address: string]: TokenState;
}
export interface AssetToken {
address?: string;
amount?: BigNumber;
@ -110,12 +106,12 @@ export enum BalanceErrs {
export enum ActionTypes {
// Portal
BatchDispatch = 'BATCH_DISPATCH',
UpdateScreenWidth = 'UPDATE_SCREEN_WIDTH',
UpdateNodeVersion = 'UPDATE_NODE_VERSION',
ResetState = 'RESET_STATE',
AddTokenToTokenByAddress = 'ADD_TOKEN_TO_TOKEN_BY_ADDRESS',
BlockchainErrEncountered = 'BLOCKCHAIN_ERR_ENCOUNTERED',
ClearTokenByAddress = 'CLEAR_TOKEN_BY_ADDRESS',
UpdateBlockchainIsLoaded = 'UPDATE_BLOCKCHAIN_IS_LOADED',
UpdateNetworkId = 'UPDATE_NETWORK_ID',
UpdateChosenAssetToken = 'UPDATE_CHOSEN_ASSET_TOKEN',
@ -125,11 +121,7 @@ export enum ActionTypes {
UpdateOrderSignatureData = 'UPDATE_ORDER_SIGNATURE_DATA',
UpdateTokenByAddress = 'UPDATE_TOKEN_BY_ADDRESS',
RemoveTokenFromTokenByAddress = 'REMOVE_TOKEN_FROM_TOKEN_BY_ADDRESS',
UpdateTokenStateByAddress = 'UPDATE_TOKEN_STATE_BY_ADDRESS',
RemoveFromTokenStateByAddress = 'REMOVE_FROM_TOKEN_STATE_BY_ADDRESS',
ReplaceTokenAllowanceByAddress = 'REPLACE_TOKEN_ALLOWANCE_BY_ADDRESS',
ReplaceTokenBalanceByAddress = 'REPLACE_TOKEN_BALANCE_BY_ADDRESS',
UpdateTokenBalanceByAddress = 'UPDATE_TOKEN_BALANCE_BY_ADDRESS',
ForceTokenStateRefetch = 'FORCE_TOKEN_STATE_REFETCH',
UpdateOrderExpiry = 'UPDATE_ORDER_EXPIRY',
SwapAssetTokens = 'SWAP_ASSET_TOKENS',
UpdateUserAddress = 'UPDATE_USER_ADDRESS',
@ -496,16 +488,6 @@ export interface SignPersonalMessageParams {
data: string;
}
export interface TxParams {
nonce: string;
gasPrice?: number;
gasLimit: string;
to: string;
value?: string;
data?: string;
chainId: number; // EIP 155 chainId - mainnet: 1, ropsten: 3
}
export interface PublicNodeUrlsByNetworkId {
[networkId: number]: string[];
}
@ -610,10 +592,10 @@ export interface AddressByContractName {
}
export enum Networks {
mainnet = 'Mainnet',
kovan = 'Kovan',
ropsten = 'Ropsten',
rinkeby = 'Rinkeby',
Mainnet = 'Mainnet',
Kovan = 'Kovan',
Ropsten = 'Ropsten',
Rinkeby = 'Rinkeby',
}
export enum AbiTypes {
@ -678,4 +660,9 @@ export enum SmartContractDocSections {
ZRXToken = 'ZRXToken',
}
export interface MaterialUIPosition {
vertical: 'bottom' | 'top' | 'center';
horizontal: 'left' | 'middle' | 'right';
}
// tslint:disable:max-file-line-count

View File

@ -16,24 +16,24 @@ const isDevelopment = _.includes(
const INFURA_API_KEY = 'T5WSC8cautR4KXyYgsRs';
export const configs = {
BACKEND_BASE_URL: isDevelopment ? 'https://localhost:3001' : 'https://website-api.0xproject.com',
BACKEND_BASE_URL: 'https://website-api.0xproject.com',
BASE_URL,
BITLY_ACCESS_TOKEN: 'ffc4c1a31e5143848fb7c523b39f91b9b213d208',
CONTRACT_ADDRESS: {
'1.0.0': {
[Networks.mainnet]: {
[Networks.Mainnet]: {
[SmartContractDocSections.Exchange]: '0x12459c951127e0c374ff9105dda097662a027093',
[SmartContractDocSections.TokenTransferProxy]: '0x8da0d80f5007ef1e431dd2127178d224e32c2ef4',
[SmartContractDocSections.ZRXToken]: '0xe41d2489571d322189246dafa5ebde1f4699f498',
[SmartContractDocSections.TokenRegistry]: '0x926a74c5c36adf004c87399e65f75628b0f98d2c',
},
[Networks.ropsten]: {
[Networks.Ropsten]: {
[SmartContractDocSections.Exchange]: '0x479cc461fecd078f766ecc58533d6f69580cf3ac',
[SmartContractDocSections.TokenTransferProxy]: '0x4e9aad8184de8833365fea970cd9149372fdf1e6',
[SmartContractDocSections.ZRXToken]: '0xa8e9fa8f91e5ae138c74648c9c304f1c75003a8d',
[SmartContractDocSections.TokenRegistry]: '0x6b1a50f0bb5a7995444bd3877b22dc89c62843ed',
},
[Networks.kovan]: {
[Networks.Kovan]: {
[SmartContractDocSections.Exchange]: '0x90fe2af704b34e0224bf2299c838e04d4dcf1364',
[SmartContractDocSections.TokenTransferProxy]: '0x087Eed4Bc1ee3DE49BeFbd66C662B434B15d49d4',
[SmartContractDocSections.ZRXToken]: '0x6ff6c0ff1d68b964901f986d4c9fa3ac68346570',
@ -120,6 +120,8 @@ export const configs = {
PUBLIC_NODE_URLS_BY_NETWORK_ID: {
[1]: [`https://mainnet.infura.io/${INFURA_API_KEY}`, 'https://mainnet.0xproject.com'],
[42]: [`https://kovan.infura.io/${INFURA_API_KEY}`, 'https://kovan.0xproject.com'],
[3]: [`https://ropsten.infura.io/${INFURA_API_KEY}`],
[4]: [`https://rinkeby.infura.io/${INFURA_API_KEY}`],
} as PublicNodeUrlsByNetworkId,
SHOULD_DEPRECATE_OLD_WETH_TOKEN: true,
SYMBOLS_OF_MINTABLE_TOKENS: ['MKR', 'MLN', 'GNT', 'DGD', 'REP'],

View File

@ -10,6 +10,8 @@ export const constants = {
1: 4145578,
42: 3117574,
50: 0,
3: 1719261,
4: 1570919,
} as { [networkId: number]: number },
HOME_SCROLL_DURATION_MS: 500,
HTTP_NO_CONTENT_STATUS_CODE: 204,
@ -19,19 +21,19 @@ export const constants = {
MAINNET_NAME: 'Main network',
MINT_AMOUNT: new BigNumber('100000000000000000000'),
NETWORK_ID_MAINNET: 1,
NETWORK_ID_TESTNET: 42,
NETWORK_ID_KOVAN: 42,
NETWORK_ID_TESTRPC: 50,
NETWORK_NAME_BY_ID: {
1: Networks.mainnet,
3: Networks.ropsten,
4: Networks.rinkeby,
42: Networks.kovan,
1: Networks.Mainnet,
3: Networks.Ropsten,
4: Networks.Rinkeby,
42: Networks.Kovan,
} as { [symbol: number]: string },
NETWORK_ID_BY_NAME: {
[Networks.mainnet]: 1,
[Networks.ropsten]: 3,
[Networks.rinkeby]: 4,
[Networks.kovan]: 42,
[Networks.Mainnet]: 1,
[Networks.Ropsten]: 3,
[Networks.Rinkeby]: 4,
[Networks.Kovan]: 42,
} as { [networkName: string]: number },
NULL_ADDRESS: '0x0000000000000000000000000000000000000000',
PROVIDER_NAME_LEDGER: 'Ledger',

View File

@ -8,6 +8,7 @@ export const muiTheme = getMuiTheme({
textColor: colors.black,
},
palette: {
accent1Color: colors.lightBlueA700,
pickerHeaderColor: colors.lightBlue,
primary1Color: colors.lightBlue,
primary2Color: colors.lightBlue,

View File

@ -151,7 +151,7 @@ export const utils = {
if (_.isUndefined(networkName)) {
return undefined;
}
const etherScanPrefix = networkName === Networks.mainnet ? '' : `${networkName.toLowerCase()}.`;
const etherScanPrefix = networkName === Networks.Mainnet ? '' : `${networkName.toLowerCase()}.`;
return `https://${etherScanPrefix}etherscan.io/${suffix}/${addressOrTxHash}`;
},
setUrlHash(anchorId: string) {
@ -183,7 +183,7 @@ export const utils = {
// after a user was prompted to sign a message or send a transaction and decided to
// reject the request.
didUserDenyWeb3Request(errMsg: string) {
const metamaskDenialErrMsg = 'User denied message';
const metamaskDenialErrMsg = 'User denied';
const paritySignerDenialErrMsg = 'Request has been rejected';
const ledgerDenialErrMsg = 'Invalid status 6985';
const isUserDeniedErrMsg =
@ -276,4 +276,10 @@ export const utils = {
exchangeContractErrorToHumanReadableError[error] || ZeroExErrorToHumanReadableError[error];
return humanReadableErrorMsg;
},
isParityNode(nodeVersion: string): boolean {
return _.includes(nodeVersion, 'Parity');
},
isTestRpc(nodeVersion: string): boolean {
return _.includes(nodeVersion, 'TestRPC');
},
};

View File

@ -24,9 +24,6 @@ export class Web3Wrapper {
this._web3 = new Web3();
this._web3.setProvider(provider);
// tslint:disable-next-line:no-floating-promises
this._startEmittingNetworkConnectionAndUserBalanceStateAsync();
}
public isAddress(address: string) {
return this._web3.isAddress(address);
@ -90,11 +87,7 @@ export class Web3Wrapper {
public updatePrevUserAddress(userAddress: string) {
this._prevUserAddress = userAddress;
}
private async _getNetworkAsync() {
const networkId = await promisify(this._web3.version.getNetwork)();
return networkId;
}
private async _startEmittingNetworkConnectionAndUserBalanceStateAsync() {
public startEmittingNetworkConnectionAndUserBalanceState() {
if (!_.isUndefined(this._watchNetworkAndBalanceIntervalId)) {
return; // we are already emitting the state
}
@ -127,7 +120,7 @@ export class Web3Wrapper {
}
// Check for user ether balance changes
if (userAddressIfExists !== '') {
if (!_.isEmpty(userAddressIfExists)) {
await this._updateUserEtherBalanceAsync(userAddressIfExists);
}
} else {
@ -140,11 +133,15 @@ export class Web3Wrapper {
},
5000,
(err: Error) => {
utils.consoleLog(`Watching network and balances failed: ${err}`);
utils.consoleLog(`Watching network and balances failed: ${err.stack}`);
this._stopEmittingNetworkConnectionAndUserBalanceStateAsync();
},
);
}
private async _getNetworkAsync() {
const networkId = await promisify(this._web3.version.getNetwork)();
return networkId;
}
private async _updateUserEtherBalanceAsync(userAddress: string) {
const balance = await this.getBalanceInEthAsync(userAddress);
if (!balance.eq(this._prevUserEtherBalanceInEth)) {
@ -153,6 +150,8 @@ export class Web3Wrapper {
}
}
private _stopEmittingNetworkConnectionAndUserBalanceStateAsync() {
intervalUtils.clearAsyncExcludingInterval(this._watchNetworkAndBalanceIntervalId);
if (!_.isUndefined(this._watchNetworkAndBalanceIntervalId)) {
intervalUtils.clearAsyncExcludingInterval(this._watchNetworkAndBalanceIntervalId);
}
}
}