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

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 888 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

View File

@ -37,10 +37,10 @@ import {
EtherscanLinkSuffixes, EtherscanLinkSuffixes,
ProviderType, ProviderType,
Side, Side,
SideToAssetToken,
SignatureData, SignatureData,
Token, Token,
TokenByAddress, TokenByAddress,
TokenStateByAddress,
} from 'ts/types'; } from 'ts/types';
import { configs } from 'ts/utils/configs'; import { configs } from 'ts/utils/configs';
import { constants } from 'ts/utils/constants'; 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'; import * as MintableArtifacts from '../contracts/Mintable.json';
const BLOCK_NUMBER_BACK_TRACK = 50; const BLOCK_NUMBER_BACK_TRACK = 50;
const GWEI_IN_WEI = 1000000000;
export class Blockchain { export class Blockchain {
public networkId: number; public networkId: number;
@ -64,8 +65,9 @@ export class Blockchain {
private _exchangeAddress: string; private _exchangeAddress: string;
private _userAddress: string; private _userAddress: string;
private _cachedProvider: Web3.Provider; private _cachedProvider: Web3.Provider;
private _cachedProviderNetworkId: number;
private _ledgerSubprovider: LedgerWalletSubprovider; private _ledgerSubprovider: LedgerWalletSubprovider;
private _zrxPollIntervalId: NodeJS.Timer; private _defaultGasPrice: BigNumber;
private static async _onPageLoadAsync(): Promise<void> { private static async _onPageLoadAsync(): Promise<void> {
if (document.readyState === 'complete') { if (document.readyState === 'complete') {
return; // Already loaded return; // Already loaded
@ -111,7 +113,7 @@ export class Blockchain {
// injected into their browser. // injected into their browser.
provider = new ProviderEngine(); provider = new ProviderEngine();
provider.addProvider(new FilterSubprovider()); 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.addProvider(new RedundantRPCSubprovider(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId]));
provider.start(); provider.start();
} }
@ -121,6 +123,10 @@ export class Blockchain {
constructor(dispatcher: Dispatcher, isSalePage: boolean = false) { constructor(dispatcher: Dispatcher, isSalePage: boolean = false) {
this._dispatcher = dispatcher; this._dispatcher = dispatcher;
this._userAddress = ''; 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 // tslint:disable-next-line:no-floating-promises
this._onPageLoadInitFireAndForgetAsync(); this._onPageLoadInitFireAndForgetAsync();
} }
@ -133,14 +139,14 @@ export class Blockchain {
} else if (this.networkId !== newNetworkId) { } else if (this.networkId !== newNetworkId) {
this.networkId = newNetworkId; this.networkId = newNetworkId;
this._dispatcher.encounteredBlockchainError(BlockchainErrs.NoError); this._dispatcher.encounteredBlockchainError(BlockchainErrs.NoError);
await this._fetchTokenInformationAsync(); await this.fetchTokenInformationAsync();
await this._rehydrateStoreWithContractEvents(); await this._rehydrateStoreWithContractEvents();
} }
} }
public async userAddressUpdatedFireAndForgetAsync(newUserAddress: string) { public async userAddressUpdatedFireAndForgetAsync(newUserAddress: string) {
if (this._userAddress !== newUserAddress) { if (this._userAddress !== newUserAddress) {
this._userAddress = newUserAddress; this._userAddress = newUserAddress;
await this._fetchTokenInformationAsync(); await this.fetchTokenInformationAsync();
await this._rehydrateStoreWithContractEvents(); await this._rehydrateStoreWithContractEvents();
} }
} }
@ -180,84 +186,96 @@ export class Blockchain {
} }
this._ledgerSubprovider.setPathIndex(pathIndex); this._ledgerSubprovider.setPathIndex(pathIndex);
} }
public async providerTypeUpdatedFireAndForgetAsync(providerType: ProviderType) { public async updateProviderToLedgerAsync(networkId: number) {
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.'); 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(); const isU2FSupported = await utils.isU2FSupportedAsync();
if (!isU2FSupported) { if (!isU2FSupported) {
throw new Error('Cannot update providerType to LEDGER without U2F support'); 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 // Cache injected provider so that we can switch the user back to it easily
if (_.isUndefined(this._cachedProvider)) {
this._cachedProvider = this._web3Wrapper.getProviderObj(); this._cachedProvider = this._web3Wrapper.getProviderObj();
this._cachedProviderNetworkId = this.networkId;
}
this._web3Wrapper.destroy();
this._userAddress = '';
this._dispatcher.updateUserAddress(''); // Clear old userAddress this._dispatcher.updateUserAddress(''); // Clear old userAddress
provider = new ProviderEngine(); const provider = new ProviderEngine();
const ledgerWalletConfigs = { const ledgerWalletConfigs = {
networkId: this.networkId, networkId,
ledgerEthereumClientFactoryAsync: ledgerEthereumBrowserClientFactoryAsync, ledgerEthereumClientFactoryAsync: ledgerEthereumBrowserClientFactoryAsync,
}; };
this._ledgerSubprovider = new LedgerSubprovider(ledgerWalletConfigs); this._ledgerSubprovider = new LedgerSubprovider(ledgerWalletConfigs);
provider.addProvider(this._ledgerSubprovider); provider.addProvider(this._ledgerSubprovider);
provider.addProvider(new FilterSubprovider()); 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.addProvider(new RedundantRPCSubprovider(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId]));
provider.start(); provider.start();
this._web3Wrapper.destroy(); this.networkId = networkId;
this._dispatcher.updateNetworkId(this.networkId);
const shouldPollUserAddress = false; const shouldPollUserAddress = false;
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress); this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress);
this._zeroEx.setProvider(provider, networkId);
await this._postInstantiationOrUpdatingProviderZeroExAsync();
break;
}
case ProviderType.Injected: {
if (_.isUndefined(this._cachedProvider)) {
return; // Going from injected to injected, so we noop
}
provider = this._cachedProvider;
const shouldPollUserAddress = true;
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress);
this._zeroEx.setProvider(provider, this.networkId); this._zeroEx.setProvider(provider, this.networkId);
await this._postInstantiationOrUpdatingProviderZeroExAsync(); await this._postInstantiationOrUpdatingProviderZeroExAsync();
this._web3Wrapper.startEmittingNetworkConnectionAndUserBalanceState();
this._dispatcher.updateProviderType(ProviderType.Ledger);
}
public async updateProviderToInjectedAsync() {
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
if (_.isUndefined(this._cachedProvider)) {
return; // Going from injected to injected, so we noop
}
this._web3Wrapper.destroy();
const provider = this._cachedProvider;
this.networkId = this._cachedProviderNetworkId;
const shouldPollUserAddress = true;
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress);
this._userAddress = await this._web3Wrapper.getFirstAccountIfExistsAsync();
this._zeroEx.setProvider(provider, this.networkId);
await this._postInstantiationOrUpdatingProviderZeroExAsync();
await this.fetchTokenInformationAsync();
this._web3Wrapper.startEmittingNetworkConnectionAndUserBalanceState();
this._dispatcher.updateProviderType(ProviderType.Injected);
delete this._ledgerSubprovider; delete this._ledgerSubprovider;
delete this._cachedProvider; delete this._cachedProvider;
break;
}
default:
throw utils.spawnSwitchErr('providerType', providerType);
}
await this._fetchTokenInformationAsync();
} }
public async setProxyAllowanceAsync(token: Token, amountInBaseUnits: BigNumber): Promise<void> { public async setProxyAllowanceAsync(token: Token, amountInBaseUnits: BigNumber): Promise<void> {
utils.assert(this.isValidAddress(token.address), BlockchainCallErrs.TokenAddressIsInvalid); utils.assert(this.isValidAddress(token.address), BlockchainCallErrs.TokenAddressIsInvalid);
utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses);
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.'); utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
this._showFlashMessageIfLedger();
const txHash = await this._zeroEx.token.setProxyAllowanceAsync( const txHash = await this._zeroEx.token.setProxyAllowanceAsync(
token.address, token.address,
this._userAddress, this._userAddress,
amountInBaseUnits, amountInBaseUnits,
{
gasPrice: this._defaultGasPrice,
},
); );
await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
const allowance = amountInBaseUnits;
this._dispatcher.replaceTokenAllowanceByAddress(token.address, allowance);
} }
public async transferAsync(token: Token, toAddress: string, amountInBaseUnits: BigNumber): Promise<void> { public async transferAsync(token: Token, toAddress: string, amountInBaseUnits: BigNumber): Promise<void> {
this._showFlashMessageIfLedger();
const txHash = await this._zeroEx.token.transferAsync( const txHash = await this._zeroEx.token.transferAsync(
token.address, token.address,
this._userAddress, this._userAddress,
toAddress, toAddress,
amountInBaseUnits, amountInBaseUnits,
{
gasPrice: this._defaultGasPrice,
},
); );
await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
const etherScanLinkIfExists = utils.getEtherScanLinkIfExists(txHash, this.networkId, EtherscanLinkSuffixes.Tx); const etherScanLinkIfExists = utils.getEtherScanLinkIfExists(txHash, this.networkId, EtherscanLinkSuffixes.Tx);
@ -309,11 +327,15 @@ export class Blockchain {
const shouldThrowOnInsufficientBalanceOrAllowance = true; const shouldThrowOnInsufficientBalanceOrAllowance = true;
this._showFlashMessageIfLedger();
const txHash = await this._zeroEx.exchange.fillOrderAsync( const txHash = await this._zeroEx.exchange.fillOrderAsync(
signedOrder, signedOrder,
fillTakerTokenAmount, fillTakerTokenAmount,
shouldThrowOnInsufficientBalanceOrAllowance, shouldThrowOnInsufficientBalanceOrAllowance,
this._userAddress, this._userAddress,
{
gasPrice: this._defaultGasPrice,
},
); );
const receipt = await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash); const receipt = await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
const logs: Array<LogWithDecodedArgs<ExchangeContractEventArgs>> = receipt.logs as any; const logs: Array<LogWithDecodedArgs<ExchangeContractEventArgs>> = receipt.logs as any;
@ -324,7 +346,10 @@ export class Blockchain {
return filledTakerTokenAmount; return filledTakerTokenAmount;
} }
public async cancelOrderAsync(signedOrder: SignedOrder, cancelTakerTokenAmount: BigNumber): Promise<BigNumber> { 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 receipt = await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
const logs: Array<LogWithDecodedArgs<ExchangeContractEventArgs>> = receipt.logs as any; const logs: Array<LogWithDecodedArgs<ExchangeContractEventArgs>> = receipt.logs as any;
this._zeroEx.exchange.throwLogErrorsAsErrors(logs); this._zeroEx.exchange.throwLogErrorsAsErrors(logs);
@ -368,22 +393,25 @@ export class Blockchain {
const [currBalance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, token.address); const [currBalance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, token.address);
this._zrxPollIntervalId = intervalUtils.setAsyncExcludingInterval( const newTokenBalancePromise = new Promise((resolve: (balance: BigNumber) => void, reject) => {
const tokenPollInterval = intervalUtils.setAsyncExcludingInterval(
async () => { async () => {
const [balance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, token.address); const [balance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, token.address);
if (!balance.eq(currBalance)) { if (!balance.eq(currBalance)) {
this._dispatcher.replaceTokenBalanceByAddress(token.address, balance); intervalUtils.clearAsyncExcludingInterval(tokenPollInterval);
intervalUtils.clearAsyncExcludingInterval(this._zrxPollIntervalId); resolve(balance);
delete this._zrxPollIntervalId;
} }
}, },
5000, 5000,
(err: Error) => { (err: Error) => {
utils.consoleLog(`Polling tokenBalance failed: ${err}`); utils.consoleLog(`Polling tokenBalance failed: ${err}`);
intervalUtils.clearAsyncExcludingInterval(this._zrxPollIntervalId); intervalUtils.clearAsyncExcludingInterval(tokenPollInterval);
delete this._zrxPollIntervalId; reject(err);
}, },
); );
});
return newTokenBalancePromise;
} }
public async signOrderHashAsync(orderHash: string): Promise<SignatureData> { public async signOrderHashAsync(orderHash: string): Promise<SignatureData> {
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.'); utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
@ -393,7 +421,21 @@ export class Blockchain {
if (_.isUndefined(makerAddress)) { if (_.isUndefined(makerAddress)) {
throw new Error('Tried to send a sign request but user has no associated addresses'); 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, { const signatureData = _.extend({}, ecSignature, {
hash: orderHash, hash: orderHash,
}); });
@ -404,11 +446,11 @@ export class Blockchain {
utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses);
const mintableContract = await this._instantiateContractIfExistsAsync(MintableArtifacts, token.address); const mintableContract = await this._instantiateContractIfExistsAsync(MintableArtifacts, token.address);
this._showFlashMessageIfLedger();
await mintableContract.mint(constants.MINT_AMOUNT, { await mintableContract.mint(constants.MINT_AMOUNT, {
from: this._userAddress, from: this._userAddress,
gasPrice: this._defaultGasPrice,
}); });
const balanceDelta = constants.MINT_AMOUNT;
this._dispatcher.updateTokenBalanceByAddress(token.address, balanceDelta);
} }
public async getBalanceInEthAsync(owner: string): Promise<BigNumber> { public async getBalanceInEthAsync(owner: string): Promise<BigNumber> {
const balance = await this._web3Wrapper.getBalanceInEthAsync(owner); 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(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); 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); await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
} }
public async convertWrappedEthTokensToEthAsync(etherTokenAddress: string, amount: BigNumber): Promise<void> { public async convertWrappedEthTokensToEthAsync(etherTokenAddress: string, amount: BigNumber): Promise<void> {
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.'); utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses); 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); await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
} }
public async doesContractExistAtAddressAsync(address: string) { public async doesContractExistAtAddressAsync(address: string) {
@ -451,22 +499,6 @@ export class Blockchain {
} }
return [balance, allowance]; 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() { public async getUserAccountsAsync() {
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.'); utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
const userAccountsIfExists = await this._zeroEx.getAvailableAddressesAsync(); const userAccountsIfExists = await this._zeroEx.getAvailableAddressesAsync();
@ -479,10 +511,59 @@ export class Blockchain {
this._web3Wrapper.updatePrevUserAddress(newUserAddress); this._web3Wrapper.updatePrevUserAddress(newUserAddress);
} }
public destroy() { public destroy() {
intervalUtils.clearAsyncExcludingInterval(this._zrxPollIntervalId);
this._web3Wrapper.destroy(); this._web3Wrapper.destroy();
this._stopWatchingExchangeLogFillEvents(); 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( private async _showEtherScanLinkAndAwaitTransactionMinedAsync(
txHash: string, txHash: string,
): Promise<TransactionReceiptWithDecodedLogs> { ): Promise<TransactionReceiptWithDecodedLogs> {
@ -665,17 +746,23 @@ export class Blockchain {
} }
const provider = await Blockchain._getProviderAsync(injectedWeb3, networkIdIfExists); const provider = await Blockchain._getProviderAsync(injectedWeb3, networkIdIfExists);
const networkId = !_.isUndefined(networkIdIfExists) this.networkId = !_.isUndefined(networkIdIfExists)
? 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 = { const zeroExConfigs = {
networkId, networkId: this.networkId,
}; };
this._zeroEx = new ZeroEx(provider, zeroExConfigs); this._zeroEx = new ZeroEx(provider, zeroExConfigs);
this._updateProviderName(injectedWeb3); this._updateProviderName(injectedWeb3);
const shouldPollUserAddress = true; 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(); 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 // This method should always be run after instantiating or updating the provider
// of the ZeroEx instance. // of the ZeroEx instance.
@ -690,60 +777,6 @@ export class Blockchain {
: constants.PROVIDER_NAME_PUBLIC; : constants.PROVIDER_NAME_PUBLIC;
this._dispatcher.updateInjectedProviderName(providerName); 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> { private async _instantiateContractIfExistsAsync(artifact: any, address?: string): Promise<ContractInstance> {
const c = await contract(artifact); const c = await contract(artifact);
const providerObj = this._web3Wrapper.getProviderObj(); 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 } // tslint:disable:max-file-line-count

View File

@ -3,7 +3,7 @@ import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton'; import FlatButton from 'material-ui/FlatButton';
import * as React from 'react'; import * as React from 'react';
import { Blockchain } from 'ts/blockchain'; import { Blockchain } from 'ts/blockchain';
import { BlockchainErrs } from 'ts/types'; import { BlockchainErrs, Networks } from 'ts/types';
import { colors } from 'ts/utils/colors'; import { colors } from 'ts/utils/colors';
import { configs } from 'ts/utils/configs'; import { configs } from 'ts/utils/configs';
import { constants } from 'ts/utils/constants'; import { constants } from 'ts/utils/constants';
@ -129,7 +129,7 @@ export class BlockchainErrDialog extends React.Component<BlockchainErrDialogProp
<div> <div>
The 0x smart contracts are not deployed on the Ethereum network you are currently connected to 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{' '} (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 {configs.IS_MAINNET_ENABLED
? ` or ${constants.MAINNET_NAME} (network Id: ${constants.NETWORK_ID_MAINNET}).` ? ` or ${constants.MAINNET_NAME} (network Id: ${constants.NETWORK_ID_MAINNET}).`
: `.`} : `.`}

View File

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

View File

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

View File

@ -3,16 +3,20 @@ import * as _ from 'lodash';
import Dialog from 'material-ui/Dialog'; import Dialog from 'material-ui/Dialog';
import FlatButton from 'material-ui/FlatButton'; import FlatButton from 'material-ui/FlatButton';
import * as React from 'react'; import * as React from 'react';
import { Blockchain } from 'ts/blockchain';
import { AddressInput } from 'ts/components/inputs/address_input'; import { AddressInput } from 'ts/components/inputs/address_input';
import { TokenAmountInput } from 'ts/components/inputs/token_amount_input'; import { TokenAmountInput } from 'ts/components/inputs/token_amount_input';
import { Token, TokenState } from 'ts/types'; import { Token } from 'ts/types';
interface SendDialogProps { interface SendDialogProps {
blockchain: Blockchain;
userAddress: string;
networkId: number;
onComplete: (recipient: string, value: BigNumber) => void; onComplete: (recipient: string, value: BigNumber) => void;
onCancelled: () => void; onCancelled: () => void;
isOpen: boolean; isOpen: boolean;
token: Token; token: Token;
tokenState: TokenState; lastForceTokenStateRefetch: number;
} }
interface SendDialogState { interface SendDialogState {
@ -66,15 +70,18 @@ export class SendDialog extends React.Component<SendDialogProps, SendDialogState
/> />
</div> </div>
<TokenAmountInput <TokenAmountInput
blockchain={this.props.blockchain}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
label="Amount to send" label="Amount to send"
token={this.props.token} token={this.props.token}
tokenState={this.props.tokenState}
shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs} shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs}
shouldCheckBalance={true} shouldCheckBalance={true}
shouldCheckAllowance={false} shouldCheckAllowance={false}
onChange={this._onValueChange.bind(this)} onChange={this._onValueChange.bind(this)}
amount={this.state.value} amount={this.state.value}
onVisitBalancesPageClick={this.props.onCancelled} onVisitBalancesPageClick={this.props.onCancelled}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
/> />
</div> </div>
); );

View File

@ -82,16 +82,6 @@ export class TrackTokenConfirmationDialog extends React.Component<
newTokenEntry.isTracked = true; newTokenEntry.isTracked = true;
trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newTokenEntry); trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newTokenEntry);
this.props.dispatcher.updateTokenByAddress([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({ this.setState({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,7 +27,6 @@ import {
SignatureData, SignatureData,
Token, Token,
TokenByAddress, TokenByAddress,
TokenStateByAddress,
} from 'ts/types'; } from 'ts/types';
import { colors } from 'ts/utils/colors'; import { colors } from 'ts/utils/colors';
import { errorReporter } from 'ts/utils/error_reporter'; import { errorReporter } from 'ts/utils/error_reporter';
@ -53,7 +52,7 @@ interface GenerateOrderFormProps {
orderSalt: BigNumber; orderSalt: BigNumber;
sideToAssetToken: SideToAssetToken; sideToAssetToken: SideToAssetToken;
tokenByAddress: TokenByAddress; tokenByAddress: TokenByAddress;
tokenStateByAddress: TokenStateByAddress; lastForceTokenStateRefetch: number;
} }
interface GenerateOrderFormState { interface GenerateOrderFormState {
@ -80,10 +79,8 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G
const dispatcher = this.props.dispatcher; const dispatcher = this.props.dispatcher;
const depositTokenAddress = this.props.sideToAssetToken[Side.Deposit].address; const depositTokenAddress = this.props.sideToAssetToken[Side.Deposit].address;
const depositToken = this.props.tokenByAddress[depositTokenAddress]; const depositToken = this.props.tokenByAddress[depositTokenAddress];
const depositTokenState = this.props.tokenStateByAddress[depositTokenAddress];
const receiveTokenAddress = this.props.sideToAssetToken[Side.Receive].address; const receiveTokenAddress = this.props.sideToAssetToken[Side.Receive].address;
const receiveToken = this.props.tokenByAddress[receiveTokenAddress]; const receiveToken = this.props.tokenByAddress[receiveTokenAddress];
const receiveTokenState = this.props.tokenStateByAddress[receiveTokenAddress];
const takerExplanation = const takerExplanation =
'If a taker is specified, only they are<br> \ 'If a taker is specified, only they are<br> \
allowed to fill this order. If no taker is<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} tokenByAddress={this.props.tokenByAddress}
/> />
<TokenAmountInput <TokenAmountInput
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
blockchain={this.props.blockchain}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
label="Sell amount" label="Sell amount"
token={depositToken} token={depositToken}
tokenState={depositTokenState}
amount={this.props.sideToAssetToken[Side.Deposit].amount} amount={this.props.sideToAssetToken[Side.Deposit].amount}
onChange={this._onTokenAmountChange.bind(this, depositToken, Side.Deposit)} onChange={this._onTokenAmountChange.bind(this, depositToken, Side.Deposit)}
shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs} shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs}
@ -139,9 +139,12 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G
tokenByAddress={this.props.tokenByAddress} tokenByAddress={this.props.tokenByAddress}
/> />
<TokenAmountInput <TokenAmountInput
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
blockchain={this.props.blockchain}
userAddress={this.props.userAddress}
networkId={this.props.networkId}
label="Receive amount" label="Receive amount"
token={receiveToken} token={receiveToken}
tokenState={receiveTokenState}
amount={this.props.sideToAssetToken[Side.Receive].amount} amount={this.props.sideToAssetToken[Side.Receive].amount}
onChange={this._onTokenAmountChange.bind(this, receiveToken, Side.Receive)} onChange={this._onTokenAmountChange.bind(this, receiveToken, Side.Receive)}
shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs} shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs}
@ -242,8 +245,10 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G
// Check if all required inputs were supplied // Check if all required inputs were supplied
const debitToken = this.props.sideToAssetToken[Side.Deposit]; const debitToken = this.props.sideToAssetToken[Side.Deposit];
const debitBalance = this.props.tokenStateByAddress[debitToken.address].balance; const [debitBalance, debitAllowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
const debitAllowance = this.props.tokenStateByAddress[debitToken.address].allowance; this.props.userAddress,
debitToken.address,
);
const receiveAmount = this.props.sideToAssetToken[Side.Receive].amount; const receiveAmount = this.props.sideToAssetToken[Side.Receive].amount;
if ( if (
!_.isUndefined(debitToken.amount) && !_.isUndefined(debitToken.amount) &&

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,11 +27,11 @@ import {
BlockchainCallErrs, BlockchainCallErrs,
BlockchainErrs, BlockchainErrs,
EtherscanLinkSuffixes, EtherscanLinkSuffixes,
Networks,
ScreenWidths, ScreenWidths,
Styles, Styles,
Token, Token,
TokenByAddress, TokenByAddress,
TokenStateByAddress,
TokenVisibility, TokenVisibility,
} from 'ts/types'; } from 'ts/types';
import { colors } from 'ts/utils/colors'; 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 { interface TokenBalancesProps {
blockchain: Blockchain; blockchain: Blockchain;
blockchainErr: BlockchainErrs; blockchainErr: BlockchainErrs;
@ -65,10 +73,11 @@ interface TokenBalancesProps {
dispatcher: Dispatcher; dispatcher: Dispatcher;
screenWidth: ScreenWidths; screenWidth: ScreenWidths;
tokenByAddress: TokenByAddress; tokenByAddress: TokenByAddress;
tokenStateByAddress: TokenStateByAddress; trackedTokens: Token[];
userAddress: string; userAddress: string;
userEtherBalance: BigNumber; userEtherBalance: BigNumber;
networkId: number; networkId: number;
lastForceTokenStateRefetch: number;
} }
interface TokenBalancesState { interface TokenBalancesState {
@ -76,14 +85,17 @@ interface TokenBalancesState {
isBalanceSpinnerVisible: boolean; isBalanceSpinnerVisible: boolean;
isDharmaDialogVisible: boolean; isDharmaDialogVisible: boolean;
isZRXSpinnerVisible: boolean; isZRXSpinnerVisible: boolean;
currentZrxBalance?: BigNumber;
isTokenPickerOpen: boolean; isTokenPickerOpen: boolean;
isAddingToken: boolean; isAddingToken: boolean;
trackedTokenStateByAddress: TokenStateByAddress;
} }
export class TokenBalances extends React.Component<TokenBalancesProps, TokenBalancesState> { export class TokenBalances extends React.Component<TokenBalancesProps, TokenBalancesState> {
private _isUnmounted: boolean;
public constructor(props: TokenBalancesProps) { public constructor(props: TokenBalancesProps) {
super(props); super(props);
this._isUnmounted = false;
const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(props.trackedTokens);
this.state = { this.state = {
errorType: undefined, errorType: undefined,
isBalanceSpinnerVisible: false, isBalanceSpinnerVisible: false,
@ -91,8 +103,17 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
isDharmaDialogVisible: DharmaLoanFrame.isAuthTokenPresent(), isDharmaDialogVisible: DharmaLoanFrame.isAuthTokenPresent(),
isTokenPickerOpen: false, isTokenPickerOpen: false,
isAddingToken: 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) { public componentWillReceiveProps(nextProps: TokenBalancesProps) {
if (nextProps.userEtherBalance !== this.props.userEtherBalance) { if (nextProps.userEtherBalance !== this.props.userEtherBalance) {
if (this.state.isBalanceSpinnerVisible) { if (this.state.isBalanceSpinnerVisible) {
@ -103,18 +124,36 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
isBalanceSpinnerVisible: false, isBalanceSpinnerVisible: false,
}); });
} }
const nextZrxToken = _.find(_.values(nextProps.tokenByAddress), t => t.symbol === ZRX_TOKEN_SYMBOL);
const nextZrxTokenBalance = nextProps.tokenStateByAddress[nextZrxToken.address].balance; if (
if (!_.isUndefined(this.state.currentZrxBalance) && !nextZrxTokenBalance.eq(this.state.currentZrxBalance)) { nextProps.userAddress !== this.props.userAddress ||
if (this.state.isZRXSpinnerVisible) { nextProps.networkId !== this.props.networkId ||
const receivedAmount = nextZrxTokenBalance.minus(this.state.currentZrxBalance); nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch
const receiveAmountInUnits = ZeroEx.toUnitAmount(receivedAmount, constants.DECIMAL_PLACES_ZRX); ) {
this.props.dispatcher.showFlashMessage(`Received ${receiveAmountInUnits.toString(10)} Kovan ZRX`); 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({ this.setState({
isZRXSpinnerVisible: false, trackedTokenStateByAddress,
currentZrxBalance: undefined,
}); });
// Fetch the actual balance/allowance.
// tslint:disable-next-line:no-floating-promises
this._fetchBalancesAndAllowancesAsync(newTokenAddresses);
} }
} }
public componentDidMount() { public componentDidMount() {
@ -137,13 +176,13 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
onTouchTap={this._onDharmaDialogToggle.bind(this, false)} 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 = { const dharmaButtonColumnStyle = {
paddingLeft: 3, paddingLeft: 3,
display: isTestNetwork ? 'table-cell' : 'none', display: isKovanTestNetwork ? 'table-cell' : 'none',
}; };
const stubColumnStyle = { const stubColumnStyle = {
display: isTestNetwork ? 'none' : 'table-cell', display: isKovanTestNetwork ? 'none' : 'table-cell',
}; };
const allTokenRowHeight = _.size(this.props.tokenByAddress) * TOKEN_TABLE_ROW_HEIGHT; const allTokenRowHeight = _.size(this.props.tokenByAddress) * TOKEN_TABLE_ROW_HEIGHT;
const tokenTableHeight = const tokenTableHeight =
@ -162,10 +201,10 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
smart contract so you can start trading that token.'; smart contract so you can start trading that token.';
return ( return (
<div className="lg-px4 md-px4 sm-px1 pb2"> <div className="lg-px4 md-px4 sm-px1 pb2">
<h3>{isTestNetwork ? 'Test ether' : 'Ether'}</h3> <h3>{isKovanTestNetwork ? 'Test ether' : 'Ether'}</h3>
<Divider /> <Divider />
<div className="pt2 pb2"> <div className="pt2 pb2">
{isTestNetwork {isKovanTestNetwork
? 'In order to try out the 0x Portal Dapp, request some test ether to pay for \ ? '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.' 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. \ : '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>Currency</TableHeaderColumn>
<TableHeaderColumn>Balance</TableHeaderColumn> <TableHeaderColumn>Balance</TableHeaderColumn>
<TableRowColumn className="sm-hide xs-hide" style={stubColumnStyle} /> <TableRowColumn className="sm-hide xs-hide" style={stubColumnStyle} />
{isTestNetwork && ( {isKovanTestNetwork && (
<TableHeaderColumn style={{ paddingLeft: 3 }}> <TableHeaderColumn style={{ paddingLeft: 3 }}>
{isSmallScreen ? 'Faucet' : 'Request from faucet'} {isSmallScreen ? 'Faucet' : 'Request from faucet'}
</TableHeaderColumn> </TableHeaderColumn>
)} )}
{isTestNetwork && ( {isKovanTestNetwork && (
<TableHeaderColumn style={dharmaButtonColumnStyle}> <TableHeaderColumn style={dharmaButtonColumnStyle}>
{isSmallScreen ? 'Loan' : 'Request Dharma loan'} {isSmallScreen ? 'Loan' : 'Request Dharma loan'}
<HelpTooltip style={{ paddingLeft: 4 }} explanation={dharmaLoanExplanation} /> <HelpTooltip style={{ paddingLeft: 4 }} explanation={dharmaLoanExplanation} />
@ -204,7 +243,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
)} )}
</TableRowColumn> </TableRowColumn>
<TableRowColumn className="sm-hide xs-hide" style={stubColumnStyle} /> <TableRowColumn className="sm-hide xs-hide" style={stubColumnStyle} />
{isTestNetwork && ( {isKovanTestNetwork && (
<TableRowColumn style={{ paddingLeft: 3 }}> <TableRowColumn style={{ paddingLeft: 3 }}>
<LifeCycleRaisedButton <LifeCycleRaisedButton
labelReady="Request" labelReady="Request"
@ -214,7 +253,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
/> />
</TableRowColumn> </TableRowColumn>
)} )}
{isTestNetwork && ( {isKovanTestNetwork && (
<TableRowColumn style={dharmaButtonColumnStyle}> <TableRowColumn style={dharmaButtonColumnStyle}>
<RaisedButton <RaisedButton
label="Request" label="Request"
@ -228,7 +267,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
</Table> </Table>
<div className="clearfix" style={{ paddingBottom: 1 }}> <div className="clearfix" style={{ paddingBottom: 1 }}>
<div className="col col-10"> <div className="col col-10">
<h3 className="pt2">{isTestNetwork ? 'Test tokens' : 'Tokens'}</h3> <h3 className="pt2">{isKovanTestNetwork ? 'Test tokens' : 'Tokens'}</h3>
</div> </div>
<div className="col col-1 pt3 align-right"> <div className="col col-1 pt3 align-right">
<FloatingActionButton mini={true} zDepth={0} onClick={this._onAddTokenClicked.bind(this)}> <FloatingActionButton mini={true} zDepth={0} onClick={this._onAddTokenClicked.bind(this)}>
@ -243,7 +282,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
</div> </div>
<Divider /> <Divider />
<div className="pt2 pb2"> <div className="pt2 pb2">
{isTestNetwork {isKovanTestNetwork
? "Mint some test tokens you'd like to use to generate or fill an order using 0x." ? "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."} : "Set trading permissions for a token you'd like to start trading."}
</div> </div>
@ -303,8 +342,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm; const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm;
const tokenColSpan = isSmallScreen ? TOKEN_COL_SPAN_SM : TOKEN_COL_SPAN_LG; const tokenColSpan = isSmallScreen ? TOKEN_COL_SPAN_SM : TOKEN_COL_SPAN_LG;
const actionPaddingX = isSmallScreen ? 2 : 24; const actionPaddingX = isSmallScreen ? 2 : 24;
const allTokens = _.values(this.props.tokenByAddress); const trackedTokens = this.props.trackedTokens;
const trackedTokens = _.filter(allTokens, t => t.isTracked);
const trackedTokensStartingWithEtherToken = trackedTokens.sort( const trackedTokensStartingWithEtherToken = trackedTokens.sort(
firstBy((t: Token) => t.symbol !== ETHER_TOKEN_SYMBOL) firstBy((t: Token) => t.symbol !== ETHER_TOKEN_SYMBOL)
.thenBy((t: Token) => t.symbol !== ZRX_TOKEN_SYMBOL) .thenBy((t: Token) => t.symbol !== ZRX_TOKEN_SYMBOL)
@ -317,7 +355,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
return tableRows; return tableRows;
} }
private _renderTokenRow(tokenColSpan: number, actionPaddingX: number, token: Token) { 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( const tokenLink = utils.getEtherScanLinkIfExists(
token.address, token.address,
this.props.networkId, this.props.networkId,
@ -338,6 +376,8 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
)} )}
</TableRowColumn> </TableRowColumn>
<TableRowColumn style={{ paddingRight: 3, paddingLeft: 3 }}> <TableRowColumn style={{ paddingRight: 3, paddingLeft: 3 }}>
{tokenState.isLoaded ? (
<span>
{this._renderAmount(tokenState.balance, token.decimals)} {token.symbol} {this._renderAmount(tokenState.balance, token.decimals)} {token.symbol}
{this.state.isZRXSpinnerVisible && {this.state.isZRXSpinnerVisible &&
token.symbol === ZRX_TOKEN_SYMBOL && ( token.symbol === ZRX_TOKEN_SYMBOL && (
@ -345,6 +385,10 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
<i className="zmdi zmdi-spinner zmdi-hc-spin" /> <i className="zmdi zmdi-spinner zmdi-hc-spin" />
</span> </span>
)} )}
</span>
) : (
<i className="zmdi zmdi-spinner zmdi-hc-spin" />
)}
</TableRowColumn> </TableRowColumn>
<TableRowColumn> <TableRowColumn>
<AllowanceToggle <AllowanceToggle
@ -354,6 +398,8 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
tokenState={tokenState} tokenState={tokenState}
onErrorOccurred={this._onErrorOccurred.bind(this)} onErrorOccurred={this._onErrorOccurred.bind(this)}
userAddress={this.props.userAddress} userAddress={this.props.userAddress}
isDisabled={!tokenState.isLoaded}
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)}
/> />
</TableRowColumn> </TableRowColumn>
<TableRowColumn style={{ paddingLeft: actionPaddingX, paddingRight: actionPaddingX }}> <TableRowColumn style={{ paddingLeft: actionPaddingX, paddingRight: actionPaddingX }}>
@ -366,7 +412,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
/> />
)} )}
{token.symbol === ZRX_TOKEN_SYMBOL && {token.symbol === ZRX_TOKEN_SYMBOL &&
this.props.networkId === constants.NETWORK_ID_TESTNET && ( this.props.networkId === constants.NETWORK_ID_KOVAN && (
<LifeCycleRaisedButton <LifeCycleRaisedButton
labelReady="Request" labelReady="Request"
labelLoading="Sending..." labelLoading="Sending..."
@ -383,11 +429,14 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
}} }}
> >
<SendButton <SendButton
userAddress={this.props.userAddress}
networkId={this.props.networkId}
blockchain={this.props.blockchain} blockchain={this.props.blockchain}
dispatcher={this.props.dispatcher} dispatcher={this.props.dispatcher}
token={token} token={token}
tokenState={tokenState}
onError={this._onSendFailed.bind(this)} onError={this._onSendFailed.bind(this)}
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)}
/> />
</TableRowColumn> </TableRowColumn>
)} )}
@ -414,7 +463,6 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
} else { } else {
this.props.dispatcher.removeTokenToTokenByAddress(token); this.props.dispatcher.removeTokenToTokenByAddress(token);
} }
this.props.dispatcher.removeFromTokenStateByAddress(tokenAddress);
trackedTokenStorage.removeTrackedToken(this.props.userAddress, this.props.networkId, tokenAddress); trackedTokenStorage.removeTrackedToken(this.props.userAddress, this.props.networkId, tokenAddress);
} else if (isDefaultTrackedToken) { } else if (isDefaultTrackedToken) {
this.props.dispatcher.showFlashMessage(`Cannot remove ${token.name} because it's a default token`); 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: case BalanceErrs.incorrectNetworkForFaucet:
return ( return (
<div> <div>
Our faucet can only send test Ether to addresses on the {constants.TESTNET_NAME} testnet Our faucet can only send test Ether to addresses on the {Networks.Kovan} testnet (networkId{' '}
(networkId {constants.NETWORK_ID_TESTNET}). Please make sure you are connected to the{' '} {constants.NETWORK_ID_KOVAN}). Please make sure you are connected to the {Networks.Kovan}{' '}
{constants.TESTNET_NAME} testnet and try requesting ether again. testnet and try requesting ether again.
</div> </div>
); );
@ -510,6 +558,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
private async _onMintTestTokensAsync(token: Token): Promise<boolean> { private async _onMintTestTokensAsync(token: Token): Promise<boolean> {
try { try {
await this.props.blockchain.mintTestTokensAsync(token); await this.props.blockchain.mintTestTokensAsync(token);
await this._refetchTokenStateAsync(token.address);
const amount = ZeroEx.toUnitAmount(constants.MINT_AMOUNT, token.decimals); const amount = ZeroEx.toUnitAmount(constants.MINT_AMOUNT, token.decimals);
this.props.dispatcher.showFlashMessage(`Successfully minted ${amount.toString(10)} ${token.symbol}`); this.props.dispatcher.showFlashMessage(`Successfully minted ${amount.toString(10)} ${token.symbol}`);
return true; return true;
@ -519,7 +568,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
return false; return false;
} }
if (_.includes(errMsg, 'User denied transaction')) { if (utils.didUserDenyWeb3Request(errMsg)) {
return false; return false;
} }
utils.consoleLog(`Unexpected error encountered: ${err}`); 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 // If on another network other then the testnet our faucet serves test ether
// from, we must show user an error message // 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({ this.setState({
errorType: BalanceErrs.incorrectNetworkForFaucet, errorType: BalanceErrs.incorrectNetworkForFaucet,
}); });
@ -569,15 +618,11 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
isBalanceSpinnerVisible: true, isBalanceSpinnerVisible: true,
}); });
} else { } 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({ this.setState({
isZRXSpinnerVisible: true, isZRXSpinnerVisible: true,
currentZrxBalance: zrxTokenState.balance,
}); });
// tslint:disable-next-line:no-floating-promises // tslint:disable-next-line:no-floating-promises
this.props.blockchain.pollTokenBalanceAsync(zrxToken); this._startPollingZrxBalanceAsync();
} }
return true; return true;
} }
@ -603,4 +648,65 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
isAddingToken: false, 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 } // tslint:disable:max-file-line-count

View File

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

View File

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

View File

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

View File

@ -1,36 +1,35 @@
import * as _ from 'lodash'; import * as _ from 'lodash';
import Menu from 'material-ui/Menu'; import Popover, { PopoverAnimationVertical } from 'material-ui/Popover';
import Popover from 'material-ui/Popover';
import * as React from 'react'; import * as React from 'react';
import { colors } from 'ts/utils/colors'; import { MaterialUIPosition } from 'ts/types';
const CHECK_CLOSE_POPOVER_INTERVAL_MS = 300; const CHECK_CLOSE_POPOVER_INTERVAL_MS = 300;
const DEFAULT_STYLE = { const DEFAULT_STYLE = {
fontSize: 14, fontSize: 14,
}; };
interface DropDownMenuItemProps { interface DropDownProps {
title: string; hoverActiveNode: React.ReactNode;
subMenuItems: React.ReactNode[]; popoverContent: React.ReactNode;
anchorOrigin: MaterialUIPosition;
targetOrigin: MaterialUIPosition;
style?: React.CSSProperties; style?: React.CSSProperties;
menuItemStyle?: React.CSSProperties; zDepth?: number;
isNightVersion?: boolean;
} }
interface DropDownMenuItemState { interface DropDownState {
isDropDownOpen: boolean; isDropDownOpen: boolean;
anchorEl?: HTMLInputElement; anchorEl?: HTMLInputElement;
} }
export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, DropDownMenuItemState> { export class DropDown extends React.Component<DropDownProps, DropDownState> {
public static defaultProps: Partial<DropDownMenuItemProps> = { public static defaultProps: Partial<DropDownProps> = {
style: DEFAULT_STYLE, style: DEFAULT_STYLE,
menuItemStyle: DEFAULT_STYLE, zDepth: 1,
isNightVersion: false,
}; };
private _isHovering: boolean; private _isHovering: boolean;
private _popoverCloseCheckIntervalId: number; private _popoverCloseCheckIntervalId: number;
constructor(props: DropDownMenuItemProps) { constructor(props: DropDownProps) {
super(props); super(props);
this.state = { this.state = {
isDropDownOpen: false, isDropDownOpen: false,
@ -44,30 +43,35 @@ export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, Dro
public componentWillUnmount() { public componentWillUnmount() {
window.clearInterval(this._popoverCloseCheckIntervalId); 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() { public render() {
const colorStyle = this.props.isNightVersion ? 'white' : this.props.style.color;
return ( return (
<div <div
style={{ ...this.props.style, color: colorStyle }} style={{ ...this.props.style, width: 'fit-content', height: '100%' }}
onMouseEnter={this._onHover.bind(this)} onMouseEnter={this._onHover.bind(this)}
onMouseLeave={this._onHoverOff.bind(this)} onMouseLeave={this._onHoverOff.bind(this)}
> >
<div className="flex relative"> {this.props.hoverActiveNode}
<div style={{ paddingRight: 10 }}>{this.props.title}</div>
<div className="absolute" style={{ paddingLeft: 3, right: 3, top: -2 }}>
<i className="zmdi zmdi-caret-right" style={{ fontSize: 22 }} />
</div>
</div>
<Popover <Popover
open={this.state.isDropDownOpen} open={this.state.isDropDownOpen}
anchorEl={this.state.anchorEl} anchorEl={this.state.anchorEl}
anchorOrigin={{ horizontal: 'middle', vertical: 'bottom' }} anchorOrigin={this.props.anchorOrigin}
targetOrigin={{ horizontal: 'middle', vertical: 'top' }} targetOrigin={this.props.targetOrigin}
onRequestClose={this._closePopover.bind(this)} onRequestClose={this._closePopover.bind(this)}
useLayerForClickAway={false} useLayerForClickAway={false}
animation={PopoverAnimationVertical}
zDepth={this.props.zDepth}
> >
<div onMouseEnter={this._onHover.bind(this)} onMouseLeave={this._onHoverOff.bind(this)}> <div onMouseEnter={this._onHover.bind(this)} onMouseLeave={this._onHoverOff.bind(this)}>
<Menu style={{ color: colors.grey }}>{this.props.subMenuItems}</Menu> {this.props.popoverContent}
</div> </div>
</Popover> </Popover>
</div> </div>
@ -87,7 +91,7 @@ export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, Dro
anchorEl: event.currentTarget, anchorEl: event.currentTarget,
}); });
} }
private _onHoverOff(event: React.FormEvent<HTMLInputElement>) { private _onHoverOff() {
this._isHovering = false; this._isHovering = false;
} }
private _checkIfShouldClosePopover() { private _checkIfShouldClosePopover() {

View File

@ -1,39 +0,0 @@
import * as _ from 'lodash';
import Paper from 'material-ui/Paper';
import * as React from 'react';
import { DefaultPlayer as Video } from 'react-html5video';
import 'react-html5video/dist/styles.css';
import { utils } from 'ts/utils/utils';
interface LoadingProps {}
interface LoadingState {}
export class Loading extends React.Component<LoadingProps, LoadingState> {
public render() {
return (
<div className="pt4 sm-px2 sm-pt2 sm-m1" style={{ height: 500 }}>
<Paper className="mx-auto" style={{ maxWidth: 400 }}>
{utils.isUserOnMobile() ? (
<img className="p1" src="/gifs/0xAnimation.gif" width="96%" />
) : (
<div style={{ pointerEvents: 'none' }}>
<Video
autoPlay={true}
loop={true}
muted={true}
controls={[]}
poster="/images/loading_poster.png"
>
<source src="/videos/0xAnimation.mp4" type="video/mp4" />
</Video>
</div>
)}
<div className="center pt2" style={{ paddingBottom: 11 }}>
Connecting to the blockchain...
</div>
</Paper>
</div>
);
}
}

View File

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

View File

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

View File

@ -10,7 +10,6 @@ declare module 'thenby';
declare module 'react-highlight'; declare module 'react-highlight';
declare module 'react-recaptcha'; declare module 'react-recaptcha';
declare module 'react-document-title'; declare module 'react-document-title';
declare module 'ledgerco';
declare module 'ethereumjs-tx'; declare module 'ethereumjs-tx';
declare module '*.json' { declare module '*.json' {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { Footer } from 'ts/components/footer'; 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'; import { Styles } from 'ts/types';
export interface NotFoundProps { export interface NotFoundProps {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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