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