aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFabio Berger <me@fabioberger.com>2018-01-31 04:27:21 +0800
committerGitHub <noreply@github.com>2018-01-31 04:27:21 +0800
commit1feac1a30886944ccadc00b2ff4f493e0d495301 (patch)
treebe68e9a62e3de3caf6e5e841899df16d8decc7e5
parent2bcb7d56394c43acd68d1823092de2ad047abc6d (diff)
parentd3e42e4b3e2f893b616246d35f46dba92f251e83 (diff)
downloaddexon-sol-tools-1feac1a30886944ccadc00b2ff4f493e0d495301.tar
dexon-sol-tools-1feac1a30886944ccadc00b2ff4f493e0d495301.tar.gz
dexon-sol-tools-1feac1a30886944ccadc00b2ff4f493e0d495301.tar.bz2
dexon-sol-tools-1feac1a30886944ccadc00b2ff4f493e0d495301.tar.lz
dexon-sol-tools-1feac1a30886944ccadc00b2ff4f493e0d495301.tar.xz
dexon-sol-tools-1feac1a30886944ccadc00b2ff4f493e0d495301.tar.zst
dexon-sol-tools-1feac1a30886944ccadc00b2ff4f493e0d495301.zip
Merge pull request #351 from 0xProject/feature/portal-ledger-support
Portal Ledger Support, Lazy-loading token balances/allowances
-rw-r--r--packages/website/package.json1
-rw-r--r--packages/website/public/gifs/0xAnimation.gifbin909585 -> 0 bytes
-rw-r--r--packages/website/public/images/ledger_icon.pngbin0 -> 4885 bytes
-rw-r--r--packages/website/public/images/loading_poster.pngbin2505 -> 0 bytes
-rw-r--r--packages/website/public/images/metamask_or_parity.pngbin0 -> 22907 bytes
-rw-r--r--packages/website/public/images/network_icons/kovan.pngbin0 -> 244 bytes
-rw-r--r--packages/website/public/images/network_icons/mainnet.pngbin0 -> 205 bytes
-rw-r--r--packages/website/public/images/network_icons/rinkeby.pngbin0 -> 126 bytes
-rw-r--r--packages/website/public/images/network_icons/ropsten.pngbin0 -> 251 bytes
-rw-r--r--packages/website/public/videos/0xAnimation.mp4bin970342 -> 0 bytes
-rw-r--r--packages/website/ts/blockchain.ts359
-rw-r--r--packages/website/ts/components/dialogs/blockchain_err_dialog.tsx4
-rw-r--r--packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx67
-rw-r--r--packages/website/ts/components/dialogs/ledger_config_dialog.tsx59
-rw-r--r--packages/website/ts/components/dialogs/send_dialog.tsx13
-rw-r--r--packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx10
-rw-r--r--packages/website/ts/components/dropdowns/network_drop_down.tsx40
-rw-r--r--packages/website/ts/components/eth_weth_conversion_button.tsx20
-rw-r--r--packages/website/ts/components/eth_wrappers.tsx100
-rw-r--r--packages/website/ts/components/fill_order.tsx40
-rw-r--r--packages/website/ts/components/generate_order/asset_picker.tsx16
-rw-r--r--packages/website/ts/components/generate_order/generate_order_form.tsx21
-rw-r--r--packages/website/ts/components/generate_order/new_token_form.tsx17
-rw-r--r--packages/website/ts/components/inputs/allowance_toggle.tsx7
-rw-r--r--packages/website/ts/components/inputs/balance_bounded_input.tsx5
-rw-r--r--packages/website/ts/components/inputs/token_amount_input.tsx64
-rw-r--r--packages/website/ts/components/portal.tsx78
-rw-r--r--packages/website/ts/components/send_button.tsx19
-rw-r--r--packages/website/ts/components/token_balances.tsx198
-rw-r--r--packages/website/ts/components/top_bar/provider_display.tsx148
-rw-r--r--packages/website/ts/components/top_bar/provider_picker.tsx81
-rw-r--r--packages/website/ts/components/top_bar/top_bar.tsx (renamed from packages/website/ts/components/top_bar.tsx)51
-rw-r--r--packages/website/ts/components/top_bar/top_bar_menu_item.tsx (renamed from packages/website/ts/components/top_bar_menu_item.tsx)0
-rw-r--r--packages/website/ts/components/ui/drop_down.tsx (renamed from packages/website/ts/components/ui/drop_down_menu_item.tsx)56
-rw-r--r--packages/website/ts/components/ui/loading.tsx39
-rw-r--r--packages/website/ts/containers/generate_order_form.tsx13
-rw-r--r--packages/website/ts/containers/portal.tsx12
-rw-r--r--packages/website/ts/globals.d.ts1
-rw-r--r--packages/website/ts/local_storage/tracked_token_storage.ts14
-rw-r--r--packages/website/ts/pages/about/about.tsx2
-rw-r--r--packages/website/ts/pages/documentation/documentation.tsx31
-rw-r--r--packages/website/ts/pages/faq/faq.tsx2
-rw-r--r--packages/website/ts/pages/landing/landing.tsx2
-rw-r--r--packages/website/ts/pages/not_found.tsx2
-rw-r--r--packages/website/ts/pages/wiki/wiki.tsx23
-rw-r--r--packages/website/ts/redux/dispatcher.ts56
-rw-r--r--packages/website/ts/redux/reducer.ts81
-rw-r--r--packages/website/ts/types.ts35
-rw-r--r--packages/website/ts/utils/configs.ts10
-rw-r--r--packages/website/ts/utils/constants.ts20
-rw-r--r--packages/website/ts/utils/mui_theme.ts1
-rw-r--r--packages/website/ts/utils/utils.ts10
-rw-r--r--packages/website/ts/web3_wrapper.ts21
53 files changed, 1204 insertions, 645 deletions
diff --git a/packages/website/package.json b/packages/website/package.json
index a5a9eb00a..3d6cc24f7 100644
--- a/packages/website/package.json
+++ b/packages/website/package.json
@@ -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",
diff --git a/packages/website/public/gifs/0xAnimation.gif b/packages/website/public/gifs/0xAnimation.gif
deleted file mode 100644
index b3e32a6ad..000000000
--- a/packages/website/public/gifs/0xAnimation.gif
+++ /dev/null
Binary files differ
diff --git a/packages/website/public/images/ledger_icon.png b/packages/website/public/images/ledger_icon.png
new file mode 100644
index 000000000..29b8df08f
--- /dev/null
+++ b/packages/website/public/images/ledger_icon.png
Binary files differ
diff --git a/packages/website/public/images/loading_poster.png b/packages/website/public/images/loading_poster.png
deleted file mode 100644
index e5618f260..000000000
--- a/packages/website/public/images/loading_poster.png
+++ /dev/null
Binary files differ
diff --git a/packages/website/public/images/metamask_or_parity.png b/packages/website/public/images/metamask_or_parity.png
new file mode 100644
index 000000000..fda646558
--- /dev/null
+++ b/packages/website/public/images/metamask_or_parity.png
Binary files differ
diff --git a/packages/website/public/images/network_icons/kovan.png b/packages/website/public/images/network_icons/kovan.png
new file mode 100644
index 000000000..f47a12e74
--- /dev/null
+++ b/packages/website/public/images/network_icons/kovan.png
Binary files differ
diff --git a/packages/website/public/images/network_icons/mainnet.png b/packages/website/public/images/network_icons/mainnet.png
new file mode 100644
index 000000000..6693635d6
--- /dev/null
+++ b/packages/website/public/images/network_icons/mainnet.png
Binary files differ
diff --git a/packages/website/public/images/network_icons/rinkeby.png b/packages/website/public/images/network_icons/rinkeby.png
new file mode 100644
index 000000000..f9ba18778
--- /dev/null
+++ b/packages/website/public/images/network_icons/rinkeby.png
Binary files differ
diff --git a/packages/website/public/images/network_icons/ropsten.png b/packages/website/public/images/network_icons/ropsten.png
new file mode 100644
index 000000000..894910b34
--- /dev/null
+++ b/packages/website/public/images/network_icons/ropsten.png
Binary files differ
diff --git a/packages/website/public/videos/0xAnimation.mp4 b/packages/website/public/videos/0xAnimation.mp4
deleted file mode 100644
index d78c07d4f..000000000
--- a/packages/website/public/videos/0xAnimation.mp4
+++ /dev/null
Binary files differ
diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts
index 5530701c0..d53994c0c 100644
--- a/packages/website/ts/blockchain.ts
+++ b/packages/website/ts/blockchain.ts
@@ -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;
- }
+ const isU2FSupported = await utils.isU2FSupportedAsync();
+ if (!isU2FSupported) {
+ throw new Error('Cannot update providerType to LEDGER without U2F support');
+ }
- 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;
- }
+ // 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;
+ }
- default:
- throw utils.spawnSwitchErr('providerType', providerType);
+ 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
}
- await this._fetchTokenInformationAsync();
+ 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
diff --git a/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx b/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx
index f555ca6b1..278e2bbf5 100644
--- a/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx
+++ b/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx
@@ -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}).`
: `.`}
diff --git a/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx b/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx
index 661cc1d8c..acd4a7110 100644
--- a/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx
+++ b/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx
@@ -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,
+ });
+ }
+ }
}
diff --git a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx
index 60db93c52..bc5f05241 100644
--- a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx
+++ b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx
@@ -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,
+ });
+ }
}
diff --git a/packages/website/ts/components/dialogs/send_dialog.tsx b/packages/website/ts/components/dialogs/send_dialog.tsx
index b3dbce598..d44dd9aab 100644
--- a/packages/website/ts/components/dialogs/send_dialog.tsx
+++ b/packages/website/ts/components/dialogs/send_dialog.tsx
@@ -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>
);
diff --git a/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx b/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx
index 3f29d46f8..bb7e3ed1a 100644
--- a/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx
+++ b/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx
@@ -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({
diff --git a/packages/website/ts/components/dropdowns/network_drop_down.tsx b/packages/website/ts/components/dropdowns/network_drop_down.tsx
new file mode 100644
index 000000000..28ec28ed5
--- /dev/null
+++ b/packages/website/ts/components/dropdowns/network_drop_down.tsx
@@ -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;
+ }
+}
diff --git a/packages/website/ts/components/eth_weth_conversion_button.tsx b/packages/website/ts/components/eth_weth_conversion_button.tsx
index 300e71f1f..62bafdba7 100644
--- a/packages/website/ts/components/eth_weth_conversion_button.tsx
+++ b/packages/website/ts/components/eth_weth_conversion_button.tsx
@@ -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 =
diff --git a/packages/website/ts/components/eth_wrappers.tsx b/packages/website/ts/components/eth_wrappers.tsx
index d074ec787..db1ceecb5 100644
--- a/packages/website/ts/components/eth_wrappers.tsx
+++ b/packages/website/ts/components/eth_wrappers.tsx
@@ -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
diff --git a/packages/website/ts/components/fill_order.tsx b/packages/website/ts/components/fill_order.tsx
index 1a150e9ee..d0cfd2cf5 100644
--- a/packages/website/ts/components/fill_order.tsx
+++ b/packages/website/ts/components/fill_order.tsx
@@ -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';
diff --git a/packages/website/ts/components/generate_order/asset_picker.tsx b/packages/website/ts/components/generate_order/asset_picker.tsx
index df7d87cfd..69fb32a21 100644
--- a/packages/website/ts/components/generate_order/asset_picker.tsx
+++ b/packages/website/ts/components/generate_order/asset_picker.tsx
@@ -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,
diff --git a/packages/website/ts/components/generate_order/generate_order_form.tsx b/packages/website/ts/components/generate_order/generate_order_form.tsx
index 3ae0d48a7..df1241d8d 100644
--- a/packages/website/ts/components/generate_order/generate_order_form.tsx
+++ b/packages/website/ts/components/generate_order/generate_order_form.tsx
@@ -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) &&
diff --git a/packages/website/ts/components/generate_order/new_token_form.tsx b/packages/website/ts/components/generate_order/new_token_form.tsx
index 63645be9a..f76830a49 100644
--- a/packages/website/ts/components/generate_order/new_token_form.tsx
+++ b/packages/website/ts/components/generate_order/new_token_form.tsx
@@ -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 = '';
diff --git a/packages/website/ts/components/inputs/allowance_toggle.tsx b/packages/website/ts/components/inputs/allowance_toggle.tsx
index da46db4f4..45531e74b 100644
--- a/packages/website/ts/components/inputs/allowance_toggle.tsx
+++ b/packages/website/ts/components/inputs/allowance_toggle.tsx
@@ -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}`);
diff --git a/packages/website/ts/components/inputs/balance_bounded_input.tsx b/packages/website/ts/components/inputs/balance_bounded_input.tsx
index ddc434b51..3bbc7a5f6 100644
--- a/packages/website/ts/components/inputs/balance_bounded_input.tsx
+++ b/packages/website/ts/components/inputs/balance_bounded_input.tsx
@@ -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);
diff --git a/packages/website/ts/components/inputs/token_amount_input.tsx b/packages/website/ts/components/inputs/token_amount_input.tsx
index 63966d759..2b167d875 100644
--- a/packages/website/ts/components/inputs/token_amount_input.tsx
+++ b/packages/website/ts/components/inputs/token_amount_input.tsx
@@ -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,
+ });
+ }
+ }
}
diff --git a/packages/website/ts/components/portal.tsx b/packages/website/ts/components/portal.tsx
index e2e28e8b6..92589f75c 100644
--- a/packages/website/ts/components/portal.tsx
+++ b/packages/website/ts/components/portal.tsx
@@ -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);
- }
}
diff --git a/packages/website/ts/components/send_button.tsx b/packages/website/ts/components/send_button.tsx
index f94ec346a..ffa165f60 100644
--- a/packages/website/ts/components/send_button.tsx
+++ b/packages/website/ts/components/send_button.tsx
@@ -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();
diff --git a/packages/website/ts/components/token_balances.tsx b/packages/website/ts/components/token_balances.tsx
index 2cef413c7..c6a9a46be 100644
--- a/packages/website/ts/components/token_balances.tsx
+++ b/packages/website/ts/components/token_balances.tsx
@@ -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
diff --git a/packages/website/ts/components/top_bar/provider_display.tsx b/packages/website/ts/components/top_bar/provider_display.tsx
new file mode 100644
index 000000000..39e7f2a8c
--- /dev/null
+++ b/packages/website/ts/components/top_bar/provider_display.tsx
@@ -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>
+ );
+ }
+ }
+}
diff --git a/packages/website/ts/components/top_bar/provider_picker.tsx b/packages/website/ts/components/top_bar/provider_picker.tsx
new file mode 100644
index 000000000..be7e57d6f
--- /dev/null
+++ b/packages/website/ts/components/top_bar/provider_picker.tsx
@@ -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();
+ }
+ }
+}
diff --git a/packages/website/ts/components/top_bar.tsx b/packages/website/ts/components/top_bar/top_bar.tsx
index 11d3e7cc2..1a0691e83 100644
--- a/packages/website/ts/components/top_bar.tsx
+++ b/packages/website/ts/components/top_bar/top_bar.tsx
@@ -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)} />
diff --git a/packages/website/ts/components/top_bar_menu_item.tsx b/packages/website/ts/components/top_bar/top_bar_menu_item.tsx
index 96ee86142..96ee86142 100644
--- a/packages/website/ts/components/top_bar_menu_item.tsx
+++ b/packages/website/ts/components/top_bar/top_bar_menu_item.tsx
diff --git a/packages/website/ts/components/ui/drop_down_menu_item.tsx b/packages/website/ts/components/ui/drop_down.tsx
index a578fb4f9..63b9eec0b 100644
--- a/packages/website/ts/components/ui/drop_down_menu_item.tsx
+++ b/packages/website/ts/components/ui/drop_down.tsx
@@ -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() {
diff --git a/packages/website/ts/components/ui/loading.tsx b/packages/website/ts/components/ui/loading.tsx
deleted file mode 100644
index aa319e9e9..000000000
--- a/packages/website/ts/components/ui/loading.tsx
+++ /dev/null
@@ -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>
- );
- }
-}
diff --git a/packages/website/ts/containers/generate_order_form.tsx b/packages/website/ts/containers/generate_order_form.tsx
index 3fd31087f..57863dbae 100644
--- a/packages/website/ts/containers/generate_order_form.tsx
+++ b/packages/website/ts/containers/generate_order_form.tsx
@@ -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)(
diff --git a/packages/website/ts/containers/portal.tsx b/packages/website/ts/containers/portal.tsx
index f0247935b..bcca0d70f 100644
--- a/packages/website/ts/containers/portal.tsx
+++ b/packages/website/ts/containers/portal.tsx
@@ -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,
diff --git a/packages/website/ts/globals.d.ts b/packages/website/ts/globals.d.ts
index 383e5cbe0..d7f887c6d 100644
--- a/packages/website/ts/globals.d.ts
+++ b/packages/website/ts/globals.d.ts
@@ -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' {
diff --git a/packages/website/ts/local_storage/tracked_token_storage.ts b/packages/website/ts/local_storage/tracked_token_storage.ts
index 7733e8436..f865f8109 100644
--- a/packages/website/ts/local_storage/tracked_token_storage.ts
+++ b/packages/website/ts/local_storage/tracked_token_storage.ts
@@ -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();
diff --git a/packages/website/ts/pages/about/about.tsx b/packages/website/ts/pages/about/about.tsx
index c929673f5..0889e79f3 100644
--- a/packages/website/ts/pages/about/about.tsx
+++ b/packages/website/ts/pages/about/about.tsx
@@ -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';
diff --git a/packages/website/ts/pages/documentation/documentation.tsx b/packages/website/ts/pages/documentation/documentation.tsx
index 2315847ad..7ad1d3b9c 100644
--- a/packages/website/ts/pages/documentation/documentation.tsx
+++ b/packages/website/ts/pages/documentation/documentation.tsx
@@ -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();
+ },
+ );
+ }
}
}
diff --git a/packages/website/ts/pages/faq/faq.tsx b/packages/website/ts/pages/faq/faq.tsx
index b4b5214a2..0a7eecc2d 100644
--- a/packages/website/ts/pages/faq/faq.tsx
+++ b/packages/website/ts/pages/faq/faq.tsx
@@ -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';
diff --git a/packages/website/ts/pages/landing/landing.tsx b/packages/website/ts/pages/landing/landing.tsx
index ca76497df..b0c622fb4 100644
--- a/packages/website/ts/pages/landing/landing.tsx
+++ b/packages/website/ts/pages/landing/landing.tsx
@@ -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';
diff --git a/packages/website/ts/pages/not_found.tsx b/packages/website/ts/pages/not_found.tsx
index ff277c377..0a6ec071c 100644
--- a/packages/website/ts/pages/not_found.tsx
+++ b/packages/website/ts/pages/not_found.tsx
@@ -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 {
diff --git a/packages/website/ts/pages/wiki/wiki.tsx b/packages/website/ts/pages/wiki/wiki.tsx
index d065614ba..daf5c27a7 100644
--- a/packages/website/ts/pages/wiki/wiki.tsx
+++ b/packages/website/ts/pages/wiki/wiki.tsx
@@ -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);
diff --git a/packages/website/ts/redux/dispatcher.ts b/packages/website/ts/redux/dispatcher.ts
index 42989e5e1..87415b285 100644
--- a/packages/website/ts/redux/dispatcher.ts
+++ b/packages/website/ts/redux/dispatcher.ts
@@ -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) {
- this._dispatch({
- data: tokenStateByAddress,
- type: ActionTypes.UpdateTokenStateByAddress,
- });
- }
- public removeFromTokenStateByAddress(tokenAddress: string) {
- this._dispatch({
- data: tokenAddress,
- type: ActionTypes.RemoveFromTokenStateByAddress,
- });
- }
- public replaceTokenAllowanceByAddress(address: string, allowance: BigNumber) {
+ public forceTokenStateRefetch() {
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) {
diff --git a/packages/website/ts/redux/reducer.ts b/packages/website/ts/redux/reducer.ts
index 06ac8b670..7b0b03dae 100644
--- a/packages/website/ts/redux/reducer.ts
+++ b/packages/website/ts/redux/reducer.ts
@@ -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;
- });
- return {
- ...state,
- tokenStateByAddress,
- };
- }
-
- case ActionTypes.RemoveFromTokenStateByAddress: {
- const tokenStateByAddress = state.tokenStateByAddress;
- const tokenAddress = action.data;
- delete tokenStateByAddress[tokenAddress];
- return {
- ...state,
- tokenStateByAddress,
- };
- }
-
- case ActionTypes.ReplaceTokenAllowanceByAddress: {
- const tokenStateByAddress = state.tokenStateByAddress;
- const allowance = action.data.allowance;
- const tokenAddress = action.data.address;
- tokenStateByAddress[tokenAddress] = {
- ...tokenStateByAddress[tokenAddress],
- allowance,
- };
+ case ActionTypes.BatchDispatch: {
return {
...state,
- tokenStateByAddress,
+ networkId: action.data.networkId,
+ userAddress: action.data.userAddress,
+ sideToAssetToken: action.data.sideToAssetToken,
+ tokenByAddress: action.data.tokenByAddress,
};
}
- case ActionTypes.ReplaceTokenBalanceByAddress: {
- const tokenStateByAddress = state.tokenStateByAddress;
- const balance = action.data.balance;
- const tokenAddress = action.data.address;
- tokenStateByAddress[tokenAddress] = {
- ...tokenStateByAddress[tokenAddress],
- balance,
- };
+ case ActionTypes.ForceTokenStateRefetch:
return {
...state,
- tokenStateByAddress,
+ lastForceTokenStateRefetch: moment().unix(),
};
- }
-
- 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 {
diff --git a/packages/website/ts/types.ts b/packages/website/ts/types.ts
index f873f95fa..c48c88cae 100644
--- a/packages/website/ts/types.ts
+++ b/packages/website/ts/types.ts
@@ -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
diff --git a/packages/website/ts/utils/configs.ts b/packages/website/ts/utils/configs.ts
index 3d37a89ab..874ad04c2 100644
--- a/packages/website/ts/utils/configs.ts
+++ b/packages/website/ts/utils/configs.ts
@@ -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'],
diff --git a/packages/website/ts/utils/constants.ts b/packages/website/ts/utils/constants.ts
index dded82114..26a793f38 100644
--- a/packages/website/ts/utils/constants.ts
+++ b/packages/website/ts/utils/constants.ts
@@ -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',
diff --git a/packages/website/ts/utils/mui_theme.ts b/packages/website/ts/utils/mui_theme.ts
index d73e80606..32891baca 100644
--- a/packages/website/ts/utils/mui_theme.ts
+++ b/packages/website/ts/utils/mui_theme.ts
@@ -8,6 +8,7 @@ export const muiTheme = getMuiTheme({
textColor: colors.black,
},
palette: {
+ accent1Color: colors.lightBlueA700,
pickerHeaderColor: colors.lightBlue,
primary1Color: colors.lightBlue,
primary2Color: colors.lightBlue,
diff --git a/packages/website/ts/utils/utils.ts b/packages/website/ts/utils/utils.ts
index 13a6d6ae2..7e69b1c5f 100644
--- a/packages/website/ts/utils/utils.ts
+++ b/packages/website/ts/utils/utils.ts
@@ -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');
+ },
};
diff --git a/packages/website/ts/web3_wrapper.ts b/packages/website/ts/web3_wrapper.ts
index 415df6e8b..9d8d771af 100644
--- a/packages/website/ts/web3_wrapper.ts
+++ b/packages/website/ts/web3_wrapper.ts
@@ -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);
+ }
}
}