diff options
author | Brandon Millman <brandon.millman@gmail.com> | 2018-02-01 07:30:09 +0800 |
---|---|---|
committer | Brandon Millman <brandon.millman@gmail.com> | 2018-02-01 07:30:09 +0800 |
commit | 03cb7233dc5b8556952b4481f87a292e0fca1acf (patch) | |
tree | 4c203211a7ce7b0f44ebc45bb6c40621d4ee5b7e /packages/website | |
parent | 3a1ca32ff172f735e4b69f125fea4237c83643f0 (diff) | |
parent | 6682abf89dcdf566f05f8d88cb6af06c4bb1f6a2 (diff) | |
download | dexon-sol-tools-03cb7233dc5b8556952b4481f87a292e0fca1acf.tar dexon-sol-tools-03cb7233dc5b8556952b4481f87a292e0fca1acf.tar.gz dexon-sol-tools-03cb7233dc5b8556952b4481f87a292e0fca1acf.tar.bz2 dexon-sol-tools-03cb7233dc5b8556952b4481f87a292e0fca1acf.tar.lz dexon-sol-tools-03cb7233dc5b8556952b4481f87a292e0fca1acf.tar.xz dexon-sol-tools-03cb7233dc5b8556952b4481f87a292e0fca1acf.tar.zst dexon-sol-tools-03cb7233dc5b8556952b4481f87a292e0fca1acf.zip |
Merge branch 'development' into feature/testnet-faucets/order-dispenser
* development: (49 commits)
Prettier
Updated contract generation in 0x to new abi-gen CLI
Add PR number to changelog
Fix lint errors
Removed deprecated CLI options
Add protected keyword to underscore lint rule
Remove unused prop
Fix prettier
Uppercase Networks enum values
Make default gasPrice more readable
Fix prettier mess
Fix linter errors
Shrink img
Fix all setState calls after unmounted errors. Decided to create our own flag rather then using a cancellablePromise since there was little to be gained from this alternative
Fix bug where we were return undefined instead of the empty object
Default the derivation path to that found in the Ledger subprovider
Add browser data to dialog info
Add Rinkeby support
Pass in whether we want the personal message prefix appended and never append it for Ledger. This fixes signing when Ledger is used and the backing node is not Parity
Wholesale replace the tokenByAddress and de-dup properly
...
Diffstat (limited to 'packages/website')
53 files changed, 1205 insertions, 647 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 Binary files differdeleted file mode 100644 index b3e32a6ad..000000000 --- a/packages/website/public/gifs/0xAnimation.gif +++ /dev/null diff --git a/packages/website/public/images/ledger_icon.png b/packages/website/public/images/ledger_icon.png Binary files differnew file mode 100644 index 000000000..29b8df08f --- /dev/null +++ b/packages/website/public/images/ledger_icon.png diff --git a/packages/website/public/images/loading_poster.png b/packages/website/public/images/loading_poster.png Binary files differdeleted file mode 100644 index e5618f260..000000000 --- a/packages/website/public/images/loading_poster.png +++ /dev/null diff --git a/packages/website/public/images/metamask_or_parity.png b/packages/website/public/images/metamask_or_parity.png Binary files differnew file mode 100644 index 000000000..fda646558 --- /dev/null +++ b/packages/website/public/images/metamask_or_parity.png diff --git a/packages/website/public/images/network_icons/kovan.png b/packages/website/public/images/network_icons/kovan.png Binary files differnew file mode 100644 index 000000000..f47a12e74 --- /dev/null +++ b/packages/website/public/images/network_icons/kovan.png diff --git a/packages/website/public/images/network_icons/mainnet.png b/packages/website/public/images/network_icons/mainnet.png Binary files differnew file mode 100644 index 000000000..6693635d6 --- /dev/null +++ b/packages/website/public/images/network_icons/mainnet.png diff --git a/packages/website/public/images/network_icons/rinkeby.png b/packages/website/public/images/network_icons/rinkeby.png Binary files differnew file mode 100644 index 000000000..f9ba18778 --- /dev/null +++ b/packages/website/public/images/network_icons/rinkeby.png diff --git a/packages/website/public/images/network_icons/ropsten.png b/packages/website/public/images/network_icons/ropsten.png Binary files differnew file mode 100644 index 000000000..894910b34 --- /dev/null +++ b/packages/website/public/images/network_icons/ropsten.png diff --git a/packages/website/public/videos/0xAnimation.mp4 b/packages/website/public/videos/0xAnimation.mp4 Binary files differdeleted file mode 100644 index d78c07d4f..000000000 --- a/packages/website/public/videos/0xAnimation.mp4 +++ /dev/null diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts index 5530701c0..71995e2cd 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> { @@ -608,7 +689,7 @@ export class Blockchain { } } private _stopWatchingExchangeLogFillEvents(): void { - this._zeroEx.exchange.unsubscribeAll(); + this._zeroEx.exchange._unsubscribeAll(); } private async _getTokenRegistryTokensByAddressAsync(): Promise<TokenByAddress> { utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.'); @@ -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..c2cdf6751 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,11 +301,14 @@ 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} ethToken={outdatedEtherToken} - ethTokenState={outdatedEtherTokenState} dispatcher={this.props.dispatcher} blockchain={this.props.blockchain} userEtherBalance={this.props.userEtherBalance} @@ -338,7 +373,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 +395,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 +420,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); + } } } |