Merge pull request #351 from 0xProject/feature/portal-ledger-support
Portal Ledger Support, Lazy-loading token balances/allowances
@ -36,7 +36,6 @@
|
||||
"find-versions": "^2.0.0",
|
||||
"is-mobile": "^0.2.2",
|
||||
"jsonschema": "^1.2.0",
|
||||
"ledgerco": "0xProject/ledger-node-js-api",
|
||||
"less": "^2.7.2",
|
||||
"lodash": "^4.17.4",
|
||||
"material-ui": "^0.17.1",
|
||||
|
Before Width: | Height: | Size: 888 KiB |
BIN
packages/website/public/images/ledger_icon.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 2.4 KiB |
BIN
packages/website/public/images/metamask_or_parity.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
packages/website/public/images/network_icons/kovan.png
Normal file
After Width: | Height: | Size: 244 B |
BIN
packages/website/public/images/network_icons/mainnet.png
Normal file
After Width: | Height: | Size: 205 B |
BIN
packages/website/public/images/network_icons/rinkeby.png
Normal file
After Width: | Height: | Size: 126 B |
BIN
packages/website/public/images/network_icons/ropsten.png
Normal file
After Width: | Height: | Size: 251 B |
@ -37,10 +37,10 @@ import {
|
||||
EtherscanLinkSuffixes,
|
||||
ProviderType,
|
||||
Side,
|
||||
SideToAssetToken,
|
||||
SignatureData,
|
||||
Token,
|
||||
TokenByAddress,
|
||||
TokenStateByAddress,
|
||||
} from 'ts/types';
|
||||
import { configs } from 'ts/utils/configs';
|
||||
import { constants } from 'ts/utils/constants';
|
||||
@ -54,6 +54,7 @@ import FilterSubprovider = require('web3-provider-engine/subproviders/filters');
|
||||
import * as MintableArtifacts from '../contracts/Mintable.json';
|
||||
|
||||
const BLOCK_NUMBER_BACK_TRACK = 50;
|
||||
const GWEI_IN_WEI = 1000000000;
|
||||
|
||||
export class Blockchain {
|
||||
public networkId: number;
|
||||
@ -64,8 +65,9 @@ export class Blockchain {
|
||||
private _exchangeAddress: string;
|
||||
private _userAddress: string;
|
||||
private _cachedProvider: Web3.Provider;
|
||||
private _cachedProviderNetworkId: number;
|
||||
private _ledgerSubprovider: LedgerWalletSubprovider;
|
||||
private _zrxPollIntervalId: NodeJS.Timer;
|
||||
private _defaultGasPrice: BigNumber;
|
||||
private static async _onPageLoadAsync(): Promise<void> {
|
||||
if (document.readyState === 'complete') {
|
||||
return; // Already loaded
|
||||
@ -111,7 +113,7 @@ export class Blockchain {
|
||||
// injected into their browser.
|
||||
provider = new ProviderEngine();
|
||||
provider.addProvider(new FilterSubprovider());
|
||||
const networkId = configs.IS_MAINNET_ENABLED ? constants.NETWORK_ID_MAINNET : constants.NETWORK_ID_TESTNET;
|
||||
const networkId = configs.IS_MAINNET_ENABLED ? constants.NETWORK_ID_MAINNET : constants.NETWORK_ID_KOVAN;
|
||||
provider.addProvider(new RedundantRPCSubprovider(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId]));
|
||||
provider.start();
|
||||
}
|
||||
@ -121,6 +123,10 @@ export class Blockchain {
|
||||
constructor(dispatcher: Dispatcher, isSalePage: boolean = false) {
|
||||
this._dispatcher = dispatcher;
|
||||
this._userAddress = '';
|
||||
const defaultGasPrice = GWEI_IN_WEI * 30;
|
||||
this._defaultGasPrice = new BigNumber(defaultGasPrice);
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._updateDefaultGasPriceAsync();
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._onPageLoadInitFireAndForgetAsync();
|
||||
}
|
||||
@ -133,14 +139,14 @@ export class Blockchain {
|
||||
} else if (this.networkId !== newNetworkId) {
|
||||
this.networkId = newNetworkId;
|
||||
this._dispatcher.encounteredBlockchainError(BlockchainErrs.NoError);
|
||||
await this._fetchTokenInformationAsync();
|
||||
await this.fetchTokenInformationAsync();
|
||||
await this._rehydrateStoreWithContractEvents();
|
||||
}
|
||||
}
|
||||
public async userAddressUpdatedFireAndForgetAsync(newUserAddress: string) {
|
||||
if (this._userAddress !== newUserAddress) {
|
||||
this._userAddress = newUserAddress;
|
||||
await this._fetchTokenInformationAsync();
|
||||
await this.fetchTokenInformationAsync();
|
||||
await this._rehydrateStoreWithContractEvents();
|
||||
}
|
||||
}
|
||||
@ -180,84 +186,96 @@ export class Blockchain {
|
||||
}
|
||||
this._ledgerSubprovider.setPathIndex(pathIndex);
|
||||
}
|
||||
public async providerTypeUpdatedFireAndForgetAsync(providerType: ProviderType) {
|
||||
public async updateProviderToLedgerAsync(networkId: number) {
|
||||
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
|
||||
// Should actually be Web3.Provider|ProviderEngine union type but it causes issues
|
||||
// later on in the logic.
|
||||
let provider;
|
||||
switch (providerType) {
|
||||
case ProviderType.Ledger: {
|
||||
|
||||
const isU2FSupported = await utils.isU2FSupportedAsync();
|
||||
if (!isU2FSupported) {
|
||||
throw new Error('Cannot update providerType to LEDGER without U2F support');
|
||||
}
|
||||
|
||||
// Cache injected provider so that we can switch the user back to it easily
|
||||
if (_.isUndefined(this._cachedProvider)) {
|
||||
this._cachedProvider = this._web3Wrapper.getProviderObj();
|
||||
this._cachedProviderNetworkId = this.networkId;
|
||||
}
|
||||
|
||||
this._web3Wrapper.destroy();
|
||||
|
||||
this._userAddress = '';
|
||||
this._dispatcher.updateUserAddress(''); // Clear old userAddress
|
||||
|
||||
provider = new ProviderEngine();
|
||||
const provider = new ProviderEngine();
|
||||
const ledgerWalletConfigs = {
|
||||
networkId: this.networkId,
|
||||
networkId,
|
||||
ledgerEthereumClientFactoryAsync: ledgerEthereumBrowserClientFactoryAsync,
|
||||
};
|
||||
this._ledgerSubprovider = new LedgerSubprovider(ledgerWalletConfigs);
|
||||
provider.addProvider(this._ledgerSubprovider);
|
||||
provider.addProvider(new FilterSubprovider());
|
||||
const networkId = configs.IS_MAINNET_ENABLED
|
||||
? constants.NETWORK_ID_MAINNET
|
||||
: constants.NETWORK_ID_TESTNET;
|
||||
provider.addProvider(new RedundantRPCSubprovider(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId]));
|
||||
provider.start();
|
||||
this._web3Wrapper.destroy();
|
||||
this.networkId = networkId;
|
||||
this._dispatcher.updateNetworkId(this.networkId);
|
||||
const shouldPollUserAddress = false;
|
||||
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress);
|
||||
this._zeroEx.setProvider(provider, networkId);
|
||||
await this._postInstantiationOrUpdatingProviderZeroExAsync();
|
||||
break;
|
||||
}
|
||||
|
||||
case ProviderType.Injected: {
|
||||
if (_.isUndefined(this._cachedProvider)) {
|
||||
return; // Going from injected to injected, so we noop
|
||||
}
|
||||
provider = this._cachedProvider;
|
||||
const shouldPollUserAddress = true;
|
||||
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress);
|
||||
this._zeroEx.setProvider(provider, this.networkId);
|
||||
await this._postInstantiationOrUpdatingProviderZeroExAsync();
|
||||
this._web3Wrapper.startEmittingNetworkConnectionAndUserBalanceState();
|
||||
this._dispatcher.updateProviderType(ProviderType.Ledger);
|
||||
}
|
||||
public async updateProviderToInjectedAsync() {
|
||||
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
|
||||
|
||||
if (_.isUndefined(this._cachedProvider)) {
|
||||
return; // Going from injected to injected, so we noop
|
||||
}
|
||||
|
||||
this._web3Wrapper.destroy();
|
||||
|
||||
const provider = this._cachedProvider;
|
||||
this.networkId = this._cachedProviderNetworkId;
|
||||
|
||||
const shouldPollUserAddress = true;
|
||||
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress);
|
||||
|
||||
this._userAddress = await this._web3Wrapper.getFirstAccountIfExistsAsync();
|
||||
|
||||
this._zeroEx.setProvider(provider, this.networkId);
|
||||
await this._postInstantiationOrUpdatingProviderZeroExAsync();
|
||||
|
||||
await this.fetchTokenInformationAsync();
|
||||
this._web3Wrapper.startEmittingNetworkConnectionAndUserBalanceState();
|
||||
this._dispatcher.updateProviderType(ProviderType.Injected);
|
||||
delete this._ledgerSubprovider;
|
||||
delete this._cachedProvider;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw utils.spawnSwitchErr('providerType', providerType);
|
||||
}
|
||||
|
||||
await this._fetchTokenInformationAsync();
|
||||
}
|
||||
public async setProxyAllowanceAsync(token: Token, amountInBaseUnits: BigNumber): Promise<void> {
|
||||
utils.assert(this.isValidAddress(token.address), BlockchainCallErrs.TokenAddressIsInvalid);
|
||||
utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses);
|
||||
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
|
||||
|
||||
this._showFlashMessageIfLedger();
|
||||
const txHash = await this._zeroEx.token.setProxyAllowanceAsync(
|
||||
token.address,
|
||||
this._userAddress,
|
||||
amountInBaseUnits,
|
||||
{
|
||||
gasPrice: this._defaultGasPrice,
|
||||
},
|
||||
);
|
||||
await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
|
||||
const allowance = amountInBaseUnits;
|
||||
this._dispatcher.replaceTokenAllowanceByAddress(token.address, allowance);
|
||||
}
|
||||
public async transferAsync(token: Token, toAddress: string, amountInBaseUnits: BigNumber): Promise<void> {
|
||||
this._showFlashMessageIfLedger();
|
||||
const txHash = await this._zeroEx.token.transferAsync(
|
||||
token.address,
|
||||
this._userAddress,
|
||||
toAddress,
|
||||
amountInBaseUnits,
|
||||
{
|
||||
gasPrice: this._defaultGasPrice,
|
||||
},
|
||||
);
|
||||
await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
|
||||
const etherScanLinkIfExists = utils.getEtherScanLinkIfExists(txHash, this.networkId, EtherscanLinkSuffixes.Tx);
|
||||
@ -309,11 +327,15 @@ export class Blockchain {
|
||||
|
||||
const shouldThrowOnInsufficientBalanceOrAllowance = true;
|
||||
|
||||
this._showFlashMessageIfLedger();
|
||||
const txHash = await this._zeroEx.exchange.fillOrderAsync(
|
||||
signedOrder,
|
||||
fillTakerTokenAmount,
|
||||
shouldThrowOnInsufficientBalanceOrAllowance,
|
||||
this._userAddress,
|
||||
{
|
||||
gasPrice: this._defaultGasPrice,
|
||||
},
|
||||
);
|
||||
const receipt = await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
|
||||
const logs: Array<LogWithDecodedArgs<ExchangeContractEventArgs>> = receipt.logs as any;
|
||||
@ -324,7 +346,10 @@ export class Blockchain {
|
||||
return filledTakerTokenAmount;
|
||||
}
|
||||
public async cancelOrderAsync(signedOrder: SignedOrder, cancelTakerTokenAmount: BigNumber): Promise<BigNumber> {
|
||||
const txHash = await this._zeroEx.exchange.cancelOrderAsync(signedOrder, cancelTakerTokenAmount);
|
||||
this._showFlashMessageIfLedger();
|
||||
const txHash = await this._zeroEx.exchange.cancelOrderAsync(signedOrder, cancelTakerTokenAmount, {
|
||||
gasPrice: this._defaultGasPrice,
|
||||
});
|
||||
const receipt = await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
|
||||
const logs: Array<LogWithDecodedArgs<ExchangeContractEventArgs>> = receipt.logs as any;
|
||||
this._zeroEx.exchange.throwLogErrorsAsErrors(logs);
|
||||
@ -368,22 +393,25 @@ export class Blockchain {
|
||||
|
||||
const [currBalance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, token.address);
|
||||
|
||||
this._zrxPollIntervalId = intervalUtils.setAsyncExcludingInterval(
|
||||
const newTokenBalancePromise = new Promise((resolve: (balance: BigNumber) => void, reject) => {
|
||||
const tokenPollInterval = intervalUtils.setAsyncExcludingInterval(
|
||||
async () => {
|
||||
const [balance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, token.address);
|
||||
if (!balance.eq(currBalance)) {
|
||||
this._dispatcher.replaceTokenBalanceByAddress(token.address, balance);
|
||||
intervalUtils.clearAsyncExcludingInterval(this._zrxPollIntervalId);
|
||||
delete this._zrxPollIntervalId;
|
||||
intervalUtils.clearAsyncExcludingInterval(tokenPollInterval);
|
||||
resolve(balance);
|
||||
}
|
||||
},
|
||||
5000,
|
||||
(err: Error) => {
|
||||
utils.consoleLog(`Polling tokenBalance failed: ${err}`);
|
||||
intervalUtils.clearAsyncExcludingInterval(this._zrxPollIntervalId);
|
||||
delete this._zrxPollIntervalId;
|
||||
intervalUtils.clearAsyncExcludingInterval(tokenPollInterval);
|
||||
reject(err);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return newTokenBalancePromise;
|
||||
}
|
||||
public async signOrderHashAsync(orderHash: string): Promise<SignatureData> {
|
||||
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
|
||||
@ -393,7 +421,21 @@ export class Blockchain {
|
||||
if (_.isUndefined(makerAddress)) {
|
||||
throw new Error('Tried to send a sign request but user has no associated addresses');
|
||||
}
|
||||
const ecSignature = await this._zeroEx.signOrderHashAsync(orderHash, makerAddress);
|
||||
|
||||
this._showFlashMessageIfLedger();
|
||||
const nodeVersion = await this._web3Wrapper.getNodeVersionAsync();
|
||||
const isParityNode = utils.isParityNode(nodeVersion);
|
||||
const isTestRpc = utils.isTestRpc(nodeVersion);
|
||||
const isLedgerSigner = !_.isUndefined(this._ledgerSubprovider);
|
||||
let shouldAddPersonalMessagePrefix = true;
|
||||
if ((isParityNode && !isLedgerSigner) || isTestRpc || isLedgerSigner) {
|
||||
shouldAddPersonalMessagePrefix = false;
|
||||
}
|
||||
const ecSignature = await this._zeroEx.signOrderHashAsync(
|
||||
orderHash,
|
||||
makerAddress,
|
||||
shouldAddPersonalMessagePrefix,
|
||||
);
|
||||
const signatureData = _.extend({}, ecSignature, {
|
||||
hash: orderHash,
|
||||
});
|
||||
@ -404,11 +446,11 @@ export class Blockchain {
|
||||
utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses);
|
||||
|
||||
const mintableContract = await this._instantiateContractIfExistsAsync(MintableArtifacts, token.address);
|
||||
this._showFlashMessageIfLedger();
|
||||
await mintableContract.mint(constants.MINT_AMOUNT, {
|
||||
from: this._userAddress,
|
||||
gasPrice: this._defaultGasPrice,
|
||||
});
|
||||
const balanceDelta = constants.MINT_AMOUNT;
|
||||
this._dispatcher.updateTokenBalanceByAddress(token.address, balanceDelta);
|
||||
}
|
||||
public async getBalanceInEthAsync(owner: string): Promise<BigNumber> {
|
||||
const balance = await this._web3Wrapper.getBalanceInEthAsync(owner);
|
||||
@ -418,14 +460,20 @@ export class Blockchain {
|
||||
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
|
||||
utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses);
|
||||
|
||||
const txHash = await this._zeroEx.etherToken.depositAsync(etherTokenAddress, amount, this._userAddress);
|
||||
this._showFlashMessageIfLedger();
|
||||
const txHash = await this._zeroEx.etherToken.depositAsync(etherTokenAddress, amount, this._userAddress, {
|
||||
gasPrice: this._defaultGasPrice,
|
||||
});
|
||||
await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
|
||||
}
|
||||
public async convertWrappedEthTokensToEthAsync(etherTokenAddress: string, amount: BigNumber): Promise<void> {
|
||||
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
|
||||
utils.assert(this._doesUserAddressExist(), BlockchainCallErrs.UserHasNoAssociatedAddresses);
|
||||
|
||||
const txHash = await this._zeroEx.etherToken.withdrawAsync(etherTokenAddress, amount, this._userAddress);
|
||||
this._showFlashMessageIfLedger();
|
||||
const txHash = await this._zeroEx.etherToken.withdrawAsync(etherTokenAddress, amount, this._userAddress, {
|
||||
gasPrice: this._defaultGasPrice,
|
||||
});
|
||||
await this._showEtherScanLinkAndAwaitTransactionMinedAsync(txHash);
|
||||
}
|
||||
public async doesContractExistAtAddressAsync(address: string) {
|
||||
@ -451,22 +499,6 @@ export class Blockchain {
|
||||
}
|
||||
return [balance, allowance];
|
||||
}
|
||||
public async updateTokenBalancesAndAllowancesAsync(tokens: Token[]) {
|
||||
const tokenStateByAddress: TokenStateByAddress = {};
|
||||
for (const token of tokens) {
|
||||
let balance = new BigNumber(0);
|
||||
let allowance = new BigNumber(0);
|
||||
if (this._doesUserAddressExist()) {
|
||||
[balance, allowance] = await this.getTokenBalanceAndAllowanceAsync(this._userAddress, token.address);
|
||||
}
|
||||
const tokenState = {
|
||||
balance,
|
||||
allowance,
|
||||
};
|
||||
tokenStateByAddress[token.address] = tokenState;
|
||||
}
|
||||
this._dispatcher.updateTokenStateByAddress(tokenStateByAddress);
|
||||
}
|
||||
public async getUserAccountsAsync() {
|
||||
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
|
||||
const userAccountsIfExists = await this._zeroEx.getAvailableAddressesAsync();
|
||||
@ -479,10 +511,59 @@ export class Blockchain {
|
||||
this._web3Wrapper.updatePrevUserAddress(newUserAddress);
|
||||
}
|
||||
public destroy() {
|
||||
intervalUtils.clearAsyncExcludingInterval(this._zrxPollIntervalId);
|
||||
this._web3Wrapper.destroy();
|
||||
this._stopWatchingExchangeLogFillEvents();
|
||||
}
|
||||
public async fetchTokenInformationAsync() {
|
||||
utils.assert(
|
||||
!_.isUndefined(this.networkId),
|
||||
'Cannot call fetchTokenInformationAsync if disconnected from Ethereum node',
|
||||
);
|
||||
|
||||
this._dispatcher.updateBlockchainIsLoaded(false);
|
||||
|
||||
const tokenRegistryTokensByAddress = await this._getTokenRegistryTokensByAddressAsync();
|
||||
|
||||
const trackedTokensByAddress = trackedTokenStorage.getTrackedTokensByAddress(this._userAddress, this.networkId);
|
||||
const tokenRegistryTokens = _.values(tokenRegistryTokensByAddress);
|
||||
if (_.isEmpty(trackedTokensByAddress)) {
|
||||
_.each(configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, symbol => {
|
||||
const token = _.find(tokenRegistryTokens, t => t.symbol === symbol);
|
||||
token.isTracked = true;
|
||||
trackedTokensByAddress[token.address] = token;
|
||||
});
|
||||
_.each(trackedTokensByAddress, (token: Token, address: string) => {
|
||||
trackedTokenStorage.addTrackedTokenToUser(this._userAddress, this.networkId, token);
|
||||
});
|
||||
} else {
|
||||
// Properly set all tokenRegistry tokens `isTracked` to true if they are in the existing trackedTokens array
|
||||
_.each(trackedTokensByAddress, (trackedToken: Token, address: string) => {
|
||||
if (!_.isUndefined(tokenRegistryTokensByAddress[address])) {
|
||||
tokenRegistryTokensByAddress[address].isTracked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
const allTokensByAddress = {
|
||||
...tokenRegistryTokensByAddress,
|
||||
...trackedTokensByAddress,
|
||||
};
|
||||
const allTokens = _.values(allTokensByAddress);
|
||||
const mostPopularTradingPairTokens: Token[] = [
|
||||
_.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[0] }),
|
||||
_.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[1] }),
|
||||
];
|
||||
const sideToAssetToken: SideToAssetToken = {
|
||||
[Side.Deposit]: {
|
||||
address: mostPopularTradingPairTokens[0].address,
|
||||
},
|
||||
[Side.Receive]: {
|
||||
address: mostPopularTradingPairTokens[1].address,
|
||||
},
|
||||
};
|
||||
this._dispatcher.batchDispatch(allTokensByAddress, this.networkId, this._userAddress, sideToAssetToken);
|
||||
|
||||
this._dispatcher.updateBlockchainIsLoaded(true);
|
||||
}
|
||||
private async _showEtherScanLinkAndAwaitTransactionMinedAsync(
|
||||
txHash: string,
|
||||
): Promise<TransactionReceiptWithDecodedLogs> {
|
||||
@ -665,17 +746,23 @@ export class Blockchain {
|
||||
}
|
||||
|
||||
const provider = await Blockchain._getProviderAsync(injectedWeb3, networkIdIfExists);
|
||||
const networkId = !_.isUndefined(networkIdIfExists)
|
||||
this.networkId = !_.isUndefined(networkIdIfExists)
|
||||
? networkIdIfExists
|
||||
: configs.IS_MAINNET_ENABLED ? constants.NETWORK_ID_MAINNET : constants.NETWORK_ID_TESTNET;
|
||||
: configs.IS_MAINNET_ENABLED ? constants.NETWORK_ID_MAINNET : constants.NETWORK_ID_KOVAN;
|
||||
this._dispatcher.updateNetworkId(this.networkId);
|
||||
const zeroExConfigs = {
|
||||
networkId,
|
||||
networkId: this.networkId,
|
||||
};
|
||||
this._zeroEx = new ZeroEx(provider, zeroExConfigs);
|
||||
this._updateProviderName(injectedWeb3);
|
||||
const shouldPollUserAddress = true;
|
||||
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, networkId, shouldPollUserAddress);
|
||||
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress);
|
||||
await this._postInstantiationOrUpdatingProviderZeroExAsync();
|
||||
this._userAddress = await this._web3Wrapper.getFirstAccountIfExistsAsync();
|
||||
this._dispatcher.updateUserAddress(this._userAddress);
|
||||
await this.fetchTokenInformationAsync();
|
||||
this._web3Wrapper.startEmittingNetworkConnectionAndUserBalanceState();
|
||||
await this._rehydrateStoreWithContractEvents();
|
||||
}
|
||||
// This method should always be run after instantiating or updating the provider
|
||||
// of the ZeroEx instance.
|
||||
@ -690,60 +777,6 @@ export class Blockchain {
|
||||
: constants.PROVIDER_NAME_PUBLIC;
|
||||
this._dispatcher.updateInjectedProviderName(providerName);
|
||||
}
|
||||
private async _fetchTokenInformationAsync() {
|
||||
utils.assert(
|
||||
!_.isUndefined(this.networkId),
|
||||
'Cannot call fetchTokenInformationAsync if disconnected from Ethereum node',
|
||||
);
|
||||
|
||||
this._dispatcher.updateBlockchainIsLoaded(false);
|
||||
this._dispatcher.clearTokenByAddress();
|
||||
|
||||
const tokenRegistryTokensByAddress = await this._getTokenRegistryTokensByAddressAsync();
|
||||
|
||||
// HACK: We need to fetch the userAddress here because otherwise we cannot save the
|
||||
// tracked tokens in localStorage under the users address nor fetch the token
|
||||
// balances and allowances and we need to do this in order not to trigger the blockchain
|
||||
// loading dialog to show up twice. First to load the contracts, and second to load the
|
||||
// balances and allowances.
|
||||
this._userAddress = await this._web3Wrapper.getFirstAccountIfExistsAsync();
|
||||
if (!_.isEmpty(this._userAddress)) {
|
||||
this._dispatcher.updateUserAddress(this._userAddress);
|
||||
}
|
||||
|
||||
let trackedTokensIfExists = trackedTokenStorage.getTrackedTokensIfExists(this._userAddress, this.networkId);
|
||||
const tokenRegistryTokens = _.values(tokenRegistryTokensByAddress);
|
||||
if (_.isUndefined(trackedTokensIfExists)) {
|
||||
trackedTokensIfExists = _.map(configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, symbol => {
|
||||
const token = _.find(tokenRegistryTokens, t => t.symbol === symbol);
|
||||
token.isTracked = true;
|
||||
return token;
|
||||
});
|
||||
_.each(trackedTokensIfExists, token => {
|
||||
trackedTokenStorage.addTrackedTokenToUser(this._userAddress, this.networkId, token);
|
||||
});
|
||||
} else {
|
||||
// Properly set all tokenRegistry tokens `isTracked` to true if they are in the existing trackedTokens array
|
||||
_.each(trackedTokensIfExists, trackedToken => {
|
||||
if (!_.isUndefined(tokenRegistryTokensByAddress[trackedToken.address])) {
|
||||
tokenRegistryTokensByAddress[trackedToken.address].isTracked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
const allTokens = _.uniq([...tokenRegistryTokens, ...trackedTokensIfExists]);
|
||||
this._dispatcher.updateTokenByAddress(allTokens);
|
||||
|
||||
// Get balance/allowance for tracked tokens
|
||||
await this.updateTokenBalancesAndAllowancesAsync(trackedTokensIfExists);
|
||||
|
||||
const mostPopularTradingPairTokens: Token[] = [
|
||||
_.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[0] }),
|
||||
_.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[1] }),
|
||||
];
|
||||
this._dispatcher.updateChosenAssetTokenAddress(Side.Deposit, mostPopularTradingPairTokens[0].address);
|
||||
this._dispatcher.updateChosenAssetTokenAddress(Side.Receive, mostPopularTradingPairTokens[1].address);
|
||||
this._dispatcher.updateBlockchainIsLoaded(true);
|
||||
}
|
||||
private async _instantiateContractIfExistsAsync(artifact: any, address?: string): Promise<ContractInstance> {
|
||||
const c = await contract(artifact);
|
||||
const providerObj = this._web3Wrapper.getProviderObj();
|
||||
@ -779,4 +812,20 @@ export class Blockchain {
|
||||
}
|
||||
}
|
||||
}
|
||||
private _showFlashMessageIfLedger() {
|
||||
if (!_.isUndefined(this._ledgerSubprovider)) {
|
||||
this._dispatcher.showFlashMessage('Confirm the transaction on your Ledger Nano S');
|
||||
}
|
||||
}
|
||||
private async _updateDefaultGasPriceAsync() {
|
||||
const endpoint = `${configs.BACKEND_BASE_URL}/eth_gas_station`;
|
||||
const response = await fetch(endpoint);
|
||||
if (response.status !== 200) {
|
||||
return; // noop and we keep hard-coded default
|
||||
}
|
||||
const gasInfo = await response.json();
|
||||
const gasPriceInGwei = new BigNumber(gasInfo.average / 10);
|
||||
const gasPriceInWei = gasPriceInGwei.mul(1000000000);
|
||||
this._defaultGasPrice = gasPriceInWei;
|
||||
}
|
||||
} // tslint:disable:max-file-line-count
|
||||
|
@ -3,7 +3,7 @@ import Dialog from 'material-ui/Dialog';
|
||||
import FlatButton from 'material-ui/FlatButton';
|
||||
import * as React from 'react';
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { BlockchainErrs } from 'ts/types';
|
||||
import { BlockchainErrs, Networks } from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
import { configs } from 'ts/utils/configs';
|
||||
import { constants } from 'ts/utils/constants';
|
||||
@ -129,7 +129,7 @@ export class BlockchainErrDialog extends React.Component<BlockchainErrDialogProp
|
||||
<div>
|
||||
The 0x smart contracts are not deployed on the Ethereum network you are currently connected to
|
||||
(network Id: {this.props.networkId}). In order to use the 0x portal dApp, please connect to the{' '}
|
||||
{constants.TESTNET_NAME} testnet (network Id: {constants.NETWORK_ID_TESTNET})
|
||||
{Networks.Kovan} testnet (network Id: {constants.NETWORK_ID_KOVAN})
|
||||
{configs.IS_MAINNET_ENABLED
|
||||
? ` or ${constants.MAINNET_NAME} (network Id: ${constants.NETWORK_ID_MAINNET}).`
|
||||
: `.`}
|
||||
|
@ -2,38 +2,55 @@ import { BigNumber } from '@0xproject/utils';
|
||||
import Dialog from 'material-ui/Dialog';
|
||||
import FlatButton from 'material-ui/FlatButton';
|
||||
import * as React from 'react';
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { EthAmountInput } from 'ts/components/inputs/eth_amount_input';
|
||||
import { TokenAmountInput } from 'ts/components/inputs/token_amount_input';
|
||||
import { Side, Token, TokenState } from 'ts/types';
|
||||
import { Side, Token } from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
|
||||
interface EthWethConversionDialogProps {
|
||||
blockchain: Blockchain;
|
||||
userAddress: string;
|
||||
networkId: number;
|
||||
direction: Side;
|
||||
onComplete: (direction: Side, value: BigNumber) => void;
|
||||
onCancelled: () => void;
|
||||
isOpen: boolean;
|
||||
token: Token;
|
||||
tokenState: TokenState;
|
||||
etherBalance: BigNumber;
|
||||
lastForceTokenStateRefetch: number;
|
||||
}
|
||||
|
||||
interface EthWethConversionDialogState {
|
||||
value?: BigNumber;
|
||||
shouldShowIncompleteErrs: boolean;
|
||||
hasErrors: boolean;
|
||||
isEthTokenBalanceLoaded: boolean;
|
||||
ethTokenBalance: BigNumber;
|
||||
}
|
||||
|
||||
export class EthWethConversionDialog extends React.Component<
|
||||
EthWethConversionDialogProps,
|
||||
EthWethConversionDialogState
|
||||
> {
|
||||
private _isUnmounted: boolean;
|
||||
constructor() {
|
||||
super();
|
||||
this._isUnmounted = false;
|
||||
this.state = {
|
||||
shouldShowIncompleteErrs: false,
|
||||
hasErrors: false,
|
||||
isEthTokenBalanceLoaded: false,
|
||||
ethTokenBalance: new BigNumber(0),
|
||||
};
|
||||
}
|
||||
public componentWillMount() {
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._fetchEthTokenBalanceAsync();
|
||||
}
|
||||
public componentWillUnmount() {
|
||||
this._isUnmounted = true;
|
||||
}
|
||||
public render() {
|
||||
const convertDialogActions = [
|
||||
<FlatButton key="cancel" label="Cancel" onTouchTap={this._onCancel.bind(this)} />,
|
||||
@ -72,8 +89,11 @@ export class EthWethConversionDialog extends React.Component<
|
||||
<div className="pt2 mx-auto" style={{ width: 245 }}>
|
||||
{this.props.direction === Side.Receive ? (
|
||||
<TokenAmountInput
|
||||
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
||||
blockchain={this.props.blockchain}
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
token={this.props.token}
|
||||
tokenState={this.props.tokenState}
|
||||
shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs}
|
||||
shouldCheckBalance={true}
|
||||
shouldCheckAllowance={false}
|
||||
@ -93,7 +113,8 @@ export class EthWethConversionDialog extends React.Component<
|
||||
)}
|
||||
<div className="pt1" style={{ fontSize: 12 }}>
|
||||
<div className="left">1 ETH = 1 WETH</div>
|
||||
{this.props.direction === Side.Receive && (
|
||||
{this.props.direction === Side.Receive &&
|
||||
this.state.isEthTokenBalanceLoaded && (
|
||||
<div
|
||||
className="right"
|
||||
onClick={this._onMaxClick.bind(this)}
|
||||
@ -132,7 +153,7 @@ export class EthWethConversionDialog extends React.Component<
|
||||
}
|
||||
private _onMaxClick() {
|
||||
this.setState({
|
||||
value: this.props.tokenState.balance,
|
||||
value: this.state.ethTokenBalance,
|
||||
});
|
||||
}
|
||||
private _onValueChange(isValid: boolean, amount?: BigNumber) {
|
||||
@ -160,4 +181,16 @@ export class EthWethConversionDialog extends React.Component<
|
||||
});
|
||||
this.props.onCancelled();
|
||||
}
|
||||
private async _fetchEthTokenBalanceAsync() {
|
||||
const [balance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
|
||||
this.props.userAddress,
|
||||
this.props.token.address,
|
||||
);
|
||||
if (!this._isUnmounted) {
|
||||
this.setState({
|
||||
isEthTokenBalanceLoaded: true,
|
||||
ethTokenBalance: balance,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,8 +7,10 @@ import TextField from 'material-ui/TextField';
|
||||
import * as React from 'react';
|
||||
import ReactTooltip = require('react-tooltip');
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { NetworkDropDown } from 'ts/components/dropdowns/network_drop_down';
|
||||
import { LifeCycleRaisedButton } from 'ts/components/ui/lifecycle_raised_button';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { ProviderType } from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
import { configs } from 'ts/utils/configs';
|
||||
import { constants } from 'ts/utils/constants';
|
||||
@ -27,27 +29,33 @@ interface LedgerConfigDialogProps {
|
||||
dispatcher: Dispatcher;
|
||||
blockchain: Blockchain;
|
||||
networkId: number;
|
||||
providerType: ProviderType;
|
||||
}
|
||||
|
||||
interface LedgerConfigDialogState {
|
||||
didConnectFail: boolean;
|
||||
connectionErrMsg: string;
|
||||
stepIndex: LedgerSteps;
|
||||
userAddresses: string[];
|
||||
addressBalances: BigNumber[];
|
||||
derivationPath: string;
|
||||
derivationErrMsg: string;
|
||||
preferredNetworkId: number;
|
||||
}
|
||||
|
||||
export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, LedgerConfigDialogState> {
|
||||
constructor(props: LedgerConfigDialogProps) {
|
||||
super(props);
|
||||
const derivationPathIfExists = props.blockchain.getLedgerDerivationPathIfExists();
|
||||
this.state = {
|
||||
didConnectFail: false,
|
||||
connectionErrMsg: '',
|
||||
stepIndex: LedgerSteps.CONNECT,
|
||||
userAddresses: [],
|
||||
addressBalances: [],
|
||||
derivationPath: configs.DEFAULT_DERIVATION_PATH,
|
||||
derivationPath: _.isUndefined(derivationPathIfExists)
|
||||
? configs.DEFAULT_DERIVATION_PATH
|
||||
: derivationPathIfExists,
|
||||
derivationErrMsg: '',
|
||||
preferredNetworkId: props.networkId,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
@ -74,19 +82,28 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
|
||||
);
|
||||
}
|
||||
private _renderConnectStep() {
|
||||
const networkIds = _.values(constants.NETWORK_ID_BY_NAME);
|
||||
return (
|
||||
<div>
|
||||
<div className="h4 pt3">Follow these instructions before proceeding:</div>
|
||||
<ol>
|
||||
<ol className="mb0">
|
||||
<li className="pb1">Connect your Ledger Nano S & Open the Ethereum application</li>
|
||||
<li className="pb1">Verify that Browser Support is enabled in Settings</li>
|
||||
<li className="pb1">Verify that "Browser Support" AND "Contract Data" are enabled in Settings</li>
|
||||
<li className="pb1">
|
||||
If no Browser Support is found in settings, verify that you have{' '}
|
||||
<a href="https://www.ledgerwallet.com/apps/manager" target="_blank">
|
||||
Firmware >1.2
|
||||
</a>
|
||||
</li>
|
||||
<li>Choose your desired network:</li>
|
||||
</ol>
|
||||
<div className="pb2">
|
||||
<NetworkDropDown
|
||||
updateSelectedNetwork={this._onSelectedNetworkUpdated.bind(this)}
|
||||
selectedNetworkId={this.state.preferredNetworkId}
|
||||
avialableNetworkIds={networkIds}
|
||||
/>
|
||||
</div>
|
||||
<div className="center pb3">
|
||||
<LifeCycleRaisedButton
|
||||
isPrimary={true}
|
||||
@ -95,9 +112,9 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
|
||||
labelComplete="Connected!"
|
||||
onClickAsyncFn={this._onConnectLedgerClickAsync.bind(this, true)}
|
||||
/>
|
||||
{this.state.didConnectFail && (
|
||||
{!_.isEmpty(this.state.connectionErrMsg) && (
|
||||
<div className="pt2 left-align" style={{ color: colors.red200 }}>
|
||||
Failed to connect. Follow the instructions and try again.
|
||||
{this.state.connectionErrMsg}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -172,7 +189,8 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
|
||||
}
|
||||
private _onClose() {
|
||||
this.setState({
|
||||
didConnectFail: false,
|
||||
connectionErrMsg: '',
|
||||
stepIndex: LedgerSteps.CONNECT,
|
||||
});
|
||||
const isOpen = false;
|
||||
this.props.toggleDialogFn(isOpen);
|
||||
@ -184,6 +202,8 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
|
||||
const selectAddressBalance = this.state.addressBalances[selectedRowIndex];
|
||||
this.props.dispatcher.updateUserAddress(selectedAddress);
|
||||
this.props.blockchain.updateWeb3WrapperPrevUserAddress(selectedAddress);
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this.props.blockchain.fetchTokenInformationAsync();
|
||||
this.props.dispatcher.updateUserEtherBalance(selectAddressBalance);
|
||||
this.setState({
|
||||
stepIndex: LedgerSteps.CONNECT,
|
||||
@ -219,7 +239,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
|
||||
} catch (err) {
|
||||
utils.consoleLog(`Ledger error: ${JSON.stringify(err)}`);
|
||||
this.setState({
|
||||
didConnectFail: true,
|
||||
connectionErrMsg: 'Failed to connect. Follow the instructions and try again.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@ -241,6 +261,22 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
|
||||
});
|
||||
}
|
||||
private async _onConnectLedgerClickAsync() {
|
||||
const isU2FSupported = await utils.isU2FSupportedAsync();
|
||||
if (!isU2FSupported) {
|
||||
utils.consoleLog(`U2F not supported in this browser`);
|
||||
this.setState({
|
||||
connectionErrMsg: 'U2F not supported by this browser. Try using Chrome.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.props.providerType !== ProviderType.Ledger ||
|
||||
(this.props.providerType === ProviderType.Ledger && this.props.networkId !== this.state.preferredNetworkId)
|
||||
) {
|
||||
await this.props.blockchain.updateProviderToLedgerAsync(this.state.preferredNetworkId);
|
||||
}
|
||||
|
||||
const didSucceed = await this._fetchAddressesAndBalancesAsync();
|
||||
if (didSucceed) {
|
||||
this.setState({
|
||||
@ -258,4 +294,9 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
|
||||
}
|
||||
return userAddresses;
|
||||
}
|
||||
private _onSelectedNetworkUpdated(e: any, index: number, networkId: number) {
|
||||
this.setState({
|
||||
preferredNetworkId: networkId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -3,16 +3,20 @@ import * as _ from 'lodash';
|
||||
import Dialog from 'material-ui/Dialog';
|
||||
import FlatButton from 'material-ui/FlatButton';
|
||||
import * as React from 'react';
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { AddressInput } from 'ts/components/inputs/address_input';
|
||||
import { TokenAmountInput } from 'ts/components/inputs/token_amount_input';
|
||||
import { Token, TokenState } from 'ts/types';
|
||||
import { Token } from 'ts/types';
|
||||
|
||||
interface SendDialogProps {
|
||||
blockchain: Blockchain;
|
||||
userAddress: string;
|
||||
networkId: number;
|
||||
onComplete: (recipient: string, value: BigNumber) => void;
|
||||
onCancelled: () => void;
|
||||
isOpen: boolean;
|
||||
token: Token;
|
||||
tokenState: TokenState;
|
||||
lastForceTokenStateRefetch: number;
|
||||
}
|
||||
|
||||
interface SendDialogState {
|
||||
@ -66,15 +70,18 @@ export class SendDialog extends React.Component<SendDialogProps, SendDialogState
|
||||
/>
|
||||
</div>
|
||||
<TokenAmountInput
|
||||
blockchain={this.props.blockchain}
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
label="Amount to send"
|
||||
token={this.props.token}
|
||||
tokenState={this.props.tokenState}
|
||||
shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs}
|
||||
shouldCheckBalance={true}
|
||||
shouldCheckAllowance={false}
|
||||
onChange={this._onValueChange.bind(this)}
|
||||
amount={this.state.value}
|
||||
onVisitBalancesPageClick={this.props.onCancelled}
|
||||
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -82,16 +82,6 @@ export class TrackTokenConfirmationDialog extends React.Component<
|
||||
newTokenEntry.isTracked = true;
|
||||
trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newTokenEntry);
|
||||
this.props.dispatcher.updateTokenByAddress([newTokenEntry]);
|
||||
|
||||
const [balance, allowance] = await this.props.blockchain.getCurrentUserTokenBalanceAndAllowanceAsync(
|
||||
token.address,
|
||||
);
|
||||
this.props.dispatcher.updateTokenStateByAddress({
|
||||
[token.address]: {
|
||||
balance,
|
||||
allowance,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -6,21 +6,24 @@ import * as React from 'react';
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { EthWethConversionDialog } from 'ts/components/dialogs/eth_weth_conversion_dialog';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { BlockchainCallErrs, Side, Token, TokenState } from 'ts/types';
|
||||
import { BlockchainCallErrs, Side, Token } from 'ts/types';
|
||||
import { constants } from 'ts/utils/constants';
|
||||
import { errorReporter } from 'ts/utils/error_reporter';
|
||||
import { utils } from 'ts/utils/utils';
|
||||
|
||||
interface EthWethConversionButtonProps {
|
||||
userAddress: string;
|
||||
networkId: number;
|
||||
direction: Side;
|
||||
ethToken: Token;
|
||||
ethTokenState: TokenState;
|
||||
dispatcher: Dispatcher;
|
||||
blockchain: Blockchain;
|
||||
userEtherBalance: BigNumber;
|
||||
isOutdatedWrappedEther: boolean;
|
||||
onConversionSuccessful?: () => void;
|
||||
isDisabled?: boolean;
|
||||
lastForceTokenStateRefetch: number;
|
||||
refetchEthTokenStateAsync: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface EthWethConversionButtonState {
|
||||
@ -64,13 +67,16 @@ export class EthWethConversionButton extends React.Component<
|
||||
onClick={this._toggleConversionDialog.bind(this)}
|
||||
/>
|
||||
<EthWethConversionDialog
|
||||
blockchain={this.props.blockchain}
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
direction={this.props.direction}
|
||||
isOpen={this.state.isEthConversionDialogVisible}
|
||||
onComplete={this._onConversionAmountSelectedAsync.bind(this)}
|
||||
onCancelled={this._toggleConversionDialog.bind(this)}
|
||||
etherBalance={this.props.userEtherBalance}
|
||||
token={this.props.ethToken}
|
||||
tokenState={this.props.ethTokenState}
|
||||
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -86,29 +92,25 @@ export class EthWethConversionButton extends React.Component<
|
||||
});
|
||||
this._toggleConversionDialog();
|
||||
const token = this.props.ethToken;
|
||||
const tokenState = this.props.ethTokenState;
|
||||
let balance = tokenState.balance;
|
||||
try {
|
||||
if (direction === Side.Deposit) {
|
||||
await this.props.blockchain.convertEthToWrappedEthTokensAsync(token.address, value);
|
||||
const ethAmount = ZeroEx.toUnitAmount(value, constants.DECIMAL_PLACES_ETH);
|
||||
this.props.dispatcher.showFlashMessage(`Successfully wrapped ${ethAmount.toString()} ETH to WETH`);
|
||||
balance = balance.plus(value);
|
||||
} else {
|
||||
await this.props.blockchain.convertWrappedEthTokensToEthAsync(token.address, value);
|
||||
const tokenAmount = ZeroEx.toUnitAmount(value, token.decimals);
|
||||
this.props.dispatcher.showFlashMessage(`Successfully unwrapped ${tokenAmount.toString()} WETH to ETH`);
|
||||
balance = balance.minus(value);
|
||||
}
|
||||
if (!this.props.isOutdatedWrappedEther) {
|
||||
this.props.dispatcher.replaceTokenBalanceByAddress(token.address, balance);
|
||||
await this.props.refetchEthTokenStateAsync();
|
||||
}
|
||||
this.props.onConversionSuccessful();
|
||||
} catch (err) {
|
||||
const errMsg = `${err}`;
|
||||
if (_.includes(errMsg, BlockchainCallErrs.UserHasNoAssociatedAddresses)) {
|
||||
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
|
||||
} else if (!_.includes(errMsg, 'User denied transaction')) {
|
||||
} else if (!utils.didUserDenyWeb3Request(errMsg)) {
|
||||
utils.consoleLog(`Unexpected error encountered: ${err}`);
|
||||
utils.consoleLog(err.stack);
|
||||
const errorMsg =
|
||||
|
@ -16,7 +16,6 @@ import {
|
||||
Token,
|
||||
TokenByAddress,
|
||||
TokenState,
|
||||
TokenStateByAddress,
|
||||
} from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
import { configs } from 'ts/utils/configs';
|
||||
@ -41,19 +40,23 @@ interface EthWrappersProps {
|
||||
blockchain: Blockchain;
|
||||
dispatcher: Dispatcher;
|
||||
tokenByAddress: TokenByAddress;
|
||||
tokenStateByAddress: TokenStateByAddress;
|
||||
userAddress: string;
|
||||
userEtherBalance: BigNumber;
|
||||
lastForceTokenStateRefetch: number;
|
||||
}
|
||||
|
||||
interface EthWrappersState {
|
||||
ethTokenState: TokenState;
|
||||
isWethStateLoaded: boolean;
|
||||
outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded;
|
||||
outdatedWETHStateByAddress: OutdatedWETHStateByAddress;
|
||||
}
|
||||
|
||||
export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersState> {
|
||||
private _isUnmounted: boolean;
|
||||
constructor(props: EthWrappersProps) {
|
||||
super(props);
|
||||
this._isUnmounted = false;
|
||||
const outdatedWETHAddresses = this._getOutdatedWETHAddresses();
|
||||
const outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded = {};
|
||||
const outdatedWETHStateByAddress: OutdatedWETHStateByAddress = {};
|
||||
@ -67,18 +70,34 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
|
||||
this.state = {
|
||||
outdatedWETHAddressToIsStateLoaded,
|
||||
outdatedWETHStateByAddress,
|
||||
isWethStateLoaded: false,
|
||||
ethTokenState: {
|
||||
balance: new BigNumber(0),
|
||||
allowance: new BigNumber(0),
|
||||
},
|
||||
};
|
||||
}
|
||||
public componentWillReceiveProps(nextProps: EthWrappersProps) {
|
||||
if (
|
||||
nextProps.userAddress !== this.props.userAddress ||
|
||||
nextProps.networkId !== this.props.networkId ||
|
||||
nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch
|
||||
) {
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._fetchWETHStateAsync();
|
||||
}
|
||||
}
|
||||
public componentDidMount() {
|
||||
window.scrollTo(0, 0);
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._fetchOutdatedWETHStateAsync();
|
||||
this._fetchWETHStateAsync();
|
||||
}
|
||||
public componentWillUnmount() {
|
||||
this._isUnmounted = true;
|
||||
}
|
||||
public render() {
|
||||
const tokens = _.values(this.props.tokenByAddress);
|
||||
const etherToken = _.find(tokens, { symbol: 'WETH' });
|
||||
const etherTokenState = this.props.tokenStateByAddress[etherToken.address];
|
||||
const wethBalance = ZeroEx.toUnitAmount(etherTokenState.balance, constants.DECIMAL_PLACES_ETH);
|
||||
const etherToken = this._getEthToken();
|
||||
const wethBalance = ZeroEx.toUnitAmount(this.state.ethTokenState.balance, constants.DECIMAL_PLACES_ETH);
|
||||
const isBidirectional = true;
|
||||
const etherscanUrl = utils.getEtherScanLinkIfExists(
|
||||
etherToken.address,
|
||||
@ -136,10 +155,13 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
|
||||
</TableRowColumn>
|
||||
<TableRowColumn>
|
||||
<EthWethConversionButton
|
||||
refetchEthTokenStateAsync={this._refetchEthTokenStateAsync.bind(this)}
|
||||
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
isOutdatedWrappedEther={false}
|
||||
direction={Side.Deposit}
|
||||
ethToken={etherToken}
|
||||
ethTokenState={etherTokenState}
|
||||
dispatcher={this.props.dispatcher}
|
||||
blockchain={this.props.blockchain}
|
||||
userEtherBalance={this.props.userEtherBalance}
|
||||
@ -150,13 +172,23 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
|
||||
<TableRowColumn className="py1">
|
||||
{this._renderTokenLink(tokenLabel, etherscanUrl)}
|
||||
</TableRowColumn>
|
||||
<TableRowColumn>{wethBalance.toFixed(PRECISION)} WETH</TableRowColumn>
|
||||
<TableRowColumn>
|
||||
{this.state.isWethStateLoaded ? (
|
||||
`${wethBalance.toFixed(PRECISION)} WETH`
|
||||
) : (
|
||||
<i className="zmdi zmdi-spinner zmdi-hc-spin" />
|
||||
)}
|
||||
</TableRowColumn>
|
||||
<TableRowColumn>
|
||||
<EthWethConversionButton
|
||||
refetchEthTokenStateAsync={this._refetchEthTokenStateAsync.bind(this)}
|
||||
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
isOutdatedWrappedEther={false}
|
||||
direction={Side.Receive}
|
||||
isDisabled={!this.state.isWethStateLoaded}
|
||||
ethToken={etherToken}
|
||||
ethTokenState={etherTokenState}
|
||||
dispatcher={this.props.dispatcher}
|
||||
blockchain={this.props.blockchain}
|
||||
userEtherBalance={this.props.userEtherBalance}
|
||||
@ -190,7 +222,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody displayRowCheckbox={false}>
|
||||
{this._renderOutdatedWeths(etherToken, etherTokenState)}
|
||||
{this._renderOutdatedWeths(etherToken, this.state.ethTokenState)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
@ -269,6 +301,10 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
|
||||
</TableRowColumn>
|
||||
<TableRowColumn>
|
||||
<EthWethConversionButton
|
||||
refetchEthTokenStateAsync={this._refetchEthTokenStateAsync.bind(this)}
|
||||
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
isDisabled={!isStateLoaded}
|
||||
isOutdatedWrappedEther={true}
|
||||
direction={Side.Receive}
|
||||
@ -338,7 +374,14 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
|
||||
},
|
||||
});
|
||||
}
|
||||
private async _fetchOutdatedWETHStateAsync() {
|
||||
private async _fetchWETHStateAsync() {
|
||||
const tokens = _.values(this.props.tokenByAddress);
|
||||
const wethToken = _.find(tokens, token => token.symbol === 'WETH');
|
||||
const [wethBalance, wethAllowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
|
||||
this.props.userAddress,
|
||||
wethToken.address,
|
||||
);
|
||||
|
||||
const outdatedWETHAddresses = this._getOutdatedWETHAddresses();
|
||||
const outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded = {};
|
||||
const outdatedWETHStateByAddress: OutdatedWETHStateByAddress = {};
|
||||
@ -353,11 +396,18 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
|
||||
};
|
||||
outdatedWETHAddressToIsStateLoaded[address] = true;
|
||||
}
|
||||
if (!this._isUnmounted) {
|
||||
this.setState({
|
||||
outdatedWETHStateByAddress,
|
||||
outdatedWETHAddressToIsStateLoaded,
|
||||
ethTokenState: {
|
||||
balance: wethBalance,
|
||||
allowance: wethAllowance,
|
||||
},
|
||||
isWethStateLoaded: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
private _getOutdatedWETHAddresses(): string[] {
|
||||
const outdatedWETHAddresses = _.compact(
|
||||
_.map(configs.OUTDATED_WRAPPED_ETHERS, outdatedWrappedEtherByNetwork => {
|
||||
@ -371,4 +421,22 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt
|
||||
);
|
||||
return outdatedWETHAddresses;
|
||||
}
|
||||
private _getEthToken() {
|
||||
const tokens = _.values(this.props.tokenByAddress);
|
||||
const etherToken = _.find(tokens, { symbol: 'WETH' });
|
||||
return etherToken;
|
||||
}
|
||||
private async _refetchEthTokenStateAsync() {
|
||||
const etherToken = this._getEthToken();
|
||||
const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
|
||||
this.props.userAddress,
|
||||
etherToken.address,
|
||||
);
|
||||
this.setState({
|
||||
ethTokenState: {
|
||||
balance,
|
||||
allowance,
|
||||
},
|
||||
});
|
||||
}
|
||||
} // tslint:disable:max-file-line-count
|
||||
|
@ -19,7 +19,7 @@ import { VisualOrder } from 'ts/components/visual_order';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { orderSchema } from 'ts/schemas/order_schema';
|
||||
import { SchemaValidator } from 'ts/schemas/validator';
|
||||
import { AlertTypes, BlockchainErrs, Order, Token, TokenByAddress, TokenStateByAddress, WebsitePaths } from 'ts/types';
|
||||
import { AlertTypes, BlockchainErrs, Order, Token, TokenByAddress, WebsitePaths } from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
import { constants } from 'ts/utils/constants';
|
||||
import { errorReporter } from 'ts/utils/error_reporter';
|
||||
@ -33,9 +33,9 @@ interface FillOrderProps {
|
||||
networkId: number;
|
||||
userAddress: string;
|
||||
tokenByAddress: TokenByAddress;
|
||||
tokenStateByAddress: TokenStateByAddress;
|
||||
initialOrder: Order;
|
||||
dispatcher: Dispatcher;
|
||||
lastForceTokenStateRefetch: number;
|
||||
}
|
||||
|
||||
interface FillOrderState {
|
||||
@ -59,8 +59,10 @@ interface FillOrderState {
|
||||
|
||||
export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
|
||||
private _validator: SchemaValidator;
|
||||
private _isUnmounted: boolean;
|
||||
constructor(props: FillOrderProps) {
|
||||
super(props);
|
||||
this._isUnmounted = false;
|
||||
this.state = {
|
||||
globalErrMsg: '',
|
||||
didOrderValidationRun: false,
|
||||
@ -90,6 +92,9 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
|
||||
public componentDidMount() {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
public componentWillUnmount() {
|
||||
this._isUnmounted = true;
|
||||
}
|
||||
public render() {
|
||||
return (
|
||||
<div className="clearfix lg-px4 md-px4 sm-px2" style={{ minHeight: 600 }}>
|
||||
@ -185,7 +190,6 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
|
||||
symbol: takerToken.symbol,
|
||||
};
|
||||
const fillToken = this.props.tokenByAddress[takerToken.address];
|
||||
const fillTokenState = this.props.tokenStateByAddress[takerToken.address];
|
||||
const makerTokenAddress = this.state.parsedOrder.maker.token.address;
|
||||
const makerToken = this.props.tokenByAddress[makerTokenAddress];
|
||||
const makerAssetToken = {
|
||||
@ -249,14 +253,17 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
|
||||
{!isUserMaker && (
|
||||
<div className="clearfix mx-auto relative" style={{ width: 235, height: 108 }}>
|
||||
<TokenAmountInput
|
||||
blockchain={this.props.blockchain}
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
label="Fill amount"
|
||||
onChange={this._onFillAmountChange.bind(this)}
|
||||
shouldShowIncompleteErrs={false}
|
||||
token={fillToken}
|
||||
tokenState={fillTokenState}
|
||||
amount={fillAssetToken.amount}
|
||||
shouldCheckBalance={true}
|
||||
shouldCheckAllowance={true}
|
||||
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
||||
/>
|
||||
<div
|
||||
className="absolute sm-hide xs-hide"
|
||||
@ -454,12 +461,14 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
|
||||
if (!_.isEmpty(orderJSON)) {
|
||||
orderJSONErrMsg = 'Submitted order JSON is not valid JSON';
|
||||
}
|
||||
if (!this._isUnmounted) {
|
||||
this.setState({
|
||||
didOrderValidationRun: true,
|
||||
orderJSON,
|
||||
orderJSONErrMsg,
|
||||
parsedOrder,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -556,11 +565,8 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
|
||||
signedOrder,
|
||||
this.props.orderFillAmount,
|
||||
);
|
||||
// After fill completes, let's update the token balances
|
||||
const makerToken = this.props.tokenByAddress[parsedOrder.maker.token.address];
|
||||
const takerToken = this.props.tokenByAddress[parsedOrder.taker.token.address];
|
||||
const tokens = [makerToken, takerToken];
|
||||
await this.props.blockchain.updateTokenBalancesAndAllowancesAsync(tokens);
|
||||
// After fill completes, let's force fetch the token balances
|
||||
this.props.dispatcher.forceTokenStateRefetch();
|
||||
this.setState({
|
||||
isFilling: false,
|
||||
didFillOrderSucceed: true,
|
||||
@ -573,7 +579,7 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
|
||||
isFilling: false,
|
||||
});
|
||||
const errMsg = `${err}`;
|
||||
if (_.includes(errMsg, 'User denied transaction signature')) {
|
||||
if (utils.didUserDenyWeb3Request(errMsg)) {
|
||||
return;
|
||||
}
|
||||
globalErrMsg = 'Failed to fill order, please refresh and try again';
|
||||
@ -653,7 +659,7 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> {
|
||||
isCancelling: false,
|
||||
});
|
||||
const errMsg = `${err}`;
|
||||
if (_.includes(errMsg, 'User denied transaction signature')) {
|
||||
if (utils.didUserDenyWeb3Request(errMsg)) {
|
||||
return;
|
||||
}
|
||||
globalErrMsg = 'Failed to cancel order, please refresh and try again';
|
||||
|
@ -8,7 +8,7 @@ import { TrackTokenConfirmation } from 'ts/components/track_token_confirmation';
|
||||
import { TokenIcon } from 'ts/components/ui/token_icon';
|
||||
import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { DialogConfigs, Token, TokenByAddress, TokenState, TokenVisibility } from 'ts/types';
|
||||
import { DialogConfigs, Token, TokenByAddress, TokenVisibility } from 'ts/types';
|
||||
|
||||
const TOKEN_ICON_DIMENSION = 100;
|
||||
const TILE_DIMENSION = 146;
|
||||
@ -223,10 +223,7 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt
|
||||
assetView: AssetViews.NEW_TOKEN_FORM,
|
||||
});
|
||||
}
|
||||
private _onNewTokenSubmitted(newToken: Token, newTokenState: TokenState) {
|
||||
this.props.dispatcher.updateTokenStateByAddress({
|
||||
[newToken.address]: newTokenState,
|
||||
});
|
||||
private _onNewTokenSubmitted(newToken: Token) {
|
||||
trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newToken);
|
||||
this.props.dispatcher.addTokenToTokenByAddress(newToken);
|
||||
this.setState({
|
||||
@ -256,15 +253,6 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt
|
||||
newTokenEntry.isTracked = true;
|
||||
trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newTokenEntry);
|
||||
|
||||
const [balance, allowance] = await this.props.blockchain.getCurrentUserTokenBalanceAndAllowanceAsync(
|
||||
token.address,
|
||||
);
|
||||
this.props.dispatcher.updateTokenStateByAddress({
|
||||
[token.address]: {
|
||||
balance,
|
||||
allowance,
|
||||
},
|
||||
});
|
||||
this.props.dispatcher.updateTokenByAddress([newTokenEntry]);
|
||||
this.setState({
|
||||
isAddingTokenToTracked: false,
|
||||
|
@ -27,7 +27,6 @@ import {
|
||||
SignatureData,
|
||||
Token,
|
||||
TokenByAddress,
|
||||
TokenStateByAddress,
|
||||
} from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
import { errorReporter } from 'ts/utils/error_reporter';
|
||||
@ -53,7 +52,7 @@ interface GenerateOrderFormProps {
|
||||
orderSalt: BigNumber;
|
||||
sideToAssetToken: SideToAssetToken;
|
||||
tokenByAddress: TokenByAddress;
|
||||
tokenStateByAddress: TokenStateByAddress;
|
||||
lastForceTokenStateRefetch: number;
|
||||
}
|
||||
|
||||
interface GenerateOrderFormState {
|
||||
@ -80,10 +79,8 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G
|
||||
const dispatcher = this.props.dispatcher;
|
||||
const depositTokenAddress = this.props.sideToAssetToken[Side.Deposit].address;
|
||||
const depositToken = this.props.tokenByAddress[depositTokenAddress];
|
||||
const depositTokenState = this.props.tokenStateByAddress[depositTokenAddress];
|
||||
const receiveTokenAddress = this.props.sideToAssetToken[Side.Receive].address;
|
||||
const receiveToken = this.props.tokenByAddress[receiveTokenAddress];
|
||||
const receiveTokenState = this.props.tokenStateByAddress[receiveTokenAddress];
|
||||
const takerExplanation =
|
||||
'If a taker is specified, only they are<br> \
|
||||
allowed to fill this order. If no taker is<br> \
|
||||
@ -110,9 +107,12 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G
|
||||
tokenByAddress={this.props.tokenByAddress}
|
||||
/>
|
||||
<TokenAmountInput
|
||||
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
||||
blockchain={this.props.blockchain}
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
label="Sell amount"
|
||||
token={depositToken}
|
||||
tokenState={depositTokenState}
|
||||
amount={this.props.sideToAssetToken[Side.Deposit].amount}
|
||||
onChange={this._onTokenAmountChange.bind(this, depositToken, Side.Deposit)}
|
||||
shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs}
|
||||
@ -139,9 +139,12 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G
|
||||
tokenByAddress={this.props.tokenByAddress}
|
||||
/>
|
||||
<TokenAmountInput
|
||||
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
||||
blockchain={this.props.blockchain}
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
label="Receive amount"
|
||||
token={receiveToken}
|
||||
tokenState={receiveTokenState}
|
||||
amount={this.props.sideToAssetToken[Side.Receive].amount}
|
||||
onChange={this._onTokenAmountChange.bind(this, receiveToken, Side.Receive)}
|
||||
shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs}
|
||||
@ -242,8 +245,10 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G
|
||||
|
||||
// Check if all required inputs were supplied
|
||||
const debitToken = this.props.sideToAssetToken[Side.Deposit];
|
||||
const debitBalance = this.props.tokenStateByAddress[debitToken.address].balance;
|
||||
const debitAllowance = this.props.tokenStateByAddress[debitToken.address].allowance;
|
||||
const [debitBalance, debitAllowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
|
||||
this.props.userAddress,
|
||||
debitToken.address,
|
||||
);
|
||||
const receiveAmount = this.props.sideToAssetToken[Side.Receive].amount;
|
||||
if (
|
||||
!_.isUndefined(debitToken.amount) &&
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { BigNumber } from '@0xproject/utils';
|
||||
import * as _ from 'lodash';
|
||||
import TextField from 'material-ui/TextField';
|
||||
import * as React from 'react';
|
||||
@ -7,13 +6,13 @@ import { AddressInput } from 'ts/components/inputs/address_input';
|
||||
import { Alert } from 'ts/components/ui/alert';
|
||||
import { LifeCycleRaisedButton } from 'ts/components/ui/lifecycle_raised_button';
|
||||
import { RequiredLabel } from 'ts/components/ui/required_label';
|
||||
import { AlertTypes, Token, TokenByAddress, TokenState } from 'ts/types';
|
||||
import { AlertTypes, Token, TokenByAddress } from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
|
||||
interface NewTokenFormProps {
|
||||
blockchain: Blockchain;
|
||||
tokenByAddress: TokenByAddress;
|
||||
onNewTokenSubmitted: (token: Token, tokenState: TokenState) => void;
|
||||
onNewTokenSubmitted: (token: Token) => void;
|
||||
}
|
||||
|
||||
interface NewTokenFormState {
|
||||
@ -110,13 +109,9 @@ export class NewTokenForm extends React.Component<NewTokenFormProps, NewTokenFor
|
||||
}
|
||||
|
||||
let hasBalanceAllowanceErr = false;
|
||||
let balance = new BigNumber(0);
|
||||
let allowance = new BigNumber(0);
|
||||
if (doesContractExist) {
|
||||
try {
|
||||
[balance, allowance] = await this.props.blockchain.getCurrentUserTokenBalanceAndAllowanceAsync(
|
||||
this.state.address,
|
||||
);
|
||||
await this.props.blockchain.getCurrentUserTokenBalanceAndAllowanceAsync(this.state.address);
|
||||
} catch (err) {
|
||||
hasBalanceAllowanceErr = true;
|
||||
}
|
||||
@ -155,11 +150,7 @@ export class NewTokenForm extends React.Component<NewTokenFormProps, NewTokenFor
|
||||
isTracked: true,
|
||||
isRegistered: false,
|
||||
};
|
||||
const newTokenState: TokenState = {
|
||||
balance,
|
||||
allowance,
|
||||
};
|
||||
this.props.onNewTokenSubmitted(newToken, newTokenState);
|
||||
this.props.onNewTokenSubmitted(newToken);
|
||||
}
|
||||
private _onTokenNameChanged(e: any, name: string) {
|
||||
let nameErrText = '';
|
||||
|
@ -17,6 +17,8 @@ interface AllowanceToggleProps {
|
||||
token: Token;
|
||||
tokenState: TokenState;
|
||||
userAddress: string;
|
||||
isDisabled: boolean;
|
||||
refetchTokenStateAsync: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface AllowanceToggleState {
|
||||
@ -45,7 +47,7 @@ export class AllowanceToggle extends React.Component<AllowanceToggleProps, Allow
|
||||
<div className="flex">
|
||||
<div>
|
||||
<Toggle
|
||||
disabled={this.state.isSpinnerVisible}
|
||||
disabled={this.state.isSpinnerVisible || this.props.isDisabled}
|
||||
toggled={this._isAllowanceSet()}
|
||||
onToggle={this._onToggleAllowanceAsync.bind(this)}
|
||||
/>
|
||||
@ -73,12 +75,13 @@ export class AllowanceToggle extends React.Component<AllowanceToggleProps, Allow
|
||||
}
|
||||
try {
|
||||
await this.props.blockchain.setProxyAllowanceAsync(this.props.token, newAllowanceAmountInBaseUnits);
|
||||
await this.props.refetchTokenStateAsync();
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
isSpinnerVisible: false,
|
||||
});
|
||||
const errMsg = `${err}`;
|
||||
if (_.includes(errMsg, 'User denied transaction')) {
|
||||
if (utils.didUserDenyWeb3Request(errMsg)) {
|
||||
return;
|
||||
}
|
||||
utils.consoleLog(`Unexpected error encountered: ${err}`);
|
||||
|
@ -18,6 +18,7 @@ interface BalanceBoundedInputProps {
|
||||
validate?: (amount: BigNumber) => InputErrMsg;
|
||||
onVisitBalancesPageClick?: () => void;
|
||||
shouldHideVisitBalancesLink?: boolean;
|
||||
isDisabled?: boolean;
|
||||
}
|
||||
|
||||
interface BalanceBoundedInputState {
|
||||
@ -29,6 +30,7 @@ export class BalanceBoundedInput extends React.Component<BalanceBoundedInputProp
|
||||
public static defaultProps: Partial<BalanceBoundedInputProps> = {
|
||||
shouldShowIncompleteErrs: false,
|
||||
shouldHideVisitBalancesLink: false,
|
||||
isDisabled: false,
|
||||
};
|
||||
constructor(props: BalanceBoundedInputProps) {
|
||||
super(props);
|
||||
@ -88,6 +90,7 @@ export class BalanceBoundedInput extends React.Component<BalanceBoundedInputProp
|
||||
hintText={<span style={{ textTransform: 'capitalize' }}>amount</span>}
|
||||
onChange={this._onValueChange.bind(this)}
|
||||
underlineStyle={{ width: 'calc(100% + 50px)' }}
|
||||
disabled={this.props.isDisabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -100,7 +103,7 @@ export class BalanceBoundedInput extends React.Component<BalanceBoundedInputProp
|
||||
},
|
||||
() => {
|
||||
const isValid = _.isUndefined(errMsg);
|
||||
if (utils.isNumeric(amountString)) {
|
||||
if (utils.isNumeric(amountString) && !_.includes(amountString, '-')) {
|
||||
this.props.onChange(isValid, new BigNumber(amountString));
|
||||
} else {
|
||||
this.props.onChange(isValid);
|
||||
|
@ -3,13 +3,16 @@ import { BigNumber } from '@0xproject/utils';
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { BalanceBoundedInput } from 'ts/components/inputs/balance_bounded_input';
|
||||
import { InputErrMsg, Token, TokenState, ValidatedBigNumberCallback, WebsitePaths } from 'ts/types';
|
||||
import { InputErrMsg, Token, ValidatedBigNumberCallback, WebsitePaths } from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
|
||||
interface TokenAmountInputProps {
|
||||
userAddress: string;
|
||||
networkId: number;
|
||||
blockchain: Blockchain;
|
||||
token: Token;
|
||||
tokenState: TokenState;
|
||||
label?: string;
|
||||
amount?: BigNumber;
|
||||
shouldShowIncompleteErrs: boolean;
|
||||
@ -17,11 +20,45 @@ interface TokenAmountInputProps {
|
||||
shouldCheckAllowance: boolean;
|
||||
onChange: ValidatedBigNumberCallback;
|
||||
onVisitBalancesPageClick?: () => void;
|
||||
lastForceTokenStateRefetch: number;
|
||||
}
|
||||
|
||||
interface TokenAmountInputState {}
|
||||
interface TokenAmountInputState {
|
||||
balance: BigNumber;
|
||||
allowance: BigNumber;
|
||||
isBalanceAndAllowanceLoaded: boolean;
|
||||
}
|
||||
|
||||
export class TokenAmountInput extends React.Component<TokenAmountInputProps, TokenAmountInputState> {
|
||||
private _isUnmounted: boolean;
|
||||
constructor(props: TokenAmountInputProps) {
|
||||
super(props);
|
||||
this._isUnmounted = false;
|
||||
const defaultAmount = new BigNumber(0);
|
||||
this.state = {
|
||||
balance: defaultAmount,
|
||||
allowance: defaultAmount,
|
||||
isBalanceAndAllowanceLoaded: false,
|
||||
};
|
||||
}
|
||||
public componentWillMount() {
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._fetchBalanceAndAllowanceAsync(this.props.token.address, this.props.userAddress);
|
||||
}
|
||||
public componentWillUnmount() {
|
||||
this._isUnmounted = true;
|
||||
}
|
||||
public componentWillReceiveProps(nextProps: TokenAmountInputProps) {
|
||||
if (
|
||||
nextProps.userAddress !== this.props.userAddress ||
|
||||
nextProps.networkId !== this.props.networkId ||
|
||||
nextProps.token.address !== this.props.token.address ||
|
||||
nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch
|
||||
) {
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._fetchBalanceAndAllowanceAsync(nextProps.token.address, nextProps.userAddress);
|
||||
}
|
||||
}
|
||||
public render() {
|
||||
const amount = this.props.amount
|
||||
? ZeroEx.toUnitAmount(this.props.amount, this.props.token.decimals)
|
||||
@ -32,12 +69,13 @@ export class TokenAmountInput extends React.Component<TokenAmountInputProps, Tok
|
||||
<BalanceBoundedInput
|
||||
label={this.props.label}
|
||||
amount={amount}
|
||||
balance={ZeroEx.toUnitAmount(this.props.tokenState.balance, this.props.token.decimals)}
|
||||
balance={ZeroEx.toUnitAmount(this.state.balance, this.props.token.decimals)}
|
||||
onChange={this._onChange.bind(this)}
|
||||
validate={this._validate.bind(this)}
|
||||
shouldCheckBalance={this.props.shouldCheckBalance}
|
||||
shouldShowIncompleteErrs={this.props.shouldShowIncompleteErrs}
|
||||
onVisitBalancesPageClick={this.props.onVisitBalancesPageClick}
|
||||
isDisabled={!this.state.isBalanceAndAllowanceLoaded}
|
||||
/>
|
||||
<div style={{ paddingTop: hasLabel ? 39 : 14 }}>{this.props.token.symbol}</div>
|
||||
</div>
|
||||
@ -51,7 +89,7 @@ export class TokenAmountInput extends React.Component<TokenAmountInputProps, Tok
|
||||
this.props.onChange(isValid, baseUnitAmount);
|
||||
}
|
||||
private _validate(amount: BigNumber): InputErrMsg {
|
||||
if (this.props.shouldCheckAllowance && amount.gt(this.props.tokenState.allowance)) {
|
||||
if (this.props.shouldCheckAllowance && amount.gt(this.state.allowance)) {
|
||||
return (
|
||||
<span>
|
||||
Insufficient allowance.{' '}
|
||||
@ -67,4 +105,20 @@ export class TokenAmountInput extends React.Component<TokenAmountInputProps, Tok
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
private async _fetchBalanceAndAllowanceAsync(tokenAddress: string, userAddress: string) {
|
||||
this.setState({
|
||||
isBalanceAndAllowanceLoaded: false,
|
||||
});
|
||||
const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
|
||||
userAddress,
|
||||
tokenAddress,
|
||||
);
|
||||
if (!this._isUnmounted) {
|
||||
this.setState({
|
||||
balance,
|
||||
allowance,
|
||||
isBalanceAndAllowanceLoaded: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { BigNumber } from '@0xproject/utils';
|
||||
import * as _ from 'lodash';
|
||||
import CircularProgress from 'material-ui/CircularProgress';
|
||||
import Paper from 'material-ui/Paper';
|
||||
import * as React from 'react';
|
||||
import * as DocumentTitle from 'react-document-title';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { BlockchainErrDialog } from 'ts/components/dialogs/blockchain_err_dialog';
|
||||
import { LedgerConfigDialog } from 'ts/components/dialogs/ledger_config_dialog';
|
||||
import { PortalDisclaimerDialog } from 'ts/components/dialogs/portal_disclaimer_dialog';
|
||||
import { WrappedEthSectionNoticeDialog } from 'ts/components/dialogs/wrapped_eth_section_notice_dialog';
|
||||
import { EthWrappers } from 'ts/components/eth_wrappers';
|
||||
@ -13,25 +15,15 @@ import { FillOrder } from 'ts/components/fill_order';
|
||||
import { Footer } from 'ts/components/footer';
|
||||
import { PortalMenu } from 'ts/components/portal_menu';
|
||||
import { TokenBalances } from 'ts/components/token_balances';
|
||||
import { TopBar } from 'ts/components/top_bar';
|
||||
import { TopBar } from 'ts/components/top_bar/top_bar';
|
||||
import { TradeHistory } from 'ts/components/trade_history/trade_history';
|
||||
import { FlashMessage } from 'ts/components/ui/flash_message';
|
||||
import { Loading } from 'ts/components/ui/loading';
|
||||
import { GenerateOrderForm } from 'ts/containers/generate_order_form';
|
||||
import { localStorage } from 'ts/local_storage/local_storage';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { orderSchema } from 'ts/schemas/order_schema';
|
||||
import { SchemaValidator } from 'ts/schemas/validator';
|
||||
import {
|
||||
BlockchainErrs,
|
||||
HashData,
|
||||
Order,
|
||||
ScreenWidths,
|
||||
Token,
|
||||
TokenByAddress,
|
||||
TokenStateByAddress,
|
||||
WebsitePaths,
|
||||
} from 'ts/types';
|
||||
import { BlockchainErrs, HashData, Order, ProviderType, ScreenWidths, TokenByAddress, WebsitePaths } from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
import { configs } from 'ts/utils/configs';
|
||||
import { constants } from 'ts/utils/constants';
|
||||
@ -46,18 +38,20 @@ export interface PortalAllProps {
|
||||
blockchainIsLoaded: boolean;
|
||||
dispatcher: Dispatcher;
|
||||
hashData: HashData;
|
||||
injectedProviderName: string;
|
||||
networkId: number;
|
||||
nodeVersion: string;
|
||||
orderFillAmount: BigNumber;
|
||||
providerType: ProviderType;
|
||||
screenWidth: ScreenWidths;
|
||||
tokenByAddress: TokenByAddress;
|
||||
tokenStateByAddress: TokenStateByAddress;
|
||||
userEtherBalance: BigNumber;
|
||||
userAddress: string;
|
||||
shouldBlockchainErrDialogBeOpen: boolean;
|
||||
userSuppliedOrderCache: Order;
|
||||
location: Location;
|
||||
flashMessage?: string | React.ReactNode;
|
||||
lastForceTokenStateRefetch: number;
|
||||
}
|
||||
|
||||
interface PortalAllState {
|
||||
@ -67,6 +61,7 @@ interface PortalAllState {
|
||||
prevPathname: string;
|
||||
isDisclaimerDialogOpen: boolean;
|
||||
isWethNoticeDialogOpen: boolean;
|
||||
isLedgerDialogOpen: boolean;
|
||||
}
|
||||
|
||||
export class Portal extends React.Component<PortalAllProps, PortalAllState> {
|
||||
@ -96,6 +91,7 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
|
||||
prevPathname: this.props.location.pathname,
|
||||
isDisclaimerDialogOpen: !hasAcceptedDisclaimer,
|
||||
isWethNoticeDialogOpen: !hasAlreadyDismissedWethNotice && isViewingBalances,
|
||||
isLedgerDialogOpen: false,
|
||||
};
|
||||
}
|
||||
public componentDidMount() {
|
||||
@ -125,11 +121,6 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
|
||||
if (nextProps.userAddress !== this.state.prevUserAddress) {
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._blockchain.userAddressUpdatedFireAndForgetAsync(nextProps.userAddress);
|
||||
if (!_.isEmpty(nextProps.userAddress) && nextProps.blockchainIsLoaded) {
|
||||
const tokens = _.values(nextProps.tokenByAddress);
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._updateBalanceAndAllowanceWithLoadingScreenAsync(tokens);
|
||||
}
|
||||
this.setState({
|
||||
prevUserAddress: nextProps.userAddress,
|
||||
});
|
||||
@ -167,8 +158,14 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
|
||||
<DocumentTitle title="0x Portal DApp" />
|
||||
<TopBar
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
injectedProviderName={this.props.injectedProviderName}
|
||||
onToggleLedgerDialog={this.onToggleLedgerDialog.bind(this)}
|
||||
dispatcher={this.props.dispatcher}
|
||||
providerType={this.props.providerType}
|
||||
blockchainIsLoaded={this.props.blockchainIsLoaded}
|
||||
location={this.props.location}
|
||||
blockchain={this._blockchain}
|
||||
/>
|
||||
<div id="portal" className="mx-auto max-width-4" style={{ width: '100%' }}>
|
||||
<Paper className="mb3 mt2">
|
||||
@ -215,7 +212,19 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
|
||||
/>
|
||||
</Switch>
|
||||
) : (
|
||||
<Loading />
|
||||
<div className="pt4 sm-px2 sm-pt2 sm-m1" style={{ height: 500 }}>
|
||||
<div
|
||||
className="relative sm-px2 sm-pt2 sm-m1"
|
||||
style={{ height: 122, top: '50%', transform: 'translateY(-50%)' }}
|
||||
>
|
||||
<div className="center pb2">
|
||||
<CircularProgress size={40} thickness={5} />
|
||||
</div>
|
||||
<div className="center pt2" style={{ paddingBottom: 11 }}>
|
||||
Loading Portal...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -239,11 +248,26 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
|
||||
onToggleDialog={this._onPortalDisclaimerAccepted.bind(this)}
|
||||
/>
|
||||
<FlashMessage dispatcher={this.props.dispatcher} flashMessage={this.props.flashMessage} />
|
||||
{this.props.blockchainIsLoaded && (
|
||||
<LedgerConfigDialog
|
||||
providerType={this.props.providerType}
|
||||
networkId={this.props.networkId}
|
||||
blockchain={this._blockchain}
|
||||
dispatcher={this.props.dispatcher}
|
||||
toggleDialogFn={this.onToggleLedgerDialog.bind(this)}
|
||||
isOpen={this.state.isLedgerDialogOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Footer />
|
||||
<Footer />;
|
||||
</div>
|
||||
);
|
||||
}
|
||||
public onToggleLedgerDialog() {
|
||||
this.setState({
|
||||
isLedgerDialogOpen: !this.state.isLedgerDialogOpen,
|
||||
});
|
||||
}
|
||||
private _renderEthWrapper() {
|
||||
return (
|
||||
<EthWrappers
|
||||
@ -251,9 +275,9 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
|
||||
blockchain={this._blockchain}
|
||||
dispatcher={this.props.dispatcher}
|
||||
tokenByAddress={this.props.tokenByAddress}
|
||||
tokenStateByAddress={this.props.tokenStateByAddress}
|
||||
userAddress={this.props.userAddress}
|
||||
userEtherBalance={this.props.userEtherBalance}
|
||||
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -267,6 +291,8 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
|
||||
);
|
||||
}
|
||||
private _renderTokenBalances() {
|
||||
const allTokens = _.values(this.props.tokenByAddress);
|
||||
const trackedTokens = _.filter(allTokens, t => t.isTracked);
|
||||
return (
|
||||
<TokenBalances
|
||||
blockchain={this._blockchain}
|
||||
@ -275,10 +301,11 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
|
||||
dispatcher={this.props.dispatcher}
|
||||
screenWidth={this.props.screenWidth}
|
||||
tokenByAddress={this.props.tokenByAddress}
|
||||
tokenStateByAddress={this.props.tokenStateByAddress}
|
||||
trackedTokens={trackedTokens}
|
||||
userAddress={this.props.userAddress}
|
||||
userEtherBalance={this.props.userEtherBalance}
|
||||
networkId={this.props.networkId}
|
||||
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -296,8 +323,8 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
|
||||
networkId={this.props.networkId}
|
||||
userAddress={this.props.userAddress}
|
||||
tokenByAddress={this.props.tokenByAddress}
|
||||
tokenStateByAddress={this.props.tokenStateByAddress}
|
||||
dispatcher={this.props.dispatcher}
|
||||
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -353,9 +380,4 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
|
||||
const newScreenWidth = utils.getScreenWidth();
|
||||
this.props.dispatcher.updateScreenWidth(newScreenWidth);
|
||||
}
|
||||
private async _updateBalanceAndAllowanceWithLoadingScreenAsync(tokens: Token[]) {
|
||||
this.props.dispatcher.updateBlockchainIsLoaded(false);
|
||||
await this._blockchain.updateTokenBalancesAndAllowancesAsync(tokens);
|
||||
this.props.dispatcher.updateBlockchainIsLoaded(true);
|
||||
}
|
||||
}
|
||||
|
@ -5,16 +5,19 @@ import * as React from 'react';
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { SendDialog } from 'ts/components/dialogs/send_dialog';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { BlockchainCallErrs, Token, TokenState } from 'ts/types';
|
||||
import { BlockchainCallErrs, Token } from 'ts/types';
|
||||
import { errorReporter } from 'ts/utils/error_reporter';
|
||||
import { utils } from 'ts/utils/utils';
|
||||
|
||||
interface SendButtonProps {
|
||||
userAddress: string;
|
||||
networkId: number;
|
||||
token: Token;
|
||||
tokenState: TokenState;
|
||||
dispatcher: Dispatcher;
|
||||
blockchain: Blockchain;
|
||||
onError: () => void;
|
||||
lastForceTokenStateRefetch: number;
|
||||
refetchTokenStateAsync: (tokenAddress: string) => Promise<void>;
|
||||
}
|
||||
|
||||
interface SendButtonState {
|
||||
@ -42,11 +45,14 @@ export class SendButton extends React.Component<SendButtonProps, SendButtonState
|
||||
onClick={this._toggleSendDialog.bind(this)}
|
||||
/>
|
||||
<SendDialog
|
||||
blockchain={this.props.blockchain}
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
isOpen={this.state.isSendDialogVisible}
|
||||
onComplete={this._onSendAmountSelectedAsync.bind(this)}
|
||||
onCancelled={this._toggleSendDialog.bind(this)}
|
||||
token={this.props.token}
|
||||
tokenState={this.props.tokenState}
|
||||
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -62,18 +68,15 @@ export class SendButton extends React.Component<SendButtonProps, SendButtonState
|
||||
});
|
||||
this._toggleSendDialog();
|
||||
const token = this.props.token;
|
||||
const tokenState = this.props.tokenState;
|
||||
let balance = tokenState.balance;
|
||||
try {
|
||||
await this.props.blockchain.transferAsync(token, recipient, value);
|
||||
balance = balance.minus(value);
|
||||
this.props.dispatcher.replaceTokenBalanceByAddress(token.address, balance);
|
||||
await this.props.refetchTokenStateAsync(token.address);
|
||||
} catch (err) {
|
||||
const errMsg = `${err}`;
|
||||
if (_.includes(errMsg, BlockchainCallErrs.UserHasNoAssociatedAddresses)) {
|
||||
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
|
||||
return;
|
||||
} else if (!_.includes(errMsg, 'User denied transaction')) {
|
||||
} else if (!utils.didUserDenyWeb3Request(errMsg)) {
|
||||
utils.consoleLog(`Unexpected error encountered: ${err}`);
|
||||
utils.consoleLog(err.stack);
|
||||
this.props.onError();
|
||||
|
@ -27,11 +27,11 @@ import {
|
||||
BlockchainCallErrs,
|
||||
BlockchainErrs,
|
||||
EtherscanLinkSuffixes,
|
||||
Networks,
|
||||
ScreenWidths,
|
||||
Styles,
|
||||
Token,
|
||||
TokenByAddress,
|
||||
TokenStateByAddress,
|
||||
TokenVisibility,
|
||||
} from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
@ -58,6 +58,14 @@ const styles: Styles = {
|
||||
},
|
||||
};
|
||||
|
||||
interface TokenStateByAddress {
|
||||
[address: string]: {
|
||||
balance: BigNumber;
|
||||
allowance: BigNumber;
|
||||
isLoaded: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface TokenBalancesProps {
|
||||
blockchain: Blockchain;
|
||||
blockchainErr: BlockchainErrs;
|
||||
@ -65,10 +73,11 @@ interface TokenBalancesProps {
|
||||
dispatcher: Dispatcher;
|
||||
screenWidth: ScreenWidths;
|
||||
tokenByAddress: TokenByAddress;
|
||||
tokenStateByAddress: TokenStateByAddress;
|
||||
trackedTokens: Token[];
|
||||
userAddress: string;
|
||||
userEtherBalance: BigNumber;
|
||||
networkId: number;
|
||||
lastForceTokenStateRefetch: number;
|
||||
}
|
||||
|
||||
interface TokenBalancesState {
|
||||
@ -76,14 +85,17 @@ interface TokenBalancesState {
|
||||
isBalanceSpinnerVisible: boolean;
|
||||
isDharmaDialogVisible: boolean;
|
||||
isZRXSpinnerVisible: boolean;
|
||||
currentZrxBalance?: BigNumber;
|
||||
isTokenPickerOpen: boolean;
|
||||
isAddingToken: boolean;
|
||||
trackedTokenStateByAddress: TokenStateByAddress;
|
||||
}
|
||||
|
||||
export class TokenBalances extends React.Component<TokenBalancesProps, TokenBalancesState> {
|
||||
private _isUnmounted: boolean;
|
||||
public constructor(props: TokenBalancesProps) {
|
||||
super(props);
|
||||
this._isUnmounted = false;
|
||||
const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(props.trackedTokens);
|
||||
this.state = {
|
||||
errorType: undefined,
|
||||
isBalanceSpinnerVisible: false,
|
||||
@ -91,8 +103,17 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
isDharmaDialogVisible: DharmaLoanFrame.isAuthTokenPresent(),
|
||||
isTokenPickerOpen: false,
|
||||
isAddingToken: false,
|
||||
trackedTokenStateByAddress: initialTrackedTokenStateByAddress,
|
||||
};
|
||||
}
|
||||
public componentWillMount() {
|
||||
const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
|
||||
}
|
||||
public componentWillUnmount() {
|
||||
this._isUnmounted = true;
|
||||
}
|
||||
public componentWillReceiveProps(nextProps: TokenBalancesProps) {
|
||||
if (nextProps.userEtherBalance !== this.props.userEtherBalance) {
|
||||
if (this.state.isBalanceSpinnerVisible) {
|
||||
@ -103,18 +124,36 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
isBalanceSpinnerVisible: false,
|
||||
});
|
||||
}
|
||||
const nextZrxToken = _.find(_.values(nextProps.tokenByAddress), t => t.symbol === ZRX_TOKEN_SYMBOL);
|
||||
const nextZrxTokenBalance = nextProps.tokenStateByAddress[nextZrxToken.address].balance;
|
||||
if (!_.isUndefined(this.state.currentZrxBalance) && !nextZrxTokenBalance.eq(this.state.currentZrxBalance)) {
|
||||
if (this.state.isZRXSpinnerVisible) {
|
||||
const receivedAmount = nextZrxTokenBalance.minus(this.state.currentZrxBalance);
|
||||
const receiveAmountInUnits = ZeroEx.toUnitAmount(receivedAmount, constants.DECIMAL_PLACES_ZRX);
|
||||
this.props.dispatcher.showFlashMessage(`Received ${receiveAmountInUnits.toString(10)} Kovan ZRX`);
|
||||
|
||||
if (
|
||||
nextProps.userAddress !== this.props.userAddress ||
|
||||
nextProps.networkId !== this.props.networkId ||
|
||||
nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch
|
||||
) {
|
||||
const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress);
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses);
|
||||
}
|
||||
|
||||
if (!_.isEqual(nextProps.trackedTokens, this.props.trackedTokens)) {
|
||||
const newTokens = _.difference(nextProps.trackedTokens, this.props.trackedTokens);
|
||||
const newTokenAddresses = _.map(newTokens, token => token.address);
|
||||
// Add placeholder entry for this token to the state, since fetching the
|
||||
// balance/allowance is asynchronous
|
||||
const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
|
||||
for (const tokenAddress of newTokenAddresses) {
|
||||
trackedTokenStateByAddress[tokenAddress] = {
|
||||
balance: new BigNumber(0),
|
||||
allowance: new BigNumber(0),
|
||||
isLoaded: false,
|
||||
};
|
||||
}
|
||||
this.setState({
|
||||
isZRXSpinnerVisible: false,
|
||||
currentZrxBalance: undefined,
|
||||
trackedTokenStateByAddress,
|
||||
});
|
||||
// Fetch the actual balance/allowance.
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._fetchBalancesAndAllowancesAsync(newTokenAddresses);
|
||||
}
|
||||
}
|
||||
public componentDidMount() {
|
||||
@ -137,13 +176,13 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
onTouchTap={this._onDharmaDialogToggle.bind(this, false)}
|
||||
/>,
|
||||
];
|
||||
const isTestNetwork = this.props.networkId === constants.NETWORK_ID_TESTNET;
|
||||
const isKovanTestNetwork = this.props.networkId === constants.NETWORK_ID_KOVAN;
|
||||
const dharmaButtonColumnStyle = {
|
||||
paddingLeft: 3,
|
||||
display: isTestNetwork ? 'table-cell' : 'none',
|
||||
display: isKovanTestNetwork ? 'table-cell' : 'none',
|
||||
};
|
||||
const stubColumnStyle = {
|
||||
display: isTestNetwork ? 'none' : 'table-cell',
|
||||
display: isKovanTestNetwork ? 'none' : 'table-cell',
|
||||
};
|
||||
const allTokenRowHeight = _.size(this.props.tokenByAddress) * TOKEN_TABLE_ROW_HEIGHT;
|
||||
const tokenTableHeight =
|
||||
@ -162,10 +201,10 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
smart contract so you can start trading that token.';
|
||||
return (
|
||||
<div className="lg-px4 md-px4 sm-px1 pb2">
|
||||
<h3>{isTestNetwork ? 'Test ether' : 'Ether'}</h3>
|
||||
<h3>{isKovanTestNetwork ? 'Test ether' : 'Ether'}</h3>
|
||||
<Divider />
|
||||
<div className="pt2 pb2">
|
||||
{isTestNetwork
|
||||
{isKovanTestNetwork
|
||||
? 'In order to try out the 0x Portal Dapp, request some test ether to pay for \
|
||||
gas costs. It might take a bit of time for the test ether to show up.'
|
||||
: 'Ether must be converted to Ether Tokens in order to be tradable via 0x. \
|
||||
@ -177,12 +216,12 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
<TableHeaderColumn>Currency</TableHeaderColumn>
|
||||
<TableHeaderColumn>Balance</TableHeaderColumn>
|
||||
<TableRowColumn className="sm-hide xs-hide" style={stubColumnStyle} />
|
||||
{isTestNetwork && (
|
||||
{isKovanTestNetwork && (
|
||||
<TableHeaderColumn style={{ paddingLeft: 3 }}>
|
||||
{isSmallScreen ? 'Faucet' : 'Request from faucet'}
|
||||
</TableHeaderColumn>
|
||||
)}
|
||||
{isTestNetwork && (
|
||||
{isKovanTestNetwork && (
|
||||
<TableHeaderColumn style={dharmaButtonColumnStyle}>
|
||||
{isSmallScreen ? 'Loan' : 'Request Dharma loan'}
|
||||
<HelpTooltip style={{ paddingLeft: 4 }} explanation={dharmaLoanExplanation} />
|
||||
@ -204,7 +243,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
)}
|
||||
</TableRowColumn>
|
||||
<TableRowColumn className="sm-hide xs-hide" style={stubColumnStyle} />
|
||||
{isTestNetwork && (
|
||||
{isKovanTestNetwork && (
|
||||
<TableRowColumn style={{ paddingLeft: 3 }}>
|
||||
<LifeCycleRaisedButton
|
||||
labelReady="Request"
|
||||
@ -214,7 +253,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
/>
|
||||
</TableRowColumn>
|
||||
)}
|
||||
{isTestNetwork && (
|
||||
{isKovanTestNetwork && (
|
||||
<TableRowColumn style={dharmaButtonColumnStyle}>
|
||||
<RaisedButton
|
||||
label="Request"
|
||||
@ -228,7 +267,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
</Table>
|
||||
<div className="clearfix" style={{ paddingBottom: 1 }}>
|
||||
<div className="col col-10">
|
||||
<h3 className="pt2">{isTestNetwork ? 'Test tokens' : 'Tokens'}</h3>
|
||||
<h3 className="pt2">{isKovanTestNetwork ? 'Test tokens' : 'Tokens'}</h3>
|
||||
</div>
|
||||
<div className="col col-1 pt3 align-right">
|
||||
<FloatingActionButton mini={true} zDepth={0} onClick={this._onAddTokenClicked.bind(this)}>
|
||||
@ -243,7 +282,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
</div>
|
||||
<Divider />
|
||||
<div className="pt2 pb2">
|
||||
{isTestNetwork
|
||||
{isKovanTestNetwork
|
||||
? "Mint some test tokens you'd like to use to generate or fill an order using 0x."
|
||||
: "Set trading permissions for a token you'd like to start trading."}
|
||||
</div>
|
||||
@ -303,8 +342,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm;
|
||||
const tokenColSpan = isSmallScreen ? TOKEN_COL_SPAN_SM : TOKEN_COL_SPAN_LG;
|
||||
const actionPaddingX = isSmallScreen ? 2 : 24;
|
||||
const allTokens = _.values(this.props.tokenByAddress);
|
||||
const trackedTokens = _.filter(allTokens, t => t.isTracked);
|
||||
const trackedTokens = this.props.trackedTokens;
|
||||
const trackedTokensStartingWithEtherToken = trackedTokens.sort(
|
||||
firstBy((t: Token) => t.symbol !== ETHER_TOKEN_SYMBOL)
|
||||
.thenBy((t: Token) => t.symbol !== ZRX_TOKEN_SYMBOL)
|
||||
@ -317,7 +355,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
return tableRows;
|
||||
}
|
||||
private _renderTokenRow(tokenColSpan: number, actionPaddingX: number, token: Token) {
|
||||
const tokenState = this.props.tokenStateByAddress[token.address];
|
||||
const tokenState = this.state.trackedTokenStateByAddress[token.address];
|
||||
const tokenLink = utils.getEtherScanLinkIfExists(
|
||||
token.address,
|
||||
this.props.networkId,
|
||||
@ -338,6 +376,8 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
)}
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={{ paddingRight: 3, paddingLeft: 3 }}>
|
||||
{tokenState.isLoaded ? (
|
||||
<span>
|
||||
{this._renderAmount(tokenState.balance, token.decimals)} {token.symbol}
|
||||
{this.state.isZRXSpinnerVisible &&
|
||||
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" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
) : (
|
||||
<i className="zmdi zmdi-spinner zmdi-hc-spin" />
|
||||
)}
|
||||
</TableRowColumn>
|
||||
<TableRowColumn>
|
||||
<AllowanceToggle
|
||||
@ -354,6 +398,8 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
tokenState={tokenState}
|
||||
onErrorOccurred={this._onErrorOccurred.bind(this)}
|
||||
userAddress={this.props.userAddress}
|
||||
isDisabled={!tokenState.isLoaded}
|
||||
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)}
|
||||
/>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={{ paddingLeft: actionPaddingX, paddingRight: actionPaddingX }}>
|
||||
@ -366,7 +412,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
/>
|
||||
)}
|
||||
{token.symbol === ZRX_TOKEN_SYMBOL &&
|
||||
this.props.networkId === constants.NETWORK_ID_TESTNET && (
|
||||
this.props.networkId === constants.NETWORK_ID_KOVAN && (
|
||||
<LifeCycleRaisedButton
|
||||
labelReady="Request"
|
||||
labelLoading="Sending..."
|
||||
@ -383,11 +429,14 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
}}
|
||||
>
|
||||
<SendButton
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
blockchain={this.props.blockchain}
|
||||
dispatcher={this.props.dispatcher}
|
||||
token={token}
|
||||
tokenState={tokenState}
|
||||
onError={this._onSendFailed.bind(this)}
|
||||
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
||||
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)}
|
||||
/>
|
||||
</TableRowColumn>
|
||||
)}
|
||||
@ -414,7 +463,6 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
} else {
|
||||
this.props.dispatcher.removeTokenToTokenByAddress(token);
|
||||
}
|
||||
this.props.dispatcher.removeFromTokenStateByAddress(tokenAddress);
|
||||
trackedTokenStorage.removeTrackedToken(this.props.userAddress, this.props.networkId, tokenAddress);
|
||||
} else if (isDefaultTrackedToken) {
|
||||
this.props.dispatcher.showFlashMessage(`Cannot remove ${token.name} because it's a default token`);
|
||||
@ -449,9 +497,9 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
case BalanceErrs.incorrectNetworkForFaucet:
|
||||
return (
|
||||
<div>
|
||||
Our faucet can only send test Ether to addresses on the {constants.TESTNET_NAME} testnet
|
||||
(networkId {constants.NETWORK_ID_TESTNET}). Please make sure you are connected to the{' '}
|
||||
{constants.TESTNET_NAME} testnet and try requesting ether again.
|
||||
Our faucet can only send test Ether to addresses on the {Networks.Kovan} testnet (networkId{' '}
|
||||
{constants.NETWORK_ID_KOVAN}). Please make sure you are connected to the {Networks.Kovan}{' '}
|
||||
testnet and try requesting ether again.
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -510,6 +558,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
private async _onMintTestTokensAsync(token: Token): Promise<boolean> {
|
||||
try {
|
||||
await this.props.blockchain.mintTestTokensAsync(token);
|
||||
await this._refetchTokenStateAsync(token.address);
|
||||
const amount = ZeroEx.toUnitAmount(constants.MINT_AMOUNT, token.decimals);
|
||||
this.props.dispatcher.showFlashMessage(`Successfully minted ${amount.toString(10)} ${token.symbol}`);
|
||||
return true;
|
||||
@ -519,7 +568,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
|
||||
return false;
|
||||
}
|
||||
if (_.includes(errMsg, 'User denied transaction')) {
|
||||
if (utils.didUserDenyWeb3Request(errMsg)) {
|
||||
return false;
|
||||
}
|
||||
utils.consoleLog(`Unexpected error encountered: ${err}`);
|
||||
@ -539,7 +588,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
|
||||
// If on another network other then the testnet our faucet serves test ether
|
||||
// from, we must show user an error message
|
||||
if (this.props.blockchain.networkId !== constants.NETWORK_ID_TESTNET) {
|
||||
if (this.props.blockchain.networkId !== constants.NETWORK_ID_KOVAN) {
|
||||
this.setState({
|
||||
errorType: BalanceErrs.incorrectNetworkForFaucet,
|
||||
});
|
||||
@ -569,15 +618,11 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
isBalanceSpinnerVisible: true,
|
||||
});
|
||||
} else {
|
||||
const tokens = _.values(this.props.tokenByAddress);
|
||||
const zrxToken = _.find(tokens, t => t.symbol === ZRX_TOKEN_SYMBOL);
|
||||
const zrxTokenState = this.props.tokenStateByAddress[zrxToken.address];
|
||||
this.setState({
|
||||
isZRXSpinnerVisible: true,
|
||||
currentZrxBalance: zrxTokenState.balance,
|
||||
});
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this.props.blockchain.pollTokenBalanceAsync(zrxToken);
|
||||
this._startPollingZrxBalanceAsync();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -603,4 +648,65 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
isAddingToken: false,
|
||||
});
|
||||
}
|
||||
private async _startPollingZrxBalanceAsync() {
|
||||
const tokens = _.values(this.props.tokenByAddress);
|
||||
const zrxToken = _.find(tokens, t => t.symbol === ZRX_TOKEN_SYMBOL);
|
||||
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
const balance = await this.props.blockchain.pollTokenBalanceAsync(zrxToken);
|
||||
const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
|
||||
trackedTokenStateByAddress[zrxToken.address] = {
|
||||
...trackedTokenStateByAddress[zrxToken.address],
|
||||
balance,
|
||||
};
|
||||
this.setState({
|
||||
isZRXSpinnerVisible: false,
|
||||
});
|
||||
}
|
||||
private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]) {
|
||||
const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
|
||||
for (const tokenAddress of tokenAddresses) {
|
||||
const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
|
||||
this.props.userAddress,
|
||||
tokenAddress,
|
||||
);
|
||||
trackedTokenStateByAddress[tokenAddress] = {
|
||||
balance,
|
||||
allowance,
|
||||
isLoaded: true,
|
||||
};
|
||||
}
|
||||
if (!this._isUnmounted) {
|
||||
this.setState({
|
||||
trackedTokenStateByAddress,
|
||||
});
|
||||
}
|
||||
}
|
||||
private _getInitialTrackedTokenStateByAddress(trackedTokens: Token[]) {
|
||||
const trackedTokenStateByAddress: TokenStateByAddress = {};
|
||||
_.each(trackedTokens, token => {
|
||||
trackedTokenStateByAddress[token.address] = {
|
||||
balance: new BigNumber(0),
|
||||
allowance: new BigNumber(0),
|
||||
isLoaded: false,
|
||||
};
|
||||
});
|
||||
return trackedTokenStateByAddress;
|
||||
}
|
||||
private async _refetchTokenStateAsync(tokenAddress: string) {
|
||||
const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
|
||||
this.props.userAddress,
|
||||
tokenAddress,
|
||||
);
|
||||
this.setState({
|
||||
trackedTokenStateByAddress: {
|
||||
...this.state.trackedTokenStateByAddress,
|
||||
[tokenAddress]: {
|
||||
balance,
|
||||
allowance,
|
||||
isLoaded: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} // tslint:disable:max-file-line-count
|
||||
|
148
packages/website/ts/components/top_bar/provider_display.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
81
packages/website/ts/components/top_bar/provider_picker.tsx
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +1,31 @@
|
||||
import * as _ from 'lodash';
|
||||
import Drawer from 'material-ui/Drawer';
|
||||
import Menu from 'material-ui/Menu';
|
||||
import MenuItem from 'material-ui/MenuItem';
|
||||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ReactTooltip = require('react-tooltip');
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { PortalMenu } from 'ts/components/portal_menu';
|
||||
import { TopBarMenuItem } from 'ts/components/top_bar_menu_item';
|
||||
import { DropDownMenuItem } from 'ts/components/ui/drop_down_menu_item';
|
||||
import { ProviderDisplay } from 'ts/components/top_bar/provider_display';
|
||||
import { TopBarMenuItem } from 'ts/components/top_bar/top_bar_menu_item';
|
||||
import { DropDown } from 'ts/components/ui/drop_down';
|
||||
import { Identicon } from 'ts/components/ui/identicon';
|
||||
import { DocsInfo } from 'ts/pages/documentation/docs_info';
|
||||
import { NestedSidebarMenu } from 'ts/pages/shared/nested_sidebar_menu';
|
||||
import { DocsMenu, MenuSubsectionsBySection, Styles, WebsitePaths } from 'ts/types';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { DocsMenu, MenuSubsectionsBySection, ProviderType, Styles, WebsitePaths } from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
import { constants } from 'ts/utils/constants';
|
||||
|
||||
interface TopBarProps {
|
||||
userAddress?: string;
|
||||
networkId?: number;
|
||||
injectedProviderName?: string;
|
||||
providerType?: ProviderType;
|
||||
onToggleLedgerDialog?: () => void;
|
||||
blockchain?: Blockchain;
|
||||
dispatcher?: Dispatcher;
|
||||
blockchainIsLoaded: boolean;
|
||||
location: Location;
|
||||
docsVersion?: string;
|
||||
@ -125,6 +135,15 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
|
||||
cursor: 'pointer',
|
||||
paddingTop: 16,
|
||||
};
|
||||
const hoverActiveNode = (
|
||||
<div className="flex relative" style={{ color: menuIconStyle.color }}>
|
||||
<div style={{ paddingRight: 10 }}>Developers</div>
|
||||
<div className="absolute" style={{ paddingLeft: 3, right: 3, top: -2 }}>
|
||||
<i className="zmdi zmdi-caret-right" style={{ fontSize: 22 }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const popoverContent = <Menu style={{ color: colors.darkGrey }}>{developerSectionMenuItems}</Menu>;
|
||||
return (
|
||||
<div style={{ ...styles.topBar, ...bottomBorderStyle, ...this.props.style }} className="pb1">
|
||||
<div className={parentClassNames}>
|
||||
@ -138,11 +157,12 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
|
||||
{!this._isViewingPortal() && (
|
||||
<div className={menuClasses}>
|
||||
<div className="flex justify-between">
|
||||
<DropDownMenuItem
|
||||
title="Developers"
|
||||
subMenuItems={developerSectionMenuItems}
|
||||
<DropDown
|
||||
hoverActiveNode={hoverActiveNode}
|
||||
popoverContent={popoverContent}
|
||||
anchorOrigin={{ horizontal: 'middle', vertical: 'bottom' }}
|
||||
targetOrigin={{ horizontal: 'middle', vertical: 'top' }}
|
||||
style={styles.menuItem}
|
||||
isNightVersion={isNightVersion}
|
||||
/>
|
||||
<TopBarMenuItem
|
||||
title="Wiki"
|
||||
@ -167,9 +187,18 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{this.props.blockchainIsLoaded &&
|
||||
!_.isEmpty(this.props.userAddress) && (
|
||||
<div className="col col-5 sm-hide xs-hide">{this._renderUser()}</div>
|
||||
{this.props.blockchainIsLoaded && (
|
||||
<div className="sm-hide xs-hide col col-5">
|
||||
<ProviderDisplay
|
||||
dispatcher={this.props.dispatcher}
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
injectedProviderName={this.props.injectedProviderName}
|
||||
providerType={this.props.providerType}
|
||||
onToggleLedgerDialog={this.props.onToggleLedgerDialog}
|
||||
blockchain={this.props.blockchain}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={`col ${isFullWidthPage ? 'col-2 pl2' : 'col-1'} md-hide lg-hide`}>
|
||||
<div style={menuIconStyle}>
|
@ -1,36 +1,35 @@
|
||||
import * as _ from 'lodash';
|
||||
import Menu from 'material-ui/Menu';
|
||||
import Popover from 'material-ui/Popover';
|
||||
import Popover, { PopoverAnimationVertical } from 'material-ui/Popover';
|
||||
import * as React from 'react';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
import { MaterialUIPosition } from 'ts/types';
|
||||
|
||||
const CHECK_CLOSE_POPOVER_INTERVAL_MS = 300;
|
||||
const DEFAULT_STYLE = {
|
||||
fontSize: 14,
|
||||
};
|
||||
|
||||
interface DropDownMenuItemProps {
|
||||
title: string;
|
||||
subMenuItems: React.ReactNode[];
|
||||
interface DropDownProps {
|
||||
hoverActiveNode: React.ReactNode;
|
||||
popoverContent: React.ReactNode;
|
||||
anchorOrigin: MaterialUIPosition;
|
||||
targetOrigin: MaterialUIPosition;
|
||||
style?: React.CSSProperties;
|
||||
menuItemStyle?: React.CSSProperties;
|
||||
isNightVersion?: boolean;
|
||||
zDepth?: number;
|
||||
}
|
||||
|
||||
interface DropDownMenuItemState {
|
||||
interface DropDownState {
|
||||
isDropDownOpen: boolean;
|
||||
anchorEl?: HTMLInputElement;
|
||||
}
|
||||
|
||||
export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, DropDownMenuItemState> {
|
||||
public static defaultProps: Partial<DropDownMenuItemProps> = {
|
||||
export class DropDown extends React.Component<DropDownProps, DropDownState> {
|
||||
public static defaultProps: Partial<DropDownProps> = {
|
||||
style: DEFAULT_STYLE,
|
||||
menuItemStyle: DEFAULT_STYLE,
|
||||
isNightVersion: false,
|
||||
zDepth: 1,
|
||||
};
|
||||
private _isHovering: boolean;
|
||||
private _popoverCloseCheckIntervalId: number;
|
||||
constructor(props: DropDownMenuItemProps) {
|
||||
constructor(props: DropDownProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isDropDownOpen: false,
|
||||
@ -44,30 +43,35 @@ export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, Dro
|
||||
public componentWillUnmount() {
|
||||
window.clearInterval(this._popoverCloseCheckIntervalId);
|
||||
}
|
||||
public componentWillReceiveProps(nextProps: DropDownProps) {
|
||||
// HACK: If the popoverContent is updated to a different dimension and the users
|
||||
// mouse is no longer above it, the dropdown can enter an inconsistent state where
|
||||
// it believes the user is still hovering over it. In order to remedy this, we
|
||||
// call hoverOff whenever the dropdown receives updated props. This is a hack
|
||||
// because it will effectively close the dropdown on any prop update, barring
|
||||
// dropdowns from having dynamic content.
|
||||
this._onHoverOff();
|
||||
}
|
||||
public render() {
|
||||
const colorStyle = this.props.isNightVersion ? 'white' : this.props.style.color;
|
||||
return (
|
||||
<div
|
||||
style={{ ...this.props.style, color: colorStyle }}
|
||||
style={{ ...this.props.style, width: 'fit-content', height: '100%' }}
|
||||
onMouseEnter={this._onHover.bind(this)}
|
||||
onMouseLeave={this._onHoverOff.bind(this)}
|
||||
>
|
||||
<div className="flex relative">
|
||||
<div style={{ paddingRight: 10 }}>{this.props.title}</div>
|
||||
<div className="absolute" style={{ paddingLeft: 3, right: 3, top: -2 }}>
|
||||
<i className="zmdi zmdi-caret-right" style={{ fontSize: 22 }} />
|
||||
</div>
|
||||
</div>
|
||||
{this.props.hoverActiveNode}
|
||||
<Popover
|
||||
open={this.state.isDropDownOpen}
|
||||
anchorEl={this.state.anchorEl}
|
||||
anchorOrigin={{ horizontal: 'middle', vertical: 'bottom' }}
|
||||
targetOrigin={{ horizontal: 'middle', vertical: 'top' }}
|
||||
anchorOrigin={this.props.anchorOrigin}
|
||||
targetOrigin={this.props.targetOrigin}
|
||||
onRequestClose={this._closePopover.bind(this)}
|
||||
useLayerForClickAway={false}
|
||||
animation={PopoverAnimationVertical}
|
||||
zDepth={this.props.zDepth}
|
||||
>
|
||||
<div onMouseEnter={this._onHover.bind(this)} onMouseLeave={this._onHoverOff.bind(this)}>
|
||||
<Menu style={{ color: colors.grey }}>{this.props.subMenuItems}</Menu>
|
||||
{this.props.popoverContent}
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
@ -87,7 +91,7 @@ export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, Dro
|
||||
anchorEl: event.currentTarget,
|
||||
});
|
||||
}
|
||||
private _onHoverOff(event: React.FormEvent<HTMLInputElement>) {
|
||||
private _onHoverOff() {
|
||||
this._isHovering = false;
|
||||
}
|
||||
private _checkIfShouldClosePopover() {
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -6,14 +6,7 @@ import { Blockchain } from 'ts/blockchain';
|
||||
import { GenerateOrderForm as GenerateOrderFormComponent } from 'ts/components/generate_order/generate_order_form';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { State } from 'ts/redux/reducer';
|
||||
import {
|
||||
BlockchainErrs,
|
||||
HashData,
|
||||
SideToAssetToken,
|
||||
SignatureData,
|
||||
TokenByAddress,
|
||||
TokenStateByAddress,
|
||||
} from 'ts/types';
|
||||
import { BlockchainErrs, HashData, SideToAssetToken, SignatureData, TokenByAddress } from 'ts/types';
|
||||
|
||||
interface GenerateOrderFormProps {
|
||||
blockchain: Blockchain;
|
||||
@ -32,7 +25,7 @@ interface ConnectedState {
|
||||
networkId: number;
|
||||
sideToAssetToken: SideToAssetToken;
|
||||
tokenByAddress: TokenByAddress;
|
||||
tokenStateByAddress: TokenStateByAddress;
|
||||
lastForceTokenStateRefetch: number;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: State, ownProps: GenerateOrderFormProps): ConnectedState => ({
|
||||
@ -45,8 +38,8 @@ const mapStateToProps = (state: State, ownProps: GenerateOrderFormProps): Connec
|
||||
networkId: state.networkId,
|
||||
sideToAssetToken: state.sideToAssetToken,
|
||||
tokenByAddress: state.tokenByAddress,
|
||||
tokenStateByAddress: state.tokenStateByAddress,
|
||||
userAddress: state.userAddress,
|
||||
lastForceTokenStateRefetch: state.lastForceTokenStateRefetch,
|
||||
});
|
||||
|
||||
export const GenerateOrderForm: React.ComponentClass<GenerateOrderFormProps> = connect(mapStateToProps)(
|
||||
|
@ -6,18 +6,20 @@ import { Dispatch } from 'redux';
|
||||
import { Portal as PortalComponent, PortalAllProps as PortalComponentAllProps } from 'ts/components/portal';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { State } from 'ts/redux/reducer';
|
||||
import { BlockchainErrs, HashData, Order, ScreenWidths, Side, TokenByAddress, TokenStateByAddress } from 'ts/types';
|
||||
import { BlockchainErrs, HashData, Order, ProviderType, ScreenWidths, Side, TokenByAddress } from 'ts/types';
|
||||
import { constants } from 'ts/utils/constants';
|
||||
|
||||
interface ConnectedState {
|
||||
blockchainErr: BlockchainErrs;
|
||||
blockchainIsLoaded: boolean;
|
||||
hashData: HashData;
|
||||
injectedProviderName: string;
|
||||
networkId: number;
|
||||
nodeVersion: string;
|
||||
orderFillAmount: BigNumber;
|
||||
providerType: ProviderType;
|
||||
tokenByAddress: TokenByAddress;
|
||||
tokenStateByAddress: TokenStateByAddress;
|
||||
lastForceTokenStateRefetch: number;
|
||||
userEtherBalance: BigNumber;
|
||||
screenWidth: ScreenWidths;
|
||||
shouldBlockchainErrDialogBeOpen: boolean;
|
||||
@ -57,14 +59,16 @@ const mapStateToProps = (state: State, ownProps: PortalComponentAllProps): Conne
|
||||
return {
|
||||
blockchainErr: state.blockchainErr,
|
||||
blockchainIsLoaded: state.blockchainIsLoaded,
|
||||
hashData,
|
||||
injectedProviderName: state.injectedProviderName,
|
||||
networkId: state.networkId,
|
||||
nodeVersion: state.nodeVersion,
|
||||
orderFillAmount: state.orderFillAmount,
|
||||
hashData,
|
||||
providerType: state.providerType,
|
||||
screenWidth: state.screenWidth,
|
||||
shouldBlockchainErrDialogBeOpen: state.shouldBlockchainErrDialogBeOpen,
|
||||
tokenByAddress: state.tokenByAddress,
|
||||
tokenStateByAddress: state.tokenStateByAddress,
|
||||
lastForceTokenStateRefetch: state.lastForceTokenStateRefetch,
|
||||
userAddress: state.userAddress,
|
||||
userEtherBalance: state.userEtherBalance,
|
||||
userSuppliedOrderCache: state.userSuppliedOrderCache,
|
||||
|
1
packages/website/ts/globals.d.ts
vendored
@ -10,7 +10,6 @@ declare module 'thenby';
|
||||
declare module 'react-highlight';
|
||||
declare module 'react-recaptcha';
|
||||
declare module 'react-document-title';
|
||||
declare module 'ledgerco';
|
||||
declare module 'ethereumjs-tx';
|
||||
|
||||
declare module '*.json' {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as _ from 'lodash';
|
||||
import { localStorage } from 'ts/local_storage/local_storage';
|
||||
import { Token, TrackedTokensByUserAddress } from 'ts/types';
|
||||
import { Token, TokenByAddress, TrackedTokensByUserAddress } from 'ts/types';
|
||||
import { configs } from 'ts/utils/configs';
|
||||
|
||||
const TRACKED_TOKENS_KEY = 'trackedTokens';
|
||||
@ -39,18 +39,22 @@ export const trackedTokenStorage = {
|
||||
const trackedTokensByUserAddress = JSON.parse(trackedTokensJSONString);
|
||||
return trackedTokensByUserAddress;
|
||||
},
|
||||
getTrackedTokensIfExists(userAddress: string, networkId: number): Token[] {
|
||||
getTrackedTokensByAddress(userAddress: string, networkId: number): TokenByAddress {
|
||||
const trackedTokensByAddress: TokenByAddress = {};
|
||||
const trackedTokensJSONString = localStorage.getItemIfExists(TRACKED_TOKENS_KEY);
|
||||
if (_.isEmpty(trackedTokensJSONString)) {
|
||||
return undefined;
|
||||
return trackedTokensByAddress;
|
||||
}
|
||||
const trackedTokensByUserAddress = JSON.parse(trackedTokensJSONString);
|
||||
const trackedTokensByNetworkId = trackedTokensByUserAddress[userAddress];
|
||||
if (_.isUndefined(trackedTokensByNetworkId)) {
|
||||
return undefined;
|
||||
return trackedTokensByAddress;
|
||||
}
|
||||
const trackedTokens = trackedTokensByNetworkId[networkId];
|
||||
return trackedTokens;
|
||||
_.each(trackedTokens, (trackedToken: Token) => {
|
||||
trackedTokensByAddress[trackedToken.address] = trackedToken;
|
||||
});
|
||||
return trackedTokensByAddress;
|
||||
},
|
||||
removeTrackedToken(userAddress: string, networkId: number, tokenAddress: string): void {
|
||||
const trackedTokensByUserAddress = this.getTrackedTokensByUserAddress();
|
||||
|
@ -2,7 +2,7 @@ import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import * as DocumentTitle from 'react-document-title';
|
||||
import { Footer } from 'ts/components/footer';
|
||||
import { TopBar } from 'ts/components/top_bar';
|
||||
import { TopBar } from 'ts/components/top_bar/top_bar';
|
||||
import { Profile } from 'ts/pages/about/profile';
|
||||
import { ProfileInfo, Styles } from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
|
@ -5,7 +5,7 @@ import * as React from 'react';
|
||||
import DocumentTitle = require('react-document-title');
|
||||
import { scroller } from 'react-scroll';
|
||||
import semverSort = require('semver-sort');
|
||||
import { TopBar } from 'ts/components/top_bar';
|
||||
import { TopBar } from 'ts/components/top_bar/top_bar';
|
||||
import { Badge } from 'ts/components/ui/badge';
|
||||
import { Comment } from 'ts/pages/documentation/comment';
|
||||
import { DocsInfo } from 'ts/pages/documentation/docs_info';
|
||||
@ -40,9 +40,9 @@ import { utils } from 'ts/utils/utils';
|
||||
const SCROLL_TOP_ID = 'docsScrollTop';
|
||||
|
||||
const networkNameToColor: { [network: string]: string } = {
|
||||
[Networks.kovan]: colors.purple,
|
||||
[Networks.ropsten]: colors.red,
|
||||
[Networks.mainnet]: colors.turquois,
|
||||
[Networks.Kovan]: colors.purple,
|
||||
[Networks.Ropsten]: colors.red,
|
||||
[Networks.Mainnet]: colors.turquois,
|
||||
};
|
||||
|
||||
export interface DocumentationAllProps {
|
||||
@ -78,8 +78,10 @@ const styles: Styles = {
|
||||
};
|
||||
|
||||
export class Documentation extends React.Component<DocumentationAllProps, DocumentationState> {
|
||||
private _isUnmounted: boolean;
|
||||
constructor(props: DocumentationAllProps) {
|
||||
super(props);
|
||||
this._isUnmounted = false;
|
||||
this.state = {
|
||||
docAgnosticFormat: undefined,
|
||||
};
|
||||
@ -92,6 +94,9 @@ export class Documentation extends React.Component<DocumentationAllProps, Docume
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._fetchJSONDocsFireAndForgetAsync(preferredVersionIfExists);
|
||||
}
|
||||
public componentWillUnmount() {
|
||||
this._isUnmounted = true;
|
||||
}
|
||||
public render() {
|
||||
const menuSubsectionsBySection = _.isUndefined(this.state.docAgnosticFormat)
|
||||
? {}
|
||||
@ -367,6 +372,7 @@ export class Documentation extends React.Component<DocumentationAllProps, Docume
|
||||
);
|
||||
const docAgnosticFormat = this.props.docsInfo.convertToDocAgnosticFormat(versionDocObj as DoxityDocObj);
|
||||
|
||||
if (!this._isUnmounted) {
|
||||
this.setState(
|
||||
{
|
||||
docAgnosticFormat,
|
||||
@ -376,4 +382,5 @@ export class Documentation extends React.Component<DocumentationAllProps, Docume
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import * as DocumentTitle from 'react-document-title';
|
||||
import { Footer } from 'ts/components/footer';
|
||||
import { TopBar } from 'ts/components/top_bar';
|
||||
import { TopBar } from 'ts/components/top_bar/top_bar';
|
||||
import { Question } from 'ts/pages/faq/question';
|
||||
import { FAQQuestion, FAQSection, Styles, WebsitePaths } from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
|
@ -4,7 +4,7 @@ import * as React from 'react';
|
||||
import DocumentTitle = require('react-document-title');
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Footer } from 'ts/components/footer';
|
||||
import { TopBar } from 'ts/components/top_bar';
|
||||
import { TopBar } from 'ts/components/top_bar/top_bar';
|
||||
import { ScreenWidths, WebsitePaths } from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
import { constants } from 'ts/utils/constants';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { Footer } from 'ts/components/footer';
|
||||
import { TopBar } from 'ts/components/top_bar';
|
||||
import { TopBar } from 'ts/components/top_bar/top_bar';
|
||||
import { Styles } from 'ts/types';
|
||||
|
||||
export interface NotFoundProps {
|
||||
|
@ -3,7 +3,7 @@ import CircularProgress from 'material-ui/CircularProgress';
|
||||
import * as React from 'react';
|
||||
import DocumentTitle = require('react-document-title');
|
||||
import { scroller } from 'react-scroll';
|
||||
import { TopBar } from 'ts/components/top_bar';
|
||||
import { TopBar } from 'ts/components/top_bar/top_bar';
|
||||
import { MarkdownSection } from 'ts/pages/shared/markdown_section';
|
||||
import { NestedSidebarMenu } from 'ts/pages/shared/nested_sidebar_menu';
|
||||
import { SectionHeader } from 'ts/pages/shared/section_header';
|
||||
@ -45,8 +45,10 @@ const styles: Styles = {
|
||||
|
||||
export class Wiki extends React.Component<WikiProps, WikiState> {
|
||||
private _wikiBackoffTimeoutId: number;
|
||||
private _isUnmounted: boolean;
|
||||
constructor(props: WikiProps) {
|
||||
super(props);
|
||||
this._isUnmounted = false;
|
||||
this.state = {
|
||||
articlesBySection: undefined,
|
||||
};
|
||||
@ -56,6 +58,7 @@ export class Wiki extends React.Component<WikiProps, WikiState> {
|
||||
this._fetchArticlesBySectionAsync();
|
||||
}
|
||||
public componentWillUnmount() {
|
||||
this._isUnmounted = true;
|
||||
clearTimeout(this._wikiBackoffTimeoutId);
|
||||
}
|
||||
public render() {
|
||||
@ -179,6 +182,7 @@ export class Wiki extends React.Component<WikiProps, WikiState> {
|
||||
return;
|
||||
}
|
||||
const articlesBySection = await response.json();
|
||||
if (!this._isUnmounted) {
|
||||
this.setState(
|
||||
{
|
||||
articlesBySection,
|
||||
@ -188,6 +192,7 @@ export class Wiki extends React.Component<WikiProps, WikiState> {
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
private _getMenuSubsectionsBySection(articlesBySection: ArticlesBySection) {
|
||||
const sectionNames = _.keys(articlesBySection);
|
||||
const menuSubsectionsBySection: { [section: string]: string[] } = {};
|
||||
|
@ -9,9 +9,10 @@ import {
|
||||
ProviderType,
|
||||
ScreenWidths,
|
||||
Side,
|
||||
SideToAssetToken,
|
||||
SignatureData,
|
||||
Token,
|
||||
TokenStateByAddress,
|
||||
TokenByAddress,
|
||||
} from 'ts/types';
|
||||
|
||||
export class Dispatcher {
|
||||
@ -120,9 +121,20 @@ export class Dispatcher {
|
||||
type: ActionTypes.RemoveTokenFromTokenByAddress,
|
||||
});
|
||||
}
|
||||
public clearTokenByAddress() {
|
||||
public batchDispatch(
|
||||
tokenByAddress: TokenByAddress,
|
||||
networkId: number,
|
||||
userAddress: string,
|
||||
sideToAssetToken: SideToAssetToken,
|
||||
) {
|
||||
this._dispatch({
|
||||
type: ActionTypes.ClearTokenByAddress,
|
||||
data: {
|
||||
tokenByAddress,
|
||||
networkId,
|
||||
userAddress,
|
||||
sideToAssetToken,
|
||||
},
|
||||
type: ActionTypes.BatchDispatch,
|
||||
});
|
||||
}
|
||||
public updateTokenByAddress(tokens: Token[]) {
|
||||
@ -131,43 +143,9 @@ export class Dispatcher {
|
||||
type: ActionTypes.UpdateTokenByAddress,
|
||||
});
|
||||
}
|
||||
public updateTokenStateByAddress(tokenStateByAddress: TokenStateByAddress) {
|
||||
public forceTokenStateRefetch() {
|
||||
this._dispatch({
|
||||
data: tokenStateByAddress,
|
||||
type: ActionTypes.UpdateTokenStateByAddress,
|
||||
});
|
||||
}
|
||||
public removeFromTokenStateByAddress(tokenAddress: string) {
|
||||
this._dispatch({
|
||||
data: tokenAddress,
|
||||
type: ActionTypes.RemoveFromTokenStateByAddress,
|
||||
});
|
||||
}
|
||||
public replaceTokenAllowanceByAddress(address: string, allowance: BigNumber) {
|
||||
this._dispatch({
|
||||
data: {
|
||||
address,
|
||||
allowance,
|
||||
},
|
||||
type: ActionTypes.ReplaceTokenAllowanceByAddress,
|
||||
});
|
||||
}
|
||||
public replaceTokenBalanceByAddress(address: string, balance: BigNumber) {
|
||||
this._dispatch({
|
||||
data: {
|
||||
address,
|
||||
balance,
|
||||
},
|
||||
type: ActionTypes.ReplaceTokenBalanceByAddress,
|
||||
});
|
||||
}
|
||||
public updateTokenBalanceByAddress(address: string, balanceDelta: BigNumber) {
|
||||
this._dispatch({
|
||||
data: {
|
||||
address,
|
||||
balanceDelta,
|
||||
},
|
||||
type: ActionTypes.UpdateTokenBalanceByAddress,
|
||||
type: ActionTypes.ForceTokenStateRefetch,
|
||||
});
|
||||
}
|
||||
public updateSignatureData(signatureData: SignatureData) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ZeroEx } from '0x.js';
|
||||
import { BigNumber } from '@0xproject/utils';
|
||||
import * as _ from 'lodash';
|
||||
import * as moment from 'moment';
|
||||
import {
|
||||
Action,
|
||||
ActionTypes,
|
||||
@ -12,8 +13,6 @@ import {
|
||||
SideToAssetToken,
|
||||
SignatureData,
|
||||
TokenByAddress,
|
||||
TokenState,
|
||||
TokenStateByAddress,
|
||||
} from 'ts/types';
|
||||
import { utils } from 'ts/utils/utils';
|
||||
|
||||
@ -37,7 +36,7 @@ export interface State {
|
||||
shouldBlockchainErrDialogBeOpen: boolean;
|
||||
sideToAssetToken: SideToAssetToken;
|
||||
tokenByAddress: TokenByAddress;
|
||||
tokenStateByAddress: TokenStateByAddress;
|
||||
lastForceTokenStateRefetch: number;
|
||||
userAddress: string;
|
||||
userEtherBalance: BigNumber;
|
||||
// Note: cache of supplied orderJSON in fill order step. Do not use for anything else.
|
||||
@ -76,7 +75,7 @@ const INITIAL_STATE: State = {
|
||||
[Side.Receive]: {},
|
||||
},
|
||||
tokenByAddress: {},
|
||||
tokenStateByAddress: {},
|
||||
lastForceTokenStateRefetch: moment().unix(),
|
||||
userAddress: '',
|
||||
userEtherBalance: new BigNumber(0),
|
||||
userSuppliedOrderCache: undefined,
|
||||
@ -139,13 +138,6 @@ export function reducer(state: State = INITIAL_STATE, action: Action) {
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.ClearTokenByAddress: {
|
||||
return {
|
||||
...state,
|
||||
tokenByAddress: {},
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.AddTokenToTokenByAddress: {
|
||||
const newTokenByAddress = state.tokenByAddress;
|
||||
newTokenByAddress[action.data.address] = action.data;
|
||||
@ -180,74 +172,21 @@ export function reducer(state: State = INITIAL_STATE, action: Action) {
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.UpdateTokenStateByAddress: {
|
||||
const tokenStateByAddress = state.tokenStateByAddress;
|
||||
const updatedTokenStateByAddress = action.data;
|
||||
_.each(updatedTokenStateByAddress, (tokenState: TokenState, address: string) => {
|
||||
const updatedTokenState = {
|
||||
...tokenStateByAddress[address],
|
||||
...tokenState,
|
||||
};
|
||||
tokenStateByAddress[address] = updatedTokenState;
|
||||
});
|
||||
case ActionTypes.BatchDispatch: {
|
||||
return {
|
||||
...state,
|
||||
tokenStateByAddress,
|
||||
networkId: action.data.networkId,
|
||||
userAddress: action.data.userAddress,
|
||||
sideToAssetToken: action.data.sideToAssetToken,
|
||||
tokenByAddress: action.data.tokenByAddress,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.RemoveFromTokenStateByAddress: {
|
||||
const tokenStateByAddress = state.tokenStateByAddress;
|
||||
const tokenAddress = action.data;
|
||||
delete tokenStateByAddress[tokenAddress];
|
||||
case ActionTypes.ForceTokenStateRefetch:
|
||||
return {
|
||||
...state,
|
||||
tokenStateByAddress,
|
||||
lastForceTokenStateRefetch: moment().unix(),
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.ReplaceTokenAllowanceByAddress: {
|
||||
const tokenStateByAddress = state.tokenStateByAddress;
|
||||
const allowance = action.data.allowance;
|
||||
const tokenAddress = action.data.address;
|
||||
tokenStateByAddress[tokenAddress] = {
|
||||
...tokenStateByAddress[tokenAddress],
|
||||
allowance,
|
||||
};
|
||||
return {
|
||||
...state,
|
||||
tokenStateByAddress,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.ReplaceTokenBalanceByAddress: {
|
||||
const tokenStateByAddress = state.tokenStateByAddress;
|
||||
const balance = action.data.balance;
|
||||
const tokenAddress = action.data.address;
|
||||
tokenStateByAddress[tokenAddress] = {
|
||||
...tokenStateByAddress[tokenAddress],
|
||||
balance,
|
||||
};
|
||||
return {
|
||||
...state,
|
||||
tokenStateByAddress,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.UpdateTokenBalanceByAddress: {
|
||||
const tokenStateByAddress = state.tokenStateByAddress;
|
||||
const balanceDelta = action.data.balanceDelta;
|
||||
const tokenAddress = action.data.address;
|
||||
const currBalance = tokenStateByAddress[tokenAddress].balance;
|
||||
tokenStateByAddress[tokenAddress] = {
|
||||
...tokenStateByAddress[tokenAddress],
|
||||
balance: currBalance.plus(balanceDelta),
|
||||
};
|
||||
return {
|
||||
...state,
|
||||
tokenStateByAddress,
|
||||
};
|
||||
}
|
||||
|
||||
case ActionTypes.UpdateOrderSignatureData: {
|
||||
return {
|
||||
|
@ -25,10 +25,6 @@ export interface TokenState {
|
||||
balance: BigNumber;
|
||||
}
|
||||
|
||||
export interface TokenStateByAddress {
|
||||
[address: string]: TokenState;
|
||||
}
|
||||
|
||||
export interface AssetToken {
|
||||
address?: string;
|
||||
amount?: BigNumber;
|
||||
@ -110,12 +106,12 @@ export enum BalanceErrs {
|
||||
|
||||
export enum ActionTypes {
|
||||
// Portal
|
||||
BatchDispatch = 'BATCH_DISPATCH',
|
||||
UpdateScreenWidth = 'UPDATE_SCREEN_WIDTH',
|
||||
UpdateNodeVersion = 'UPDATE_NODE_VERSION',
|
||||
ResetState = 'RESET_STATE',
|
||||
AddTokenToTokenByAddress = 'ADD_TOKEN_TO_TOKEN_BY_ADDRESS',
|
||||
BlockchainErrEncountered = 'BLOCKCHAIN_ERR_ENCOUNTERED',
|
||||
ClearTokenByAddress = 'CLEAR_TOKEN_BY_ADDRESS',
|
||||
UpdateBlockchainIsLoaded = 'UPDATE_BLOCKCHAIN_IS_LOADED',
|
||||
UpdateNetworkId = 'UPDATE_NETWORK_ID',
|
||||
UpdateChosenAssetToken = 'UPDATE_CHOSEN_ASSET_TOKEN',
|
||||
@ -125,11 +121,7 @@ export enum ActionTypes {
|
||||
UpdateOrderSignatureData = 'UPDATE_ORDER_SIGNATURE_DATA',
|
||||
UpdateTokenByAddress = 'UPDATE_TOKEN_BY_ADDRESS',
|
||||
RemoveTokenFromTokenByAddress = 'REMOVE_TOKEN_FROM_TOKEN_BY_ADDRESS',
|
||||
UpdateTokenStateByAddress = 'UPDATE_TOKEN_STATE_BY_ADDRESS',
|
||||
RemoveFromTokenStateByAddress = 'REMOVE_FROM_TOKEN_STATE_BY_ADDRESS',
|
||||
ReplaceTokenAllowanceByAddress = 'REPLACE_TOKEN_ALLOWANCE_BY_ADDRESS',
|
||||
ReplaceTokenBalanceByAddress = 'REPLACE_TOKEN_BALANCE_BY_ADDRESS',
|
||||
UpdateTokenBalanceByAddress = 'UPDATE_TOKEN_BALANCE_BY_ADDRESS',
|
||||
ForceTokenStateRefetch = 'FORCE_TOKEN_STATE_REFETCH',
|
||||
UpdateOrderExpiry = 'UPDATE_ORDER_EXPIRY',
|
||||
SwapAssetTokens = 'SWAP_ASSET_TOKENS',
|
||||
UpdateUserAddress = 'UPDATE_USER_ADDRESS',
|
||||
@ -496,16 +488,6 @@ export interface SignPersonalMessageParams {
|
||||
data: string;
|
||||
}
|
||||
|
||||
export interface TxParams {
|
||||
nonce: string;
|
||||
gasPrice?: number;
|
||||
gasLimit: string;
|
||||
to: string;
|
||||
value?: string;
|
||||
data?: string;
|
||||
chainId: number; // EIP 155 chainId - mainnet: 1, ropsten: 3
|
||||
}
|
||||
|
||||
export interface PublicNodeUrlsByNetworkId {
|
||||
[networkId: number]: string[];
|
||||
}
|
||||
@ -610,10 +592,10 @@ export interface AddressByContractName {
|
||||
}
|
||||
|
||||
export enum Networks {
|
||||
mainnet = 'Mainnet',
|
||||
kovan = 'Kovan',
|
||||
ropsten = 'Ropsten',
|
||||
rinkeby = 'Rinkeby',
|
||||
Mainnet = 'Mainnet',
|
||||
Kovan = 'Kovan',
|
||||
Ropsten = 'Ropsten',
|
||||
Rinkeby = 'Rinkeby',
|
||||
}
|
||||
|
||||
export enum AbiTypes {
|
||||
@ -678,4 +660,9 @@ export enum SmartContractDocSections {
|
||||
ZRXToken = 'ZRXToken',
|
||||
}
|
||||
|
||||
export interface MaterialUIPosition {
|
||||
vertical: 'bottom' | 'top' | 'center';
|
||||
horizontal: 'left' | 'middle' | 'right';
|
||||
}
|
||||
|
||||
// tslint:disable:max-file-line-count
|
||||
|
@ -16,24 +16,24 @@ const isDevelopment = _.includes(
|
||||
const INFURA_API_KEY = 'T5WSC8cautR4KXyYgsRs';
|
||||
|
||||
export const configs = {
|
||||
BACKEND_BASE_URL: isDevelopment ? 'https://localhost:3001' : 'https://website-api.0xproject.com',
|
||||
BACKEND_BASE_URL: 'https://website-api.0xproject.com',
|
||||
BASE_URL,
|
||||
BITLY_ACCESS_TOKEN: 'ffc4c1a31e5143848fb7c523b39f91b9b213d208',
|
||||
CONTRACT_ADDRESS: {
|
||||
'1.0.0': {
|
||||
[Networks.mainnet]: {
|
||||
[Networks.Mainnet]: {
|
||||
[SmartContractDocSections.Exchange]: '0x12459c951127e0c374ff9105dda097662a027093',
|
||||
[SmartContractDocSections.TokenTransferProxy]: '0x8da0d80f5007ef1e431dd2127178d224e32c2ef4',
|
||||
[SmartContractDocSections.ZRXToken]: '0xe41d2489571d322189246dafa5ebde1f4699f498',
|
||||
[SmartContractDocSections.TokenRegistry]: '0x926a74c5c36adf004c87399e65f75628b0f98d2c',
|
||||
},
|
||||
[Networks.ropsten]: {
|
||||
[Networks.Ropsten]: {
|
||||
[SmartContractDocSections.Exchange]: '0x479cc461fecd078f766ecc58533d6f69580cf3ac',
|
||||
[SmartContractDocSections.TokenTransferProxy]: '0x4e9aad8184de8833365fea970cd9149372fdf1e6',
|
||||
[SmartContractDocSections.ZRXToken]: '0xa8e9fa8f91e5ae138c74648c9c304f1c75003a8d',
|
||||
[SmartContractDocSections.TokenRegistry]: '0x6b1a50f0bb5a7995444bd3877b22dc89c62843ed',
|
||||
},
|
||||
[Networks.kovan]: {
|
||||
[Networks.Kovan]: {
|
||||
[SmartContractDocSections.Exchange]: '0x90fe2af704b34e0224bf2299c838e04d4dcf1364',
|
||||
[SmartContractDocSections.TokenTransferProxy]: '0x087Eed4Bc1ee3DE49BeFbd66C662B434B15d49d4',
|
||||
[SmartContractDocSections.ZRXToken]: '0x6ff6c0ff1d68b964901f986d4c9fa3ac68346570',
|
||||
@ -120,6 +120,8 @@ export const configs = {
|
||||
PUBLIC_NODE_URLS_BY_NETWORK_ID: {
|
||||
[1]: [`https://mainnet.infura.io/${INFURA_API_KEY}`, 'https://mainnet.0xproject.com'],
|
||||
[42]: [`https://kovan.infura.io/${INFURA_API_KEY}`, 'https://kovan.0xproject.com'],
|
||||
[3]: [`https://ropsten.infura.io/${INFURA_API_KEY}`],
|
||||
[4]: [`https://rinkeby.infura.io/${INFURA_API_KEY}`],
|
||||
} as PublicNodeUrlsByNetworkId,
|
||||
SHOULD_DEPRECATE_OLD_WETH_TOKEN: true,
|
||||
SYMBOLS_OF_MINTABLE_TOKENS: ['MKR', 'MLN', 'GNT', 'DGD', 'REP'],
|
||||
|
@ -10,6 +10,8 @@ export const constants = {
|
||||
1: 4145578,
|
||||
42: 3117574,
|
||||
50: 0,
|
||||
3: 1719261,
|
||||
4: 1570919,
|
||||
} as { [networkId: number]: number },
|
||||
HOME_SCROLL_DURATION_MS: 500,
|
||||
HTTP_NO_CONTENT_STATUS_CODE: 204,
|
||||
@ -19,19 +21,19 @@ export const constants = {
|
||||
MAINNET_NAME: 'Main network',
|
||||
MINT_AMOUNT: new BigNumber('100000000000000000000'),
|
||||
NETWORK_ID_MAINNET: 1,
|
||||
NETWORK_ID_TESTNET: 42,
|
||||
NETWORK_ID_KOVAN: 42,
|
||||
NETWORK_ID_TESTRPC: 50,
|
||||
NETWORK_NAME_BY_ID: {
|
||||
1: Networks.mainnet,
|
||||
3: Networks.ropsten,
|
||||
4: Networks.rinkeby,
|
||||
42: Networks.kovan,
|
||||
1: Networks.Mainnet,
|
||||
3: Networks.Ropsten,
|
||||
4: Networks.Rinkeby,
|
||||
42: Networks.Kovan,
|
||||
} as { [symbol: number]: string },
|
||||
NETWORK_ID_BY_NAME: {
|
||||
[Networks.mainnet]: 1,
|
||||
[Networks.ropsten]: 3,
|
||||
[Networks.rinkeby]: 4,
|
||||
[Networks.kovan]: 42,
|
||||
[Networks.Mainnet]: 1,
|
||||
[Networks.Ropsten]: 3,
|
||||
[Networks.Rinkeby]: 4,
|
||||
[Networks.Kovan]: 42,
|
||||
} as { [networkName: string]: number },
|
||||
NULL_ADDRESS: '0x0000000000000000000000000000000000000000',
|
||||
PROVIDER_NAME_LEDGER: 'Ledger',
|
||||
|
@ -8,6 +8,7 @@ export const muiTheme = getMuiTheme({
|
||||
textColor: colors.black,
|
||||
},
|
||||
palette: {
|
||||
accent1Color: colors.lightBlueA700,
|
||||
pickerHeaderColor: colors.lightBlue,
|
||||
primary1Color: colors.lightBlue,
|
||||
primary2Color: colors.lightBlue,
|
||||
|
@ -151,7 +151,7 @@ export const utils = {
|
||||
if (_.isUndefined(networkName)) {
|
||||
return undefined;
|
||||
}
|
||||
const etherScanPrefix = networkName === Networks.mainnet ? '' : `${networkName.toLowerCase()}.`;
|
||||
const etherScanPrefix = networkName === Networks.Mainnet ? '' : `${networkName.toLowerCase()}.`;
|
||||
return `https://${etherScanPrefix}etherscan.io/${suffix}/${addressOrTxHash}`;
|
||||
},
|
||||
setUrlHash(anchorId: string) {
|
||||
@ -183,7 +183,7 @@ export const utils = {
|
||||
// after a user was prompted to sign a message or send a transaction and decided to
|
||||
// reject the request.
|
||||
didUserDenyWeb3Request(errMsg: string) {
|
||||
const metamaskDenialErrMsg = 'User denied message';
|
||||
const metamaskDenialErrMsg = 'User denied';
|
||||
const paritySignerDenialErrMsg = 'Request has been rejected';
|
||||
const ledgerDenialErrMsg = 'Invalid status 6985';
|
||||
const isUserDeniedErrMsg =
|
||||
@ -276,4 +276,10 @@ export const utils = {
|
||||
exchangeContractErrorToHumanReadableError[error] || ZeroExErrorToHumanReadableError[error];
|
||||
return humanReadableErrorMsg;
|
||||
},
|
||||
isParityNode(nodeVersion: string): boolean {
|
||||
return _.includes(nodeVersion, 'Parity');
|
||||
},
|
||||
isTestRpc(nodeVersion: string): boolean {
|
||||
return _.includes(nodeVersion, 'TestRPC');
|
||||
},
|
||||
};
|
||||
|
@ -24,9 +24,6 @@ export class Web3Wrapper {
|
||||
|
||||
this._web3 = new Web3();
|
||||
this._web3.setProvider(provider);
|
||||
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._startEmittingNetworkConnectionAndUserBalanceStateAsync();
|
||||
}
|
||||
public isAddress(address: string) {
|
||||
return this._web3.isAddress(address);
|
||||
@ -90,11 +87,7 @@ export class Web3Wrapper {
|
||||
public updatePrevUserAddress(userAddress: string) {
|
||||
this._prevUserAddress = userAddress;
|
||||
}
|
||||
private async _getNetworkAsync() {
|
||||
const networkId = await promisify(this._web3.version.getNetwork)();
|
||||
return networkId;
|
||||
}
|
||||
private async _startEmittingNetworkConnectionAndUserBalanceStateAsync() {
|
||||
public startEmittingNetworkConnectionAndUserBalanceState() {
|
||||
if (!_.isUndefined(this._watchNetworkAndBalanceIntervalId)) {
|
||||
return; // we are already emitting the state
|
||||
}
|
||||
@ -127,7 +120,7 @@ export class Web3Wrapper {
|
||||
}
|
||||
|
||||
// Check for user ether balance changes
|
||||
if (userAddressIfExists !== '') {
|
||||
if (!_.isEmpty(userAddressIfExists)) {
|
||||
await this._updateUserEtherBalanceAsync(userAddressIfExists);
|
||||
}
|
||||
} else {
|
||||
@ -140,11 +133,15 @@ export class Web3Wrapper {
|
||||
},
|
||||
5000,
|
||||
(err: Error) => {
|
||||
utils.consoleLog(`Watching network and balances failed: ${err}`);
|
||||
utils.consoleLog(`Watching network and balances failed: ${err.stack}`);
|
||||
this._stopEmittingNetworkConnectionAndUserBalanceStateAsync();
|
||||
},
|
||||
);
|
||||
}
|
||||
private async _getNetworkAsync() {
|
||||
const networkId = await promisify(this._web3.version.getNetwork)();
|
||||
return networkId;
|
||||
}
|
||||
private async _updateUserEtherBalanceAsync(userAddress: string) {
|
||||
const balance = await this.getBalanceInEthAsync(userAddress);
|
||||
if (!balance.eq(this._prevUserEtherBalanceInEth)) {
|
||||
@ -153,6 +150,8 @@ export class Web3Wrapper {
|
||||
}
|
||||
}
|
||||
private _stopEmittingNetworkConnectionAndUserBalanceStateAsync() {
|
||||
if (!_.isUndefined(this._watchNetworkAndBalanceIntervalId)) {
|
||||
intervalUtils.clearAsyncExcludingInterval(this._watchNetworkAndBalanceIntervalId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|