commit
279a6da364
|
@ -1,6 +1,7 @@
|
|||
# MyCrypto Beta (VISIT [MyCryptoHQ/mycrypto.com](https://github.com/MyCryptoHQ/mycrypto.com) for the current site)<br/>Just looking to download? Grab our [latest release](https://github.com/MyCryptoHQ/MyCrypto/releases)
|
||||
|
||||
[![Greenkeeper badge](https://badges.greenkeeper.io/MyCryptoHq/MyCrypto.svg)](https://greenkeeper.io/)
|
||||
[![Coverage Status](https://coveralls.io/repos/github/MyCryptoHQ/MyCrypto/badge.svg?branch=develop)](https://coveralls.io/github/MyCryptoHQ/MyCrypto?branch=develop)
|
||||
|
||||
## Running the App
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import SendTransaction from 'containers/Tabs/SendTransaction';
|
|||
import Swap from 'containers/Tabs/Swap';
|
||||
import SignAndVerifyMessage from 'containers/Tabs/SignAndVerifyMessage';
|
||||
import BroadcastTx from 'containers/Tabs/BroadcastTx';
|
||||
import CheckTransaction from 'containers/Tabs/CheckTransaction';
|
||||
import ErrorScreen from 'components/ErrorScreen';
|
||||
import PageNotFound from 'components/PageNotFound';
|
||||
import LogOutPrompt from 'components/LogOutPrompt';
|
||||
|
@ -67,6 +68,7 @@ export default class Root extends Component<Props, State> {
|
|||
<Route path="/contracts" component={Contracts} />
|
||||
<Route path="/ens" component={ENS} exact={true} />
|
||||
<Route path="/sign-and-verify-message" component={SignAndVerifyMessage} />
|
||||
<Route path="/tx-status" component={CheckTransaction} exact={true} />
|
||||
<Route path="/pushTx" component={BroadcastTx} />
|
||||
<RouteNotFound />
|
||||
</Switch>
|
||||
|
@ -120,8 +122,7 @@ const LegacyRoutes = withRouter(props => {
|
|||
history.push('/account/info');
|
||||
break;
|
||||
case '#check-tx-status':
|
||||
history.push('/check-tx-status');
|
||||
break;
|
||||
return <RedirectWithQuery from={pathname} to={'/tx-status'} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import * as interfaces from './actionTypes';
|
||||
import { TypeKeys } from './constants';
|
||||
|
||||
export type TFetchGasEstimates = typeof fetchGasEstimates;
|
||||
export function fetchGasEstimates(): interfaces.FetchGasEstimatesAction {
|
||||
return {
|
||||
type: TypeKeys.GAS_FETCH_ESTIMATES
|
||||
};
|
||||
}
|
||||
|
||||
export type TSetGasEstimates = typeof setGasEstimates;
|
||||
export function setGasEstimates(
|
||||
payload: interfaces.SetGasEstimatesAction['payload']
|
||||
): interfaces.SetGasEstimatesAction {
|
||||
return {
|
||||
type: TypeKeys.GAS_SET_ESTIMATES,
|
||||
payload
|
||||
};
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { TypeKeys } from './constants';
|
||||
import { GasEstimates } from 'api/gas';
|
||||
|
||||
export interface FetchGasEstimatesAction {
|
||||
type: TypeKeys.GAS_FETCH_ESTIMATES;
|
||||
}
|
||||
|
||||
export interface SetGasEstimatesAction {
|
||||
type: TypeKeys.GAS_SET_ESTIMATES;
|
||||
payload: GasEstimates;
|
||||
}
|
||||
|
||||
/*** Union Type ***/
|
||||
export type GasAction = FetchGasEstimatesAction | SetGasEstimatesAction;
|
|
@ -0,0 +1,4 @@
|
|||
export enum TypeKeys {
|
||||
GAS_FETCH_ESTIMATES = 'GAS_FETCH_ESTIMATES',
|
||||
GAS_SET_ESTIMATES = 'GAS_SET_ESTIMATES'
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from './actionCreators';
|
||||
export * from './actionTypes';
|
||||
export * from './constants';
|
|
@ -0,0 +1,20 @@
|
|||
import * as interfaces from './actionTypes';
|
||||
import { TypeKeys } from './constants';
|
||||
|
||||
export type TFetchTransactionData = typeof fetchTransactionData;
|
||||
export function fetchTransactionData(txhash: string): interfaces.FetchTransactionDataAction {
|
||||
return {
|
||||
type: TypeKeys.TRANSACTIONS_FETCH_TRANSACTION_DATA,
|
||||
payload: txhash
|
||||
};
|
||||
}
|
||||
|
||||
export type TSetTransactionData = typeof setTransactionData;
|
||||
export function setTransactionData(
|
||||
payload: interfaces.SetTransactionDataAction['payload']
|
||||
): interfaces.SetTransactionDataAction {
|
||||
return {
|
||||
type: TypeKeys.TRANSACTIONS_SET_TRANSACTION_DATA,
|
||||
payload
|
||||
};
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
import { TypeKeys } from './constants';
|
||||
import { TransactionData, TransactionReceipt } from 'libs/nodes';
|
||||
|
||||
export interface FetchTransactionDataAction {
|
||||
type: TypeKeys.TRANSACTIONS_FETCH_TRANSACTION_DATA;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export interface SetTransactionDataAction {
|
||||
type: TypeKeys.TRANSACTIONS_SET_TRANSACTION_DATA;
|
||||
payload: {
|
||||
txhash: string;
|
||||
data: TransactionData | null;
|
||||
receipt: TransactionReceipt | null;
|
||||
error: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
/*** Union Type ***/
|
||||
export type TransactionsAction = FetchTransactionDataAction | SetTransactionDataAction;
|
|
@ -0,0 +1,5 @@
|
|||
export enum TypeKeys {
|
||||
TRANSACTIONS_FETCH_TRANSACTION_DATA = 'TRANSACTIONS_FETCH_TRANSACTION_DATA',
|
||||
TRANSACTIONS_SET_TRANSACTION_DATA = 'TRANSACTIONS_SET_TRANSACTION_DATA',
|
||||
TRANSACTIONS_SET_TRANSACTION_ERROR = 'TRANSACTIONS_SET_TRANSACTION_ERROR'
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from './actionCreators';
|
||||
export * from './actionTypes';
|
||||
export * from './constants';
|
|
@ -0,0 +1,71 @@
|
|||
import { checkHttpStatus, parseJSON } from './utils';
|
||||
|
||||
const MAX_GAS_FAST = 250;
|
||||
|
||||
interface RawGasEstimates {
|
||||
safeLow: number;
|
||||
standard: number;
|
||||
fast: number;
|
||||
fastest: number;
|
||||
block_time: number;
|
||||
blockNum: number;
|
||||
}
|
||||
|
||||
export interface GasEstimates {
|
||||
safeLow: number;
|
||||
standard: number;
|
||||
fast: number;
|
||||
fastest: number;
|
||||
time: number;
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
export function fetchGasEstimates(): Promise<GasEstimates> {
|
||||
return fetch('https://dev.blockscale.net/api/gasexpress.json', {
|
||||
mode: 'cors'
|
||||
})
|
||||
.then(checkHttpStatus)
|
||||
.then(parseJSON)
|
||||
.then((res: object) => {
|
||||
// Make sure it looks like a raw gas estimate, and it has valid values
|
||||
const keys = ['safeLow', 'standard', 'fast', 'fastest'];
|
||||
keys.forEach(key => {
|
||||
if (typeof res[key] !== 'number') {
|
||||
throw new Error(
|
||||
`Gas estimate API has invalid shape: Expected numeric key '${key}' in response, got '${
|
||||
res[key]
|
||||
}' instead`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure the estimate isn't totally crazy
|
||||
const estimateRes = res as RawGasEstimates;
|
||||
if (estimateRes.fast > MAX_GAS_FAST) {
|
||||
throw new Error(
|
||||
`Gas estimate response estimate too high: Max fast is ${MAX_GAS_FAST}, was given ${
|
||||
estimateRes.fast
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
estimateRes.safeLow > estimateRes.standard ||
|
||||
estimateRes.standard > estimateRes.fast ||
|
||||
estimateRes.fast > estimateRes.fastest
|
||||
) {
|
||||
throw new Error(
|
||||
`Gas esimates are in illogical order: should be safeLow < standard < fast < fastest, received ${
|
||||
estimateRes.safeLow
|
||||
} < ${estimateRes.standard} < ${estimateRes.fast} < ${estimateRes.fastest}`
|
||||
);
|
||||
}
|
||||
|
||||
return estimateRes;
|
||||
})
|
||||
.then((res: RawGasEstimates) => ({
|
||||
...res,
|
||||
time: Date.now(),
|
||||
isDefault: false
|
||||
}));
|
||||
}
|
|
@ -134,17 +134,21 @@ class AccountInfo extends React.Component<Props, State> {
|
|||
symbol={balance.wei ? network.name : null}
|
||||
/>
|
||||
</span>
|
||||
{balance.isPending ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
!isOffline && (
|
||||
<button
|
||||
className="AccountInfo-section-refresh"
|
||||
onClick={this.props.setAccountBalance}
|
||||
>
|
||||
<i className="fa fa-refresh" />
|
||||
</button>
|
||||
)
|
||||
{balance.wei && (
|
||||
<React.Fragment>
|
||||
{balance.isPending ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
!isOffline && (
|
||||
<button
|
||||
className="AccountInfo-section-refresh"
|
||||
onClick={this.props.setAccountBalance}
|
||||
>
|
||||
<i className="fa fa-refresh" />
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import BityLogo from 'assets/images/logo-bity-white.svg';
|
||||
|
||||
export const Bity: React.SFC = () => (
|
||||
<Link className="Promos-promo Promos-Bity" target="_blank" rel="noopener noreferrer" to="/swap">
|
||||
<div className="Promos-promo-inner">
|
||||
<div className="Promos-promo-text">
|
||||
<p>It’s now easier to get more ETH</p>
|
||||
<h5>Swap BTC <-> ETH</h5>
|
||||
</div>
|
||||
<div className="Promos-promo-images">
|
||||
<img src={BityLogo} />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
|
@ -1,11 +1,10 @@
|
|||
import React from 'react';
|
||||
import CoinbaseLogo from 'assets/images/logo-coinbase.svg';
|
||||
import { NewTabLink } from 'components/ui';
|
||||
|
||||
export const Coinbase: React.SFC = () => (
|
||||
<a
|
||||
className="Promos-promo Promos-Coinbase"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<NewTabLink
|
||||
className="Promos-promo Promos--coinbase"
|
||||
href="https://buy.coinbase.com?code=60c05061-3a76-57be-b1cd-a7afa97bcb8c&address=0xA7DeFf12461661212734dB35AdE9aE7d987D648c&crypto_currency=ETH¤cy=USD"
|
||||
>
|
||||
<div className="Promos-promo-inner">
|
||||
|
@ -17,5 +16,5 @@ export const Coinbase: React.SFC = () => (
|
|||
<img src={CoinbaseLogo} />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</NewTabLink>
|
||||
);
|
||||
|
|
|
@ -5,10 +5,7 @@ import ledgerLogo from 'assets/images/logo-ledger.svg';
|
|||
import trezorLogo from 'assets/images/logo-trezor.svg';
|
||||
|
||||
export const HardwareWallets: React.SFC = () => (
|
||||
<HelpLink
|
||||
className="Promos-promo Promos-HardwareWallets"
|
||||
article={HELP_ARTICLE.PROTECT_YOUR_FUNDS}
|
||||
>
|
||||
<HelpLink className="Promos-promo Promos--hardware" article={HELP_ARTICLE.PROTECT_YOUR_FUNDS}>
|
||||
<div className="Promos-promo-inner">
|
||||
<div className="Promos-promo-text">
|
||||
<h6>Learn more about protecting your funds.</h6>
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ShapeshiftLogo from 'assets/images/logo-shapeshift.svg';
|
||||
|
||||
export const Shapeshift: React.SFC = () => (
|
||||
<Link className="Promos-promo Promos--shapeshift" to="/swap">
|
||||
<div className="Promos-promo-inner">
|
||||
<div className="Promos-promo-text">
|
||||
<h5>
|
||||
Exchange Coins
|
||||
<br />
|
||||
& Tokens with
|
||||
</h5>
|
||||
</div>
|
||||
<div className="Promos-promo-images">
|
||||
<img src={ShapeshiftLogo} />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
|
@ -1,3 +1,3 @@
|
|||
export * from './HardwareWallets';
|
||||
export * from './Coinbase';
|
||||
export * from './Bity';
|
||||
export * from './Shapeshift';
|
||||
|
|
|
@ -9,18 +9,6 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-Bity {
|
||||
background-color: #006e79;
|
||||
}
|
||||
|
||||
&-Coinbase {
|
||||
background-color: #2b71b1;
|
||||
}
|
||||
|
||||
&-HardwareWallets {
|
||||
background-color: #6e9a3e;
|
||||
}
|
||||
|
||||
&-promo {
|
||||
position: relative;
|
||||
height: inherit;
|
||||
|
@ -42,19 +30,18 @@
|
|||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
&-text,
|
||||
&-images {
|
||||
padding: 0 $space-sm;
|
||||
padding: 0 $space;
|
||||
}
|
||||
|
||||
&-text {
|
||||
flex: 1;
|
||||
flex: 5;
|
||||
padding-right: $space-xs;
|
||||
max-width: 220px;
|
||||
|
||||
p,
|
||||
h4,
|
||||
|
@ -73,15 +60,15 @@
|
|||
}
|
||||
|
||||
&-images {
|
||||
padding: 0 $space * 1.5;
|
||||
flex: 3;
|
||||
max-width: 108px;
|
||||
padding-left: $space-xs;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 96px;
|
||||
height: auto;
|
||||
padding: $space-xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -106,6 +93,23 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Per-promo customizations
|
||||
&--shapeshift {
|
||||
background-color: #263A52;
|
||||
|
||||
.Promos-promo-images {
|
||||
max-width: 130px;
|
||||
}
|
||||
}
|
||||
|
||||
&--coinbase {
|
||||
background-color: #2b71b1;
|
||||
}
|
||||
|
||||
&--hardware {
|
||||
background-color: #6e9a3e;
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-exit {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react';
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||
import { HardwareWallets, Coinbase, Bity } from './PromoComponents';
|
||||
import { HardwareWallets, Coinbase, Shapeshift } from './PromoComponents';
|
||||
import './Promos.scss';
|
||||
|
||||
const promos = [HardwareWallets, Coinbase, Bity];
|
||||
const promos = [HardwareWallets, Coinbase, Shapeshift];
|
||||
|
||||
const CarouselAnimation = ({ children, ...props }) => (
|
||||
<CSSTransition {...props} timeout={300} classNames="carousel">
|
||||
|
@ -36,11 +36,7 @@ export default class Promos extends React.PureComponent<{}, State> {
|
|||
return (
|
||||
<div className="Promos">
|
||||
<TransitionGroup className="Promos-promo-wrapper">
|
||||
{promos
|
||||
.filter(i => {
|
||||
return i === promos[activePromo];
|
||||
})
|
||||
.map(promo => <CarouselAnimation key={Math.random()}>{promo}</CarouselAnimation>)}
|
||||
<CarouselAnimation key={Math.random()}>{promos[activePromo]}</CarouselAnimation>
|
||||
</TransitionGroup>
|
||||
<div className="Promos-nav">
|
||||
{promos.map((_, index) => {
|
||||
|
|
|
@ -13,19 +13,24 @@ interface ReduxProps {
|
|||
wallet: AppState['wallet']['inst'];
|
||||
}
|
||||
|
||||
type Props = ReduxProps;
|
||||
|
||||
interface State {
|
||||
walletAddress: string | null;
|
||||
}
|
||||
|
||||
class CurrentCustomMessageClass extends PureComponent<ReduxProps, State> {
|
||||
class CurrentCustomMessageClass extends PureComponent<Props, State> {
|
||||
public state: State = {
|
||||
walletAddress: null
|
||||
};
|
||||
|
||||
public async componentDidMount() {
|
||||
if (this.props.wallet) {
|
||||
const walletAddress = await this.props.wallet.getAddressString();
|
||||
this.setState({ walletAddress });
|
||||
this.setAddressState(this.props);
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
if (this.props.wallet !== nextProps.wallet) {
|
||||
this.setAddressState(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,6 +47,15 @@ class CurrentCustomMessageClass extends PureComponent<ReduxProps, State> {
|
|||
}
|
||||
}
|
||||
|
||||
private async setAddressState(props: Props) {
|
||||
if (props.wallet) {
|
||||
const walletAddress = await props.wallet.getAddressString();
|
||||
this.setState({ walletAddress });
|
||||
} else {
|
||||
this.setState({ walletAddress: '' });
|
||||
}
|
||||
}
|
||||
|
||||
private getMessage() {
|
||||
const { currentTo, tokens } = this.props;
|
||||
const { walletAddress } = this.state;
|
||||
|
|
|
@ -53,8 +53,8 @@ class GasPriceDropdown extends Component<Props> {
|
|||
<input
|
||||
type="range"
|
||||
value={this.props.gasPrice.raw}
|
||||
min={gasPriceDefaults.gasPriceMinGwei}
|
||||
max={gasPriceDefaults.gasPriceMaxGwei}
|
||||
min={gasPriceDefaults.minGwei}
|
||||
max={gasPriceDefaults.maxGwei}
|
||||
onChange={this.handleGasPriceChange}
|
||||
/>
|
||||
<p className="small col-xs-4 text-left GasPrice-padding-reset">Not So Fast</p>
|
||||
|
|
|
@ -34,6 +34,10 @@ const tabs: TabLink[] = [
|
|||
name: 'Sign & Verify Message',
|
||||
to: '/sign-and-verify-message'
|
||||
},
|
||||
{
|
||||
name: 'NAV_TxStatus',
|
||||
to: '/tx-status'
|
||||
},
|
||||
{
|
||||
name: 'Broadcast Transaction',
|
||||
to: '/pushTx'
|
||||
|
|
|
@ -3,7 +3,9 @@ import BN from 'bn.js';
|
|||
import { connect } from 'react-redux';
|
||||
import { AppState } from 'reducers';
|
||||
import { getNetworkConfig, getOffline } from 'selectors/config';
|
||||
import { UnitDisplay } from 'components/ui';
|
||||
import { getIsEstimating } from 'selectors/gas';
|
||||
import { getGasLimit } from 'selectors/transaction';
|
||||
import { UnitDisplay, Spinner } from 'components/ui';
|
||||
import { NetworkConfig } from 'types/network';
|
||||
import './FeeSummary.scss';
|
||||
|
||||
|
@ -20,6 +22,7 @@ interface ReduxStateProps {
|
|||
rates: AppState['rates']['rates'];
|
||||
network: NetworkConfig;
|
||||
isOffline: AppState['config']['meta']['offline'];
|
||||
isGasEstimating: AppState['gas']['isEstimating'];
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
|
@ -31,7 +34,15 @@ type Props = OwnProps & ReduxStateProps;
|
|||
|
||||
class FeeSummary extends React.Component<Props> {
|
||||
public render() {
|
||||
const { gasPrice, gasLimit, rates, network, isOffline } = this.props;
|
||||
const { gasPrice, gasLimit, rates, network, isOffline, isGasEstimating } = this.props;
|
||||
|
||||
if (isGasEstimating) {
|
||||
return (
|
||||
<div className="FeeSummary is-loading">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const feeBig = gasPrice.value && gasLimit.value && gasPrice.value.mul(gasLimit.value);
|
||||
const fee = (
|
||||
|
@ -73,10 +84,11 @@ class FeeSummary extends React.Component<Props> {
|
|||
|
||||
function mapStateToProps(state: AppState): ReduxStateProps {
|
||||
return {
|
||||
gasLimit: state.transaction.fields.gasLimit,
|
||||
gasLimit: getGasLimit(state),
|
||||
rates: state.rates.rates,
|
||||
network: getNetworkConfig(state),
|
||||
isOffline: getOffline(state)
|
||||
isOffline: getOffline(state),
|
||||
isGasEstimating: getIsEstimating(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,33 +11,50 @@ import {
|
|||
nonceRequestPending
|
||||
} from 'selectors/transaction';
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchGasEstimates, TFetchGasEstimates } from 'actions/gas';
|
||||
import { getIsWeb3Node } from 'selectors/config';
|
||||
import { getEstimates, getIsEstimating } from 'selectors/gas';
|
||||
import { Wei, fromWei } from 'libs/units';
|
||||
import { InlineSpinner } from 'components/ui/InlineSpinner';
|
||||
const SliderWithTooltip = Slider.createSliderWithTooltip(Slider);
|
||||
|
||||
interface OwnProps {
|
||||
gasPrice: AppState['transaction']['fields']['gasPrice'];
|
||||
noncePending: boolean;
|
||||
gasLimitPending: boolean;
|
||||
inputGasPrice(rawGas: string);
|
||||
setGasPrice(rawGas: string);
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
gasEstimates: AppState['gas']['estimates'];
|
||||
isGasEstimating: AppState['gas']['isEstimating'];
|
||||
noncePending: boolean;
|
||||
gasLimitPending: boolean;
|
||||
isWeb3Node: boolean;
|
||||
gasLimitEstimationTimedOut: boolean;
|
||||
}
|
||||
|
||||
type Props = OwnProps & StateProps;
|
||||
interface ActionProps {
|
||||
fetchGasEstimates: TFetchGasEstimates;
|
||||
}
|
||||
|
||||
type Props = OwnProps & StateProps & ActionProps;
|
||||
|
||||
class SimpleGas extends React.Component<Props> {
|
||||
public componentDidMount() {
|
||||
this.fixGasPrice(this.props.gasPrice);
|
||||
this.props.fetchGasEstimates();
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
if (!this.props.gasEstimates && nextProps.gasEstimates) {
|
||||
this.props.setGasPrice(nextProps.gasEstimates.fast.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
isGasEstimating,
|
||||
gasEstimates,
|
||||
gasPrice,
|
||||
gasLimitEstimationTimedOut,
|
||||
isWeb3Node,
|
||||
|
@ -45,6 +62,11 @@ class SimpleGas extends React.Component<Props> {
|
|||
gasLimitPending
|
||||
} = this.props;
|
||||
|
||||
const bounds = {
|
||||
max: gasEstimates ? gasEstimates.fastest : gasPriceDefaults.minGwei,
|
||||
min: gasEstimates ? gasEstimates.safeLow : gasPriceDefaults.maxGwei
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="SimpleGas row form-group">
|
||||
<div className="SimpleGas-title">
|
||||
|
@ -69,14 +91,14 @@ class SimpleGas extends React.Component<Props> {
|
|||
<div className="SimpleGas-slider">
|
||||
<SliderWithTooltip
|
||||
onChange={this.handleSlider}
|
||||
min={gasPriceDefaults.gasPriceMinGwei}
|
||||
max={gasPriceDefaults.gasPriceMaxGwei}
|
||||
min={bounds.min}
|
||||
max={bounds.max}
|
||||
value={this.getGasPriceGwei(gasPrice.value)}
|
||||
tipFormatter={gas => `${gas} Gwei`}
|
||||
tipFormatter={this.formatTooltip}
|
||||
disabled={isGasEstimating}
|
||||
/>
|
||||
<div className="SimpleGas-slider-labels">
|
||||
<span>{translate('Cheap')}</span>
|
||||
<span>{translate('Balanced')}</span>
|
||||
<span>{translate('Fast')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -100,21 +122,38 @@ class SimpleGas extends React.Component<Props> {
|
|||
private fixGasPrice(gasPrice: AppState['transaction']['fields']['gasPrice']) {
|
||||
// If the gas price is above or below our minimum, bring it in line
|
||||
const gasPriceGwei = this.getGasPriceGwei(gasPrice.value);
|
||||
if (gasPriceGwei > gasPriceDefaults.gasPriceMaxGwei) {
|
||||
this.props.setGasPrice(gasPriceDefaults.gasPriceMaxGwei.toString());
|
||||
} else if (gasPriceGwei < gasPriceDefaults.gasPriceMinGwei) {
|
||||
this.props.setGasPrice(gasPriceDefaults.gasPriceMinGwei.toString());
|
||||
if (gasPriceGwei > gasPriceDefaults.maxGwei) {
|
||||
this.props.setGasPrice(gasPriceDefaults.maxGwei.toString());
|
||||
} else if (gasPriceGwei < gasPriceDefaults.minGwei) {
|
||||
this.props.setGasPrice(gasPriceDefaults.minGwei.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private getGasPriceGwei(gasPriceValue: Wei) {
|
||||
return parseFloat(fromWei(gasPriceValue, 'gwei'));
|
||||
}
|
||||
|
||||
private formatTooltip = (gas: number) => {
|
||||
const { gasEstimates } = this.props;
|
||||
let recommended = '';
|
||||
if (gasEstimates && !gasEstimates.isDefault && gas === gasEstimates.fast) {
|
||||
recommended = '(Recommended)';
|
||||
}
|
||||
|
||||
return `${gas} Gwei ${recommended}`;
|
||||
};
|
||||
}
|
||||
|
||||
export default connect((state: AppState) => ({
|
||||
noncePending: nonceRequestPending(state),
|
||||
gasLimitPending: getGasEstimationPending(state),
|
||||
gasLimitEstimationTimedOut: getGasLimitEstimationTimedOut(state),
|
||||
isWeb3Node: getIsWeb3Node(state)
|
||||
}))(SimpleGas);
|
||||
export default connect(
|
||||
(state: AppState): StateProps => ({
|
||||
gasEstimates: getEstimates(state),
|
||||
isGasEstimating: getIsEstimating(state),
|
||||
noncePending: nonceRequestPending(state),
|
||||
gasLimitPending: getGasEstimationPending(state),
|
||||
gasLimitEstimationTimedOut: getGasLimitEstimationTimedOut(state),
|
||||
isWeb3Node: getIsWeb3Node(state)
|
||||
}),
|
||||
{
|
||||
fetchGasEstimates
|
||||
}
|
||||
)(SimpleGas);
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
@import 'common/sass/variables';
|
||||
@import 'common/sass/mixins';
|
||||
|
||||
.TxData {
|
||||
&-row {
|
||||
font-size: 0.9rem;
|
||||
|
||||
&-label {
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&-data {
|
||||
@include mono;
|
||||
|
||||
&-more {
|
||||
margin-left: $space-sm;
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&-status {
|
||||
font-weight: bold;
|
||||
|
||||
&.is-success {
|
||||
color: $brand-success;
|
||||
}
|
||||
&.is-warning {
|
||||
color: $brand-warning;
|
||||
}
|
||||
&.is-danger {
|
||||
color: $brand-danger;
|
||||
}
|
||||
}
|
||||
|
||||
.Identicon {
|
||||
float: left;
|
||||
margin-right: $space-sm;
|
||||
}
|
||||
|
||||
textarea {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 7.2rem;
|
||||
background: $gray-lighter;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
import React from 'react';
|
||||
import translate from 'translations';
|
||||
import { Identicon, UnitDisplay, NewTabLink } from 'components/ui';
|
||||
import { TransactionData, TransactionReceipt } from 'libs/nodes';
|
||||
import { NetworkConfig } from 'types/network';
|
||||
import './TransactionDataTable.scss';
|
||||
|
||||
interface TableRow {
|
||||
label: React.ReactElement<string> | string;
|
||||
data: React.ReactElement<string> | string | number | null;
|
||||
}
|
||||
|
||||
const MaybeLink: React.SFC<{
|
||||
href: string | null | undefined | false;
|
||||
children: any; // Too many damn React element types
|
||||
}> = ({ href, children }) => {
|
||||
if (href) {
|
||||
return <NewTabLink href={href}>{children}</NewTabLink>;
|
||||
} else {
|
||||
return <React.Fragment>{children}</React.Fragment>;
|
||||
}
|
||||
};
|
||||
|
||||
interface Props {
|
||||
data: TransactionData;
|
||||
receipt: TransactionReceipt | null;
|
||||
network: NetworkConfig;
|
||||
}
|
||||
|
||||
const TransactionDataTable: React.SFC<Props> = ({ data, receipt, network }) => {
|
||||
const explorer: { [key: string]: string | false | null } = {};
|
||||
const hasInputData = data.input && data.input !== '0x';
|
||||
|
||||
if (!network.isCustom) {
|
||||
explorer.tx = network.blockExplorer && network.blockExplorer.txUrl(data.hash);
|
||||
explorer.block =
|
||||
network.blockExplorer &&
|
||||
!!data.blockNumber &&
|
||||
network.blockExplorer.blockUrl(data.blockNumber);
|
||||
explorer.to = network.blockExplorer && network.blockExplorer.addressUrl(data.to);
|
||||
explorer.from = network.blockExplorer && network.blockExplorer.addressUrl(data.from);
|
||||
explorer.contract =
|
||||
network.blockExplorer &&
|
||||
receipt &&
|
||||
receipt.contractAddress &&
|
||||
network.blockExplorer.addressUrl(receipt.contractAddress);
|
||||
}
|
||||
|
||||
let statusMsg = '';
|
||||
let statusType = '';
|
||||
let statusSeeMore = false;
|
||||
if (receipt) {
|
||||
if (receipt.status === 1) {
|
||||
statusMsg = 'SUCCESSFUL';
|
||||
statusType = 'success';
|
||||
} else if (receipt.status === 0) {
|
||||
statusMsg = 'FAILED';
|
||||
statusType = 'danger';
|
||||
statusSeeMore = true;
|
||||
} else {
|
||||
// Pre-byzantium transactions don't use status, and cannot have their
|
||||
// success determined over the JSON RPC api
|
||||
statusMsg = 'UNKNOWN';
|
||||
statusType = 'warning';
|
||||
statusSeeMore = true;
|
||||
}
|
||||
} else {
|
||||
statusMsg = 'PENDING';
|
||||
statusType = 'warning';
|
||||
}
|
||||
|
||||
const rows: TableRow[] = [
|
||||
{
|
||||
label: 'Status',
|
||||
data: (
|
||||
<React.Fragment>
|
||||
<strong className={`TxData-row-data-status is-${statusType}`}>{statusMsg}</strong>
|
||||
{statusSeeMore &&
|
||||
explorer.tx &&
|
||||
!network.isCustom && (
|
||||
<NewTabLink className="TxData-row-data-more" href={explorer.tx as string}>
|
||||
(See more on {network.blockExplorer.name})
|
||||
</NewTabLink>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
},
|
||||
{
|
||||
label: translate('x_TxHash'),
|
||||
data: <MaybeLink href={explorer.tx}>{data.hash}</MaybeLink>
|
||||
},
|
||||
{
|
||||
label: 'Block Number',
|
||||
data: receipt && <MaybeLink href={explorer.block}>{receipt.blockNumber}</MaybeLink>
|
||||
},
|
||||
{
|
||||
label: translate('OFFLINE_Step1_Label_1'),
|
||||
data: (
|
||||
<MaybeLink href={explorer.from}>
|
||||
<Identicon address={data.from} size="26px" />
|
||||
{data.from}
|
||||
</MaybeLink>
|
||||
)
|
||||
},
|
||||
{
|
||||
label: translate('OFFLINE_Step2_Label_1'),
|
||||
data: (
|
||||
<MaybeLink href={explorer.to}>
|
||||
<Identicon address={data.to} size="26px" />
|
||||
{data.to}
|
||||
</MaybeLink>
|
||||
)
|
||||
},
|
||||
{
|
||||
label: translate('SEND_amount_short'),
|
||||
data: <UnitDisplay value={data.value} unit="ether" symbol={network.unit} />
|
||||
},
|
||||
{
|
||||
label: translate('OFFLINE_Step2_Label_3'),
|
||||
data: <UnitDisplay value={data.gasPrice} unit="gwei" symbol="Gwei" />
|
||||
},
|
||||
{
|
||||
label: translate('OFFLINE_Step2_Label_4'),
|
||||
data: <UnitDisplay value={data.gas} unit="wei" />
|
||||
},
|
||||
{
|
||||
label: 'Gas Used',
|
||||
data: receipt && <UnitDisplay value={receipt.gasUsed} unit="wei" />
|
||||
},
|
||||
{
|
||||
label: 'Transaction Fee',
|
||||
data: receipt && (
|
||||
<UnitDisplay
|
||||
value={receipt.gasUsed.mul(data.gasPrice)}
|
||||
unit="ether"
|
||||
symbol={network.unit}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
label: translate('New contract address'),
|
||||
data: receipt &&
|
||||
receipt.contractAddress && (
|
||||
<MaybeLink href={explorer.contract}>{receipt.contractAddress}</MaybeLink>
|
||||
)
|
||||
},
|
||||
{
|
||||
label: translate('OFFLINE_Step2_Label_5'),
|
||||
data: data.nonce
|
||||
},
|
||||
{
|
||||
label: translate('TRANS_data'),
|
||||
data: hasInputData ? (
|
||||
<textarea className="form-control" value={data.input} disabled={true} />
|
||||
) : null
|
||||
}
|
||||
];
|
||||
|
||||
const filteredRows = rows.filter(row => !!row.data);
|
||||
return (
|
||||
<table className="TxData table table-striped">
|
||||
<tbody>
|
||||
{filteredRows.map((row, idx) => (
|
||||
<tr className="TxData-row" key={idx}>
|
||||
<td className="TxData-row-label">{row.label}</td>
|
||||
<td className="TxData-row-data">{row.data}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransactionDataTable;
|
|
@ -0,0 +1,35 @@
|
|||
@import 'common/sass/variables';
|
||||
|
||||
.TxStatus {
|
||||
text-align: center;
|
||||
|
||||
&-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&-data {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&-loading {
|
||||
padding: $space * 3 0;
|
||||
}
|
||||
|
||||
&-error {
|
||||
max-width: 620px;
|
||||
margin: 0 auto;
|
||||
|
||||
&-title {
|
||||
color: $brand-danger;
|
||||
}
|
||||
|
||||
&-desc {
|
||||
font-size: $font-size-bump;
|
||||
margin-bottom: $space-md;
|
||||
}
|
||||
|
||||
&-list {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import translate from 'translations';
|
||||
import { fetchTransactionData, TFetchTransactionData } from 'actions/transactions';
|
||||
import { getTransactionDatas } from 'selectors/transactions';
|
||||
import { getNetworkConfig } from 'selectors/config';
|
||||
import { Spinner } from 'components/ui';
|
||||
import TransactionDataTable from './TransactionDataTable';
|
||||
import { AppState } from 'reducers';
|
||||
import { NetworkConfig } from 'types/network';
|
||||
import { TransactionState } from 'reducers/transactions';
|
||||
import './TransactionStatus.scss';
|
||||
|
||||
interface OwnProps {
|
||||
txHash: string;
|
||||
}
|
||||
|
||||
interface StateProps {
|
||||
tx: TransactionState | null;
|
||||
network: NetworkConfig;
|
||||
}
|
||||
|
||||
interface ActionProps {
|
||||
fetchTransactionData: TFetchTransactionData;
|
||||
}
|
||||
|
||||
type Props = OwnProps & StateProps & ActionProps;
|
||||
|
||||
class TransactionStatus extends React.Component<Props> {
|
||||
public componentDidMount() {
|
||||
this.props.fetchTransactionData(this.props.txHash);
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
if (this.props.txHash !== nextProps.txHash) {
|
||||
this.props.fetchTransactionData(nextProps.txHash);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { tx, network } = this.props;
|
||||
let content;
|
||||
|
||||
if (tx && tx.data) {
|
||||
content = (
|
||||
<React.Fragment>
|
||||
<h2 className="TxStatus-title">Transaction Found</h2>
|
||||
<div className="TxStatus-data">
|
||||
<TransactionDataTable network={network} data={tx.data} receipt={tx.receipt} />
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
} else if (tx && tx.error) {
|
||||
content = (
|
||||
<div className="TxStatus-error">
|
||||
<h2 className="TxStatus-error-title">{translate('tx_notFound')}</h2>
|
||||
<p className="TxStatus-error-desc">{translate('tx_notFound_1')}</p>
|
||||
<ul className="TxStatus-error-list">
|
||||
<li>Make sure you copied the Transaction Hash correctly</li>
|
||||
<li>{translate('tx_notFound_2')}</li>
|
||||
<li>{translate('tx_notFound_3')}</li>
|
||||
<li>{translate('tx_notFound_4')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
} else if (tx && tx.isLoading) {
|
||||
// tx.isLoading... probably.
|
||||
content = (
|
||||
<div className="TxStatus-loading">
|
||||
<Spinner size="x3" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="TxStatus">{content}</div>;
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state: AppState, ownProps: OwnProps): StateProps {
|
||||
const { txHash } = ownProps;
|
||||
|
||||
return {
|
||||
tx: getTransactionDatas(state)[txHash],
|
||||
network: getNetworkConfig(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, { fetchTransactionData })(TransactionStatus);
|
|
@ -0,0 +1,2 @@
|
|||
import TransactionStatus from './TransactionStatus';
|
||||
export default TransactionStatus;
|
|
@ -114,7 +114,7 @@ export class InsecureWalletWarning extends React.Component<Props, State> {
|
|||
|
||||
private makeCheckbox = (checkbox: Checkbox) => {
|
||||
return (
|
||||
<label className="AcknowledgeCheckbox">
|
||||
<label className="AcknowledgeCheckbox" key={checkbox.name}>
|
||||
<input
|
||||
type="checkbox"
|
||||
name={checkbox.name}
|
||||
|
|
|
@ -18,3 +18,4 @@ export { default as TXMetaDataPanel } from './TXMetaDataPanel';
|
|||
export { default as WalletDecrypt } from './WalletDecrypt';
|
||||
export { default as TogglablePassword } from './TogglablePassword';
|
||||
export { default as GenerateKeystoreModal } from './GenerateKeystoreModal';
|
||||
export { default as TransactionStatus } from './TransactionStatus';
|
||||
|
|
|
@ -108,6 +108,11 @@ $m-anim-speed: 400ms;
|
|||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
// Mobile styles
|
||||
@media(max-width: $screen-sm) {
|
||||
width: calc(100% - 40px);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-modal {
|
||||
|
|
|
@ -31,8 +31,8 @@ export interface AAttributes {
|
|||
|
||||
interface NewTabLinkProps extends AAttributes {
|
||||
href: string;
|
||||
content?: React.ReactElement<any> | string | string[];
|
||||
children?: React.ReactElement<any> | string | string[];
|
||||
content?: React.ReactElement<any> | string | string[] | number;
|
||||
children?: React.ReactElement<any> | string | string[] | number;
|
||||
}
|
||||
|
||||
export class NewTabLink extends React.Component<NewTabLinkProps> {
|
||||
|
|
|
@ -5,3 +5,4 @@ export const GAS_LIMIT_UPPER_BOUND = 8000000;
|
|||
// Lower/upper ranges for gas price in gwei
|
||||
export const GAS_PRICE_GWEI_LOWER_BOUND = 1;
|
||||
export const GAS_PRICE_GWEI_UPPER_BOUND = 10000;
|
||||
export const GAS_PRICE_GWEI_DEFAULT = 40;
|
||||
|
|
|
@ -4,7 +4,7 @@ import { getValues } from '../utils/helpers';
|
|||
export const languages = require('./languages.json');
|
||||
|
||||
// Displays in the footer
|
||||
export const VERSION = '0.2.1 (BETA)';
|
||||
export const VERSION = '0.3.0 (BETA)';
|
||||
export const N_FACTOR = 8192;
|
||||
|
||||
// Displays at the top of the site, make message empty string to remove.
|
||||
|
@ -42,9 +42,11 @@ export const donationAddressMap = {
|
|||
};
|
||||
|
||||
export const gasPriceDefaults = {
|
||||
gasPriceMinGwei: 1,
|
||||
gasPriceMaxGwei: 60
|
||||
minGwei: 1,
|
||||
maxGwei: 60,
|
||||
default: 21
|
||||
};
|
||||
export const gasEstimateCacheTime = 60000;
|
||||
|
||||
export const MINIMUM_PASSWORD_LENGTH = 12;
|
||||
|
||||
|
|
|
@ -3,10 +3,10 @@ export enum HELP_ARTICLE {
|
|||
ENS = 'ens',
|
||||
ENS_BAD_REVEAL = 'ens/ens-debugging-a-bad-instruction-reveal',
|
||||
DIFFERENCE_BETWEEN_PKEY_AND_KEYSTORE = 'private-keys-passwords/difference-beween-private-key-and-keystore-file.html',
|
||||
RUNNING_LOCALLY = 'offline/running-myetherwallet-locally.html',
|
||||
RUNNING_LOCALLY = 'offline/running-mycrypto-locally.html',
|
||||
MIGRATE_TO_METAMASK = 'migration/moving-from-private-key-to-metamask.html',
|
||||
MIGRATE_TO_LEDGER = 'migration/moving-from-private-key-to-ledger-hardware-wallet.html',
|
||||
ADDING_NEW_TOKENS = 'send/adding-new-token-and-sending-custom-tokens.html',
|
||||
ADDING_NEW_TOKENS = 'tokens/adding-new-token-and-sending-custom-tokens.html',
|
||||
HARDWARE_WALLET_RECOMMENDATIONS = 'hardware-wallets/hardware-wallet-recommendations.html',
|
||||
SENDING_TO_TREZOR = 'hardware-wallets/trezor-sending-to-trezor-device.html',
|
||||
SECURING_YOUR_ETH = 'security/securing-your-ethereum.html',
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.TxHashInput {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import React from 'react';
|
||||
import translate from 'translations';
|
||||
import { isValidTxHash, isValidETHAddress } from 'libs/validators';
|
||||
import './TxHashInput.scss';
|
||||
|
||||
interface Props {
|
||||
hash?: string;
|
||||
onSubmit(hash: string): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export default class TxHashInput extends React.Component<Props, State> {
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hash: props.hash || '' };
|
||||
}
|
||||
|
||||
public componentWillReceiveProps(nextProps: Props) {
|
||||
if (this.props.hash !== nextProps.hash && nextProps.hash) {
|
||||
this.setState({ hash: nextProps.hash });
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { hash } = this.state;
|
||||
const validClass = hash ? (isValidTxHash(hash) ? 'is-valid' : 'is-invalid') : '';
|
||||
|
||||
return (
|
||||
<form className="TxHashInput" onSubmit={this.handleSubmit}>
|
||||
<input
|
||||
value={hash}
|
||||
placeholder="0x16e521..."
|
||||
className={`TxHashInput-field form-control ${validClass}`}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
|
||||
{isValidETHAddress(hash) && (
|
||||
<p className="TxHashInput-message help-block is-invalid">
|
||||
You cannot use an address, you must use a transaction hash
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button className="TxHashInput-submit btn btn-primary btn-block">
|
||||
{translate('NAV_CheckTxStatus')}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
private handleChange = (ev: React.FormEvent<HTMLInputElement>) => {
|
||||
this.setState({ hash: ev.currentTarget.value });
|
||||
};
|
||||
|
||||
private handleSubmit = (ev: React.FormEvent<HTMLFormElement>) => {
|
||||
ev.preventDefault();
|
||||
if (isValidTxHash(this.state.hash)) {
|
||||
this.props.onSubmit(this.state.hash);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
@import 'common/sass/variables';
|
||||
|
||||
.CheckTransaction {
|
||||
&-form {
|
||||
text-align: center;
|
||||
|
||||
&-desc {
|
||||
max-width: 580px;
|
||||
margin: 0 auto $space;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import TabSection from 'containers/TabSection';
|
||||
import TxHashInput from './components/TxHashInput';
|
||||
import { TransactionStatus as TransactionStatusComponent } from 'components';
|
||||
import { NewTabLink } from 'components/ui';
|
||||
import { getNetworkConfig } from 'selectors/config';
|
||||
import { AppState } from 'reducers';
|
||||
import { NetworkConfig } from 'types/network';
|
||||
import './index.scss';
|
||||
|
||||
interface Props {
|
||||
network: NetworkConfig;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hash: string;
|
||||
}
|
||||
|
||||
class CheckTransaction extends React.Component<Props, State> {
|
||||
public state: State = {
|
||||
hash: ''
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { network } = this.props;
|
||||
const { hash } = this.state;
|
||||
|
||||
return (
|
||||
<TabSection>
|
||||
<div className="CheckTransaction Tab-content">
|
||||
<section className="CheckTransaction-form Tab-content-pane">
|
||||
<h1 className="CheckTransaction-form-title">Check Transaction Status</h1>
|
||||
<p className="CheckTransaction-form-desc">
|
||||
Enter your Transaction Hash to check on its status.{' '}
|
||||
{!network.isCustom && (
|
||||
<React.Fragment>
|
||||
If you don’t know your Transaction Hash, you can look it up on the{' '}
|
||||
<NewTabLink href={network.blockExplorer.origin}>
|
||||
{network.blockExplorer.name} explorer
|
||||
</NewTabLink>{' '}
|
||||
by looking up your address.
|
||||
</React.Fragment>
|
||||
)}
|
||||
</p>
|
||||
<TxHashInput onSubmit={this.handleHashSubmit} />
|
||||
</section>
|
||||
|
||||
{hash && (
|
||||
<section className="CheckTransaction-tx Tab-content-pane">
|
||||
<TransactionStatusComponent txHash={hash} />
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</TabSection>
|
||||
);
|
||||
}
|
||||
|
||||
private handleHashSubmit = (hash: string) => {
|
||||
// Reset to re-mount the component
|
||||
this.setState({ hash: '' }, () => {
|
||||
this.setState({ hash });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default connect((state: AppState) => ({
|
||||
network: getNetworkConfig(state)
|
||||
}))(CheckTransaction);
|
|
@ -34,11 +34,11 @@
|
|||
<p class="is-desktop">
|
||||
MyCrypto requires certain features that your browser doesn't offer. Your browser may also be missing security updates
|
||||
that could open you up to vulnerabilities. Please update your browser, or switch to one of the following browsers
|
||||
ones to continue using MyCrypto.
|
||||
to continue using MyCrypto.
|
||||
</p>
|
||||
<p class="is-mobile">
|
||||
MyCrypto requires certain features that your browser doesn't offer. Please use your device's default browser, or switch
|
||||
to a laptop or computer to continue using MyCrypto.
|
||||
to a laptop or desktop computer to continue using MyCrypto.
|
||||
</p>
|
||||
<div class="BadBrowser-content-browsers is-desktop">
|
||||
<a class="BadBrowser-content-browsers-browser firefox" href="https://www.mozilla.org/en-US/firefox/new/" rel="noopener noreferrer" target="_blank">
|
||||
|
|
|
@ -6,10 +6,39 @@ export interface TxObj {
|
|||
to: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
interface TokenBalanceResult {
|
||||
balance: TokenValue;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface TransactionData {
|
||||
hash: string;
|
||||
nonce: number;
|
||||
blockHash: string | null;
|
||||
blockNumber: number | null;
|
||||
transactionIndex: number | null;
|
||||
from: string;
|
||||
to: string;
|
||||
value: Wei;
|
||||
gasPrice: Wei;
|
||||
gas: Wei;
|
||||
input: string;
|
||||
}
|
||||
|
||||
export interface TransactionReceipt {
|
||||
transactionHash: string;
|
||||
transactionIndex: number;
|
||||
blockHash: string;
|
||||
blockNumber: number;
|
||||
cumulativeGasUsed: Wei;
|
||||
gasUsed: Wei;
|
||||
contractAddress: string | null;
|
||||
logs: string[];
|
||||
logsBloom: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
export interface INode {
|
||||
ping(): Promise<boolean>;
|
||||
getBalance(address: string): Promise<Wei>;
|
||||
|
@ -17,6 +46,8 @@ export interface INode {
|
|||
getTokenBalances(address: string, tokens: Token[]): Promise<TokenBalanceResult[]>;
|
||||
estimateGas(tx: Partial<IHexStrTransaction>): Promise<Wei>;
|
||||
getTransactionCount(address: string): Promise<string>;
|
||||
getTransactionByHash(txhash: string): Promise<TransactionData>;
|
||||
getTransactionReceipt(txhash: string): Promise<TransactionReceipt>;
|
||||
sendRawTx(tx: string): Promise<string>;
|
||||
sendCallRequest(txObj: TxObj): Promise<string>;
|
||||
getCurrentBlock(): Promise<string>;
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
GetBalanceRequest,
|
||||
GetTokenBalanceRequest,
|
||||
GetTransactionCountRequest,
|
||||
GetTransactionByHashRequest,
|
||||
SendRawTxRequest,
|
||||
GetCurrentBlockRequest
|
||||
} from './types';
|
||||
|
@ -58,6 +59,14 @@ export default class EtherscanRequests extends RPCRequests {
|
|||
};
|
||||
}
|
||||
|
||||
public getTransactionByHash(txhash: string): GetTransactionByHashRequest {
|
||||
return {
|
||||
module: 'proxy',
|
||||
action: 'eth_getTransactionByHash',
|
||||
txhash
|
||||
};
|
||||
}
|
||||
|
||||
public getTokenBalance(address: string, token: Token): GetTokenBalanceRequest {
|
||||
return this.ethCall({
|
||||
to: token.address,
|
||||
|
|
|
@ -41,6 +41,12 @@ export interface GetTransactionCountRequest extends EtherscanReqBase {
|
|||
tag: 'latest';
|
||||
}
|
||||
|
||||
export interface GetTransactionByHashRequest extends EtherscanReqBase {
|
||||
module: 'proxy';
|
||||
action: 'eth_getTransactionByHash';
|
||||
txhash: string;
|
||||
}
|
||||
|
||||
export interface GetCurrentBlockRequest extends EtherscanReqBase {
|
||||
module: 'proxy';
|
||||
action: 'eth_blockNumber';
|
||||
|
@ -53,4 +59,5 @@ export type EtherscanRequest =
|
|||
| GetTokenBalanceRequest
|
||||
| EstimateGasRequest
|
||||
| GetTransactionCountRequest
|
||||
| GetTransactionByHashRequest
|
||||
| GetCurrentBlockRequest;
|
||||
|
|
|
@ -3,3 +3,4 @@ export { default as InfuraNode } from './infura';
|
|||
export { default as EtherscanNode } from './etherscan';
|
||||
export { default as CustomNode } from './custom';
|
||||
export { default as Web3Node } from './web3';
|
||||
export * from './INode';
|
||||
|
|
|
@ -2,7 +2,8 @@ import BN from 'bn.js';
|
|||
import { IHexStrTransaction } from 'libs/transaction';
|
||||
import { Wei, TokenValue } from 'libs/units';
|
||||
import { stripHexPrefix } from 'libs/values';
|
||||
import { INode, TxObj } from '../INode';
|
||||
import { hexToNumber } from 'utils/formatters';
|
||||
import { INode, TxObj, TransactionData, TransactionReceipt } from '../INode';
|
||||
import RPCClient from './client';
|
||||
import RPCRequests from './requests';
|
||||
import {
|
||||
|
@ -11,9 +12,11 @@ import {
|
|||
isValidCallRequest,
|
||||
isValidTokenBalance,
|
||||
isValidTransactionCount,
|
||||
isValidTransactionByHash,
|
||||
isValidTransactionReceipt,
|
||||
isValidCurrentBlock,
|
||||
isValidRawTxApi
|
||||
} from '../../validators';
|
||||
} from 'libs/validators';
|
||||
import { Token } from 'types/network';
|
||||
|
||||
export default class RpcNode implements INode {
|
||||
|
@ -46,8 +49,6 @@ export default class RpcNode implements INode {
|
|||
}
|
||||
|
||||
public estimateGas(transaction: Partial<IHexStrTransaction>): Promise<Wei> {
|
||||
// Timeout after 10 seconds
|
||||
|
||||
return this.client
|
||||
.call(this.requests.estimateGas(transaction))
|
||||
.then(isValidEstimateGas)
|
||||
|
@ -106,6 +107,37 @@ export default class RpcNode implements INode {
|
|||
.then(({ result }) => result);
|
||||
}
|
||||
|
||||
public getTransactionByHash(txhash: string): Promise<TransactionData> {
|
||||
return this.client
|
||||
.call(this.requests.getTransactionByHash(txhash))
|
||||
.then(isValidTransactionByHash)
|
||||
.then(({ result }) => ({
|
||||
...result,
|
||||
to: result.to || '0x0',
|
||||
value: Wei(result.value),
|
||||
gasPrice: Wei(result.gasPrice),
|
||||
gas: Wei(result.gas),
|
||||
nonce: hexToNumber(result.nonce),
|
||||
blockNumber: result.blockNumber ? hexToNumber(result.blockNumber) : null,
|
||||
transactionIndex: result.transactionIndex ? hexToNumber(result.transactionIndex) : null
|
||||
}));
|
||||
}
|
||||
|
||||
public getTransactionReceipt(txhash: string): Promise<TransactionReceipt> {
|
||||
return this.client
|
||||
.call(this.requests.getTransactionReceipt(txhash))
|
||||
.then(isValidTransactionReceipt)
|
||||
.then(({ result }) => ({
|
||||
...result,
|
||||
transactionIndex: hexToNumber(result.transactionIndex),
|
||||
blockNumber: hexToNumber(result.blockNumber),
|
||||
cumulativeGasUsed: Wei(result.cumulativeGasUsed),
|
||||
gasUsed: Wei(result.gasUsed),
|
||||
status: result.status ? hexToNumber(result.status) : null,
|
||||
root: result.root || null
|
||||
}));
|
||||
}
|
||||
|
||||
public getCurrentBlock(): Promise<string> {
|
||||
return this.client
|
||||
.call(this.requests.getCurrentBlock())
|
||||
|
|
|
@ -6,7 +6,9 @@ import {
|
|||
GetTokenBalanceRequest,
|
||||
GetTransactionCountRequest,
|
||||
SendRawTxRequest,
|
||||
GetCurrentBlockRequest
|
||||
GetCurrentBlockRequest,
|
||||
GetTransactionByHashRequest,
|
||||
GetTransactionReceiptRequest
|
||||
} from './types';
|
||||
import { hexEncodeData } from './utils';
|
||||
import { TxObj } from '../INode';
|
||||
|
@ -17,6 +19,8 @@ export default class RPCRequests {
|
|||
return { method: 'net_version' };
|
||||
}
|
||||
|
||||
/* TODO: Fix `| any` on all of these */
|
||||
|
||||
public sendRawTx(signedTx: string): SendRawTxRequest | any {
|
||||
return {
|
||||
method: 'eth_sendRawTransaction',
|
||||
|
@ -52,6 +56,20 @@ export default class RPCRequests {
|
|||
};
|
||||
}
|
||||
|
||||
public getTransactionByHash(txhash: string): GetTransactionByHashRequest | any {
|
||||
return {
|
||||
method: 'eth_getTransactionByHash',
|
||||
params: [txhash]
|
||||
};
|
||||
}
|
||||
|
||||
public getTransactionReceipt(txhash: string): GetTransactionReceiptRequest | any {
|
||||
return {
|
||||
method: 'eth_getTransactionReceipt',
|
||||
params: [txhash]
|
||||
};
|
||||
}
|
||||
|
||||
public getTokenBalance(address: string, token: Token): GetTokenBalanceRequest | any {
|
||||
return {
|
||||
method: 'eth_call',
|
||||
|
|
|
@ -69,6 +69,16 @@ export interface GetTransactionCountRequest extends RPCRequestBase {
|
|||
params: [DATA, DEFAULT_BLOCK];
|
||||
}
|
||||
|
||||
export interface GetTransactionByHashRequest extends RPCRequestBase {
|
||||
method: 'eth_getTransactionByHash';
|
||||
params: [string];
|
||||
}
|
||||
|
||||
export interface GetTransactionReceiptRequest extends RPCRequestBase {
|
||||
method: 'eth_getTransactionReceipt';
|
||||
params: [string];
|
||||
}
|
||||
|
||||
export interface GetCurrentBlockRequest extends RPCRequestBase {
|
||||
method: 'eth_blockNumber';
|
||||
}
|
||||
|
@ -80,4 +90,6 @@ export type RPCRequest =
|
|||
| CallRequest
|
||||
| EstimateGasRequest
|
||||
| GetTransactionCountRequest
|
||||
| GetTransactionByHashRequest
|
||||
| GetTransactionReceiptRequest
|
||||
| GetCurrentBlockRequest;
|
||||
|
|
|
@ -167,7 +167,9 @@ export const schema = {
|
|||
properties: {
|
||||
jsonrpc: { type: 'string' },
|
||||
id: { oneOf: [{ type: 'string' }, { type: 'integer' }] },
|
||||
result: { oneOf: [{ type: 'string' }, { type: 'array' }] },
|
||||
result: {
|
||||
oneOf: [{ type: 'string' }, { type: 'array' }, { type: 'object' }]
|
||||
},
|
||||
status: { type: 'string' },
|
||||
message: { type: 'string', maxLength: 2 }
|
||||
}
|
||||
|
@ -236,6 +238,12 @@ export const isValidTokenBalance = (response: JsonRpcResponse) =>
|
|||
export const isValidTransactionCount = (response: JsonRpcResponse) =>
|
||||
isValidEthCall(response, schema.RpcNode)('Transaction Count');
|
||||
|
||||
export const isValidTransactionByHash = (response: JsonRpcResponse) =>
|
||||
isValidEthCall(response, schema.RpcNode)('Transaction By Hash');
|
||||
|
||||
export const isValidTransactionReceipt = (response: JsonRpcResponse) =>
|
||||
isValidEthCall(response, schema.RpcNode)('Transaction Receipt');
|
||||
|
||||
export const isValidCurrentBlock = (response: JsonRpcResponse) =>
|
||||
isValidEthCall(response, schema.RpcNode)('Current Block');
|
||||
|
||||
|
@ -253,3 +261,6 @@ export const isValidGetAccounts = (response: JsonRpcResponse) =>
|
|||
|
||||
export const isValidGetNetVersion = (response: JsonRpcResponse) =>
|
||||
isValidEthCall(response, schema.RpcNode)('Net Version');
|
||||
|
||||
export const isValidTxHash = (hash: string) =>
|
||||
hash.substring(0, 2) === '0x' && hash.length === 66 && isValidHex(hash);
|
||||
|
|
|
@ -17,11 +17,13 @@ export type State = { [key in StaticNetworkIds]: StaticNetworkConfig };
|
|||
// Must be a website that follows the ethplorer convention of /tx/[hash] and
|
||||
// address/[address] to generate the correct functions.
|
||||
// TODO: put this in utils / libs
|
||||
export function makeExplorer(origin: string): BlockExplorerConfig {
|
||||
export function makeExplorer(name: string, origin: string): BlockExplorerConfig {
|
||||
return {
|
||||
name,
|
||||
origin,
|
||||
txUrl: hash => `${origin}/tx/${hash}`,
|
||||
addressUrl: address => `${origin}/address/${address}`
|
||||
addressUrl: address => `${origin}/address/${address}`,
|
||||
blockUrl: blockNum => `${origin}/block/${blockNum}`
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -32,7 +34,7 @@ const INITIAL_STATE: State = {
|
|||
chainId: 1,
|
||||
isCustom: false,
|
||||
color: '#0e97c0',
|
||||
blockExplorer: makeExplorer('https://etherscan.io'),
|
||||
blockExplorer: makeExplorer('Etherscan', 'https://etherscan.io'),
|
||||
tokenExplorer: {
|
||||
name: ethPlorer,
|
||||
address: ETHTokenExplorer
|
||||
|
@ -51,7 +53,7 @@ const INITIAL_STATE: State = {
|
|||
chainId: 3,
|
||||
isCustom: false,
|
||||
color: '#adc101',
|
||||
blockExplorer: makeExplorer('https://ropsten.etherscan.io'),
|
||||
blockExplorer: makeExplorer('Etherscan', 'https://ropsten.etherscan.io'),
|
||||
tokens: require('config/tokens/ropsten.json'),
|
||||
contracts: require('config/contracts/ropsten.json'),
|
||||
isTestnet: true,
|
||||
|
@ -67,7 +69,7 @@ const INITIAL_STATE: State = {
|
|||
chainId: 42,
|
||||
isCustom: false,
|
||||
color: '#adc101',
|
||||
blockExplorer: makeExplorer('https://kovan.etherscan.io'),
|
||||
blockExplorer: makeExplorer('Etherscan', 'https://kovan.etherscan.io'),
|
||||
tokens: require('config/tokens/ropsten.json'),
|
||||
contracts: require('config/contracts/ropsten.json'),
|
||||
isTestnet: true,
|
||||
|
@ -83,7 +85,7 @@ const INITIAL_STATE: State = {
|
|||
chainId: 4,
|
||||
isCustom: false,
|
||||
color: '#adc101',
|
||||
blockExplorer: makeExplorer('https://rinkeby.etherscan.io'),
|
||||
blockExplorer: makeExplorer('Etherscan', 'https://rinkeby.etherscan.io'),
|
||||
tokens: require('config/tokens/rinkeby.json'),
|
||||
contracts: require('config/contracts/rinkeby.json'),
|
||||
isTestnet: true,
|
||||
|
@ -99,7 +101,7 @@ const INITIAL_STATE: State = {
|
|||
chainId: 61,
|
||||
isCustom: false,
|
||||
color: '#669073',
|
||||
blockExplorer: makeExplorer('https://gastracker.io'),
|
||||
blockExplorer: makeExplorer('GasTracker', 'https://gastracker.io'),
|
||||
tokens: require('config/tokens/etc.json'),
|
||||
contracts: require('config/contracts/etc.json'),
|
||||
dPathFormats: {
|
||||
|
@ -114,7 +116,7 @@ const INITIAL_STATE: State = {
|
|||
chainId: 8,
|
||||
isCustom: false,
|
||||
color: '#b37aff',
|
||||
blockExplorer: makeExplorer('https://ubiqscan.io/en'),
|
||||
blockExplorer: makeExplorer('Ubiqscan', 'https://ubiqscan.io/en'),
|
||||
tokens: require('config/tokens/ubq.json'),
|
||||
contracts: require('config/contracts/ubq.json'),
|
||||
dPathFormats: {
|
||||
|
@ -129,7 +131,7 @@ const INITIAL_STATE: State = {
|
|||
chainId: 2,
|
||||
isCustom: false,
|
||||
color: '#673ab7',
|
||||
blockExplorer: makeExplorer('https://www.gander.tech'),
|
||||
blockExplorer: makeExplorer('Gander', 'https://www.gander.tech'),
|
||||
tokens: require('config/tokens/exp.json'),
|
||||
contracts: require('config/contracts/exp.json'),
|
||||
dPathFormats: {
|
||||
|
|
|
@ -23,14 +23,14 @@ export const INITIAL_STATE: State = {
|
|||
network: 'ETH',
|
||||
isCustom: false,
|
||||
service: 'infura.io',
|
||||
lib: new InfuraNode('https://mainnet.infura.io/mew'),
|
||||
lib: new InfuraNode('https://mainnet.infura.io/mycrypto'),
|
||||
estimateGas: false
|
||||
},
|
||||
rop_infura: {
|
||||
network: 'Ropsten',
|
||||
isCustom: false,
|
||||
service: 'infura.io',
|
||||
lib: new InfuraNode('https://ropsten.infura.io/mew'),
|
||||
lib: new InfuraNode('https://ropsten.infura.io/mycrypto'),
|
||||
estimateGas: false
|
||||
},
|
||||
kov_ethscan: {
|
||||
|
@ -51,7 +51,7 @@ export const INITIAL_STATE: State = {
|
|||
network: 'Rinkeby',
|
||||
isCustom: false,
|
||||
service: 'infura.io',
|
||||
lib: new InfuraNode('https://rinkeby.infura.io/mew'),
|
||||
lib: new InfuraNode('https://rinkeby.infura.io/mycrypto'),
|
||||
estimateGas: false
|
||||
},
|
||||
etc_epool: {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { SetGasEstimatesAction, GasAction, TypeKeys } from 'actions/gas';
|
||||
import { GasEstimates } from 'api/gas';
|
||||
|
||||
export interface State {
|
||||
estimates: GasEstimates | null;
|
||||
isEstimating: boolean;
|
||||
}
|
||||
|
||||
export const INITIAL_STATE: State = {
|
||||
estimates: null,
|
||||
isEstimating: false
|
||||
};
|
||||
|
||||
function fetchGasEstimates(state: State): State {
|
||||
return {
|
||||
...state,
|
||||
isEstimating: true
|
||||
};
|
||||
}
|
||||
|
||||
function setGasEstimates(state: State, action: SetGasEstimatesAction): State {
|
||||
return {
|
||||
...state,
|
||||
estimates: action.payload,
|
||||
isEstimating: false
|
||||
};
|
||||
}
|
||||
|
||||
export function gas(state: State = INITIAL_STATE, action: GasAction): State {
|
||||
switch (action.type) {
|
||||
case TypeKeys.GAS_FETCH_ESTIMATES:
|
||||
return fetchGasEstimates(state);
|
||||
case TypeKeys.GAS_SET_ESTIMATES:
|
||||
return setGasEstimates(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -9,7 +9,9 @@ import { rates, State as RatesState } from './rates';
|
|||
import { State as SwapState, swap } from './swap';
|
||||
import { State as WalletState, wallet } from './wallet';
|
||||
import { State as TransactionState, transaction } from './transaction';
|
||||
import { State as GasState, gas } from './gas';
|
||||
import { onboardStatus, State as OnboardStatusState } from './onboardStatus';
|
||||
import { State as TransactionsState, transactions } from './transactions';
|
||||
|
||||
export interface AppState {
|
||||
// Custom reducers
|
||||
|
@ -21,11 +23,12 @@ export interface AppState {
|
|||
customTokens: CustomTokensState;
|
||||
rates: RatesState;
|
||||
deterministicWallets: DeterministicWalletsState;
|
||||
// Third party reducers (TODO: Fill these out)
|
||||
form: any;
|
||||
routing: any;
|
||||
swap: SwapState;
|
||||
transaction: TransactionState;
|
||||
transactions: TransactionsState;
|
||||
gas: GasState;
|
||||
// Third party reducers (TODO: Fill these out)
|
||||
routing: any;
|
||||
}
|
||||
|
||||
export default combineReducers<AppState>({
|
||||
|
@ -38,6 +41,8 @@ export default combineReducers<AppState>({
|
|||
customTokens,
|
||||
rates,
|
||||
deterministicWallets,
|
||||
routing: routerReducer,
|
||||
transaction
|
||||
transaction,
|
||||
transactions,
|
||||
gas,
|
||||
routing: routerReducer
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { Reducer } from 'redux';
|
||||
import { State } from './typings';
|
||||
import { gasPricetoBase } from 'libs/units';
|
||||
import { gasPriceDefaults } from 'config';
|
||||
|
||||
const INITIAL_STATE: State = {
|
||||
to: { raw: '', value: null },
|
||||
|
@ -18,7 +19,10 @@ const INITIAL_STATE: State = {
|
|||
nonce: { raw: '', value: null },
|
||||
value: { raw: '', value: null },
|
||||
gasLimit: { raw: '21000', value: new BN(21000) },
|
||||
gasPrice: { raw: '21', value: gasPricetoBase(21) }
|
||||
gasPrice: {
|
||||
raw: gasPriceDefaults.default.toString(),
|
||||
value: gasPricetoBase(gasPriceDefaults.default)
|
||||
}
|
||||
};
|
||||
|
||||
const updateField = (key: keyof State): Reducer<State> => (state: State, action: FieldAction) => ({
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import {
|
||||
FetchTransactionDataAction,
|
||||
SetTransactionDataAction,
|
||||
TransactionsAction,
|
||||
TypeKeys
|
||||
} from 'actions/transactions';
|
||||
import { TransactionData, TransactionReceipt } from 'libs/nodes';
|
||||
|
||||
export interface TransactionState {
|
||||
data: TransactionData | null;
|
||||
receipt: TransactionReceipt | null;
|
||||
error: string | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
txData: { [txhash: string]: TransactionState };
|
||||
}
|
||||
|
||||
export const INITIAL_STATE: State = {
|
||||
txData: {}
|
||||
};
|
||||
|
||||
function fetchTxData(state: State, action: FetchTransactionDataAction): State {
|
||||
return {
|
||||
...state,
|
||||
txData: {
|
||||
...state.txData,
|
||||
[action.payload]: {
|
||||
data: null,
|
||||
receipt: null,
|
||||
error: null,
|
||||
isLoading: true
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function setTxData(state: State, action: SetTransactionDataAction): State {
|
||||
return {
|
||||
...state,
|
||||
txData: {
|
||||
...state.txData,
|
||||
[action.payload.txhash]: {
|
||||
data: action.payload.data,
|
||||
receipt: action.payload.receipt,
|
||||
error: action.payload.error,
|
||||
isLoading: false
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function transactions(state: State = INITIAL_STATE, action: TransactionsAction): State {
|
||||
switch (action.type) {
|
||||
case TypeKeys.TRANSACTIONS_FETCH_TRANSACTION_DATA:
|
||||
return fetchTxData(state, action);
|
||||
case TypeKeys.TRANSACTIONS_SET_TRANSACTION_DATA:
|
||||
return setTxData(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import { setGasEstimates, TypeKeys } from 'actions/gas';
|
||||
import { SagaIterator } from 'redux-saga';
|
||||
import { call, put, select, takeLatest } from 'redux-saga/effects';
|
||||
import { AppState } from 'reducers';
|
||||
import { fetchGasEstimates, GasEstimates } from 'api/gas';
|
||||
import { gasPriceDefaults, gasEstimateCacheTime } from 'config';
|
||||
import { getEstimates } from 'selectors/gas';
|
||||
import { getOffline } from 'selectors/config';
|
||||
|
||||
export function* setDefaultEstimates(): SagaIterator {
|
||||
// Must yield time for testability
|
||||
const time = yield call(Date.now);
|
||||
|
||||
yield put(
|
||||
setGasEstimates({
|
||||
safeLow: gasPriceDefaults.minGwei,
|
||||
standard: gasPriceDefaults.default,
|
||||
fast: gasPriceDefaults.default,
|
||||
fastest: gasPriceDefaults.maxGwei,
|
||||
isDefault: true,
|
||||
time
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function* fetchEstimates(): SagaIterator {
|
||||
// Don't even try offline
|
||||
const isOffline: boolean = yield select(getOffline);
|
||||
if (isOffline) {
|
||||
yield call(setDefaultEstimates);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache estimates for a bit
|
||||
const oldEstimates: AppState['gas']['estimates'] = yield select(getEstimates);
|
||||
if (oldEstimates && oldEstimates.time + gasEstimateCacheTime > Date.now()) {
|
||||
yield put(setGasEstimates(oldEstimates));
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to fetch new estimates
|
||||
try {
|
||||
const estimates: GasEstimates = yield call(fetchGasEstimates);
|
||||
yield put(setGasEstimates(estimates));
|
||||
} catch (err) {
|
||||
console.warn('Failed to fetch gas estimates:', err);
|
||||
yield call(setDefaultEstimates);
|
||||
}
|
||||
}
|
||||
|
||||
export default function* gas(): SagaIterator {
|
||||
yield takeLatest(TypeKeys.GAS_FETCH_ESTIMATES, fetchEstimates);
|
||||
}
|
|
@ -15,6 +15,8 @@ import { getBityRatesSaga, getShapeShiftRatesSaga, swapProviderSaga } from './sw
|
|||
import wallet from './wallet';
|
||||
import { ens } from './ens';
|
||||
import { transaction } from './transaction';
|
||||
import transactions from './transactions';
|
||||
import gas from './gas';
|
||||
|
||||
export default {
|
||||
ens,
|
||||
|
@ -33,5 +35,7 @@ export default {
|
|||
transaction,
|
||||
deterministicWallets,
|
||||
swapProviderSaga,
|
||||
rates
|
||||
rates,
|
||||
transactions,
|
||||
gas
|
||||
};
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import { setTransactionData, FetchTransactionDataAction, TypeKeys } from 'actions/transactions';
|
||||
import { SagaIterator } from 'redux-saga';
|
||||
import { put, select, apply, takeEvery } from 'redux-saga/effects';
|
||||
import { getNodeLib } from 'selectors/config';
|
||||
import { INode, TransactionData, TransactionReceipt } from 'libs/nodes';
|
||||
|
||||
export function* fetchTxData(action: FetchTransactionDataAction): SagaIterator {
|
||||
const txhash = action.payload;
|
||||
let data: TransactionData | null = null;
|
||||
let receipt: TransactionReceipt | null = null;
|
||||
let error: string | null = null;
|
||||
|
||||
const node: INode = yield select(getNodeLib);
|
||||
|
||||
// Fetch data and receipt separately, not in parallel. Receipt should only be
|
||||
// fetched if the tx is mined, and throws if it's not, but that's not really
|
||||
// an "error", since we'd still want to show the unmined tx data.
|
||||
try {
|
||||
data = yield apply(node, node.getTransactionByHash, [txhash]);
|
||||
} catch (err) {
|
||||
console.warn('Failed to fetch transaction data', err);
|
||||
error = err.message;
|
||||
}
|
||||
|
||||
if (data && data.blockHash) {
|
||||
try {
|
||||
receipt = yield apply(node, node.getTransactionReceipt, [txhash]);
|
||||
} catch (err) {
|
||||
console.warn('Failed to fetch transaction receipt', err);
|
||||
receipt = null;
|
||||
}
|
||||
}
|
||||
|
||||
yield put(setTransactionData({ txhash, data, receipt, error }));
|
||||
}
|
||||
|
||||
export default function* transactions(): SagaIterator {
|
||||
yield takeEvery(TypeKeys.TRANSACTIONS_FETCH_TRANSACTION_DATA, fetchTxData);
|
||||
}
|
|
@ -5,6 +5,15 @@ $handle-size: 22px;
|
|||
$speed: 70ms;
|
||||
$tooltip-bg: rgba(#222, 0.95);
|
||||
|
||||
@keyframes slider-loading {
|
||||
0%, 100% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.rc-slider {
|
||||
&-rail {
|
||||
background: $gray-lighter;
|
||||
|
@ -39,4 +48,20 @@ $tooltip-bg: rgba(#222, 0.95);
|
|||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
// Disabled styles
|
||||
&-disabled {
|
||||
background: none;
|
||||
|
||||
.rc-slider {
|
||||
&-handle,
|
||||
&-track {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-rail {
|
||||
animation: slider-loading 1s ease infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,3 +57,7 @@ hr {
|
|||
border: 0;
|
||||
border-top: 1px solid $hr-border;
|
||||
}
|
||||
|
||||
#app {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { AppState } from 'reducers';
|
||||
|
||||
const getGas = (state: AppState) => state.gas;
|
||||
export const getEstimates = (state: AppState) => getGas(state).estimates;
|
||||
export const getIsEstimating = (state: AppState) => getGas(state).isEstimating;
|
|
@ -0,0 +1,5 @@
|
|||
import { AppState } from 'reducers';
|
||||
|
||||
export function getTransactionDatas(state: AppState) {
|
||||
return state.transactions.txData;
|
||||
}
|
|
@ -654,7 +654,3 @@
|
|||
"ONBOARD_resume": "It looks like you didn't finish reading through these slides last time. ProTip: Finish reading through the slides 😉"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import BN from 'bn.js';
|
||||
import { Wei } from 'libs/units';
|
||||
import { stripHexPrefix } from 'libs/values';
|
||||
|
||||
export function toFixedIfLarger(num: number, fixedSize: number = 6): string {
|
||||
return parseFloat(num.toFixed(fixedSize)).toString();
|
||||
|
@ -113,3 +115,7 @@ export function bytesToHuman(bytes: number) {
|
|||
export function ensV3Url(name: string) {
|
||||
return `https://mycrypto.com/?ensname=${name}#ens`;
|
||||
}
|
||||
|
||||
export function hexToNumber(hex: string) {
|
||||
return new BN(stripHexPrefix(hex)).toNumber();
|
||||
}
|
||||
|
|
32
package.json
32
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "MyCrypto",
|
||||
"author": "MyCryptoHQ",
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.0",
|
||||
"main": "main.js",
|
||||
"description": "MyCrypto web and electron app",
|
||||
"repository": "https://github.com/MyCryptoHQ/MyCrypto",
|
||||
|
@ -37,7 +37,7 @@
|
|||
"react": "16.2.0",
|
||||
"react-dom": "16.2.0",
|
||||
"react-markdown": "3.2.0",
|
||||
"react-redux": "5.0.6",
|
||||
"react-redux": "5.0.7",
|
||||
"react-router-dom": "4.2.2",
|
||||
"react-router-redux": "4.0.8",
|
||||
"react-select": "1.2.1",
|
||||
|
@ -55,17 +55,17 @@
|
|||
"devDependencies": {
|
||||
"@types/classnames": "2.2.3",
|
||||
"@types/history": "4.6.2",
|
||||
"@types/jest": "22.1.2",
|
||||
"@types/lodash": "4.14.102",
|
||||
"@types/jest": "22.1.3",
|
||||
"@types/lodash": "4.14.104",
|
||||
"@types/qrcode": "0.8.0",
|
||||
"@types/qrcode.react": "0.6.3",
|
||||
"@types/query-string": "5.0.1",
|
||||
"@types/react": "16.0.37",
|
||||
"@types/query-string": "5.1.0",
|
||||
"@types/react": "16.0.38",
|
||||
"@types/react-dom": "16.0.4",
|
||||
"@types/react-redux": "5.0.15",
|
||||
"@types/react-router-dom": "4.2.4",
|
||||
"@types/react-router-redux": "5.0.12",
|
||||
"@types/react-select": "1.2.2",
|
||||
"@types/react-select": "1.2.3",
|
||||
"@types/redux-logger": "3.0.5",
|
||||
"@types/uuid": "3.4.3",
|
||||
"@types/webpack-env": "1.13.4",
|
||||
|
@ -80,7 +80,7 @@
|
|||
"copy-webpack-plugin": "4.4.1",
|
||||
"css-loader": "0.28.9",
|
||||
"electron": "1.8.2",
|
||||
"electron-builder": "19.56.0",
|
||||
"electron-builder": "20.0.4",
|
||||
"empty": "0.10.1",
|
||||
"enzyme": "3.3.0",
|
||||
"enzyme-adapter-react-16": "1.1.1",
|
||||
|
@ -91,7 +91,7 @@
|
|||
"file-loader": "1.1.6",
|
||||
"friendly-errors-webpack-plugin": "1.6.1",
|
||||
"glob": "7.1.2",
|
||||
"hoist-non-react-statics": "2.3.1",
|
||||
"hoist-non-react-statics": "2.5.0",
|
||||
"html-webpack-plugin": "2.30.1",
|
||||
"husky": "0.14.3",
|
||||
"image-webpack-loader": "4.1.0",
|
||||
|
@ -99,10 +99,10 @@
|
|||
"klaw-sync": "3.0.2",
|
||||
"less": "2.7.3",
|
||||
"less-loader": "4.0.5",
|
||||
"lint-staged": "6.1.0",
|
||||
"lint-staged": "6.1.1",
|
||||
"minimist": "1.2.0",
|
||||
"node-sass": "4.7.2",
|
||||
"nodemon": "1.14.12",
|
||||
"nodemon": "1.15.0",
|
||||
"null-loader": "0.1.1",
|
||||
"prettier": "1.10.2",
|
||||
"progress": "2.0.0",
|
||||
|
@ -113,14 +113,14 @@
|
|||
"resolve-url-loader": "2.2.1",
|
||||
"rimraf": "2.6.2",
|
||||
"sass-loader": "6.0.6",
|
||||
"style-loader": "0.20.0",
|
||||
"style-loader": "0.20.2",
|
||||
"thread-loader": "1.1.2",
|
||||
"ts-jest": "22.0.4",
|
||||
"ts-loader": "3.3.1",
|
||||
"tslint": "5.9.1",
|
||||
"tslint-config-prettier": "1.7.0",
|
||||
"tslint-microsoft-contrib": "5.0.2",
|
||||
"tslint-react": "3.5.0",
|
||||
"tslint-config-prettier": "1.8.0",
|
||||
"tslint-microsoft-contrib": "5.0.3",
|
||||
"tslint-react": "3.5.1",
|
||||
"types-rlp": "0.0.1",
|
||||
"typescript": "2.6.2",
|
||||
"url-loader": "0.6.2",
|
||||
|
@ -129,7 +129,7 @@
|
|||
"webpack-dev-middleware": "2.0.5",
|
||||
"webpack-hot-middleware": "2.21.0",
|
||||
"webpack-sources": "1.0.1",
|
||||
"webpack-subresource-integrity": "1.0.3",
|
||||
"webpack-subresource-integrity": "1.0.4",
|
||||
"worker-loader": "1.1.0",
|
||||
"what-input": "5.0.5"
|
||||
},
|
||||
|
|
|
@ -3,9 +3,11 @@ import { StaticNetworksState, CustomNetworksState } from 'reducers/config/networ
|
|||
type StaticNetworkIds = 'ETH' | 'Ropsten' | 'Kovan' | 'Rinkeby' | 'ETC' | 'UBQ' | 'EXP';
|
||||
|
||||
interface BlockExplorerConfig {
|
||||
name: string;
|
||||
origin: string;
|
||||
txUrl(txHash: string): string;
|
||||
addressUrl(address: string): string;
|
||||
blockUrl(blockNum: string | number): string;
|
||||
}
|
||||
|
||||
interface Token {
|
||||
|
@ -32,7 +34,7 @@ interface StaticNetworkConfig {
|
|||
name: StaticNetworkIds;
|
||||
unit: string;
|
||||
color?: string;
|
||||
blockExplorer?: BlockExplorerConfig;
|
||||
blockExplorer: BlockExplorerConfig;
|
||||
tokenExplorer?: {
|
||||
name: string;
|
||||
address(address: string): string;
|
||||
|
|
|
@ -18,7 +18,7 @@ const expectedInitialState = {
|
|||
chainId: 1,
|
||||
isCustom: false,
|
||||
color: '#0e97c0',
|
||||
blockExplorer: makeExplorer('https://etherscan.io'),
|
||||
blockExplorer: makeExplorer('Etherscan', 'https://etherscan.io'),
|
||||
tokenExplorer: {
|
||||
name: ethPlorer,
|
||||
address: ETHTokenExplorer
|
||||
|
@ -37,7 +37,7 @@ const expectedInitialState = {
|
|||
chainId: 3,
|
||||
isCustom: false,
|
||||
color: '#adc101',
|
||||
blockExplorer: makeExplorer('https://ropsten.etherscan.io'),
|
||||
blockExplorer: makeExplorer('Etherscan', 'https://ropsten.etherscan.io'),
|
||||
tokens: require('config/tokens/ropsten.json'),
|
||||
contracts: require('config/contracts/ropsten.json'),
|
||||
isTestnet: true,
|
||||
|
@ -53,7 +53,7 @@ const expectedInitialState = {
|
|||
chainId: 42,
|
||||
isCustom: false,
|
||||
color: '#adc101',
|
||||
blockExplorer: makeExplorer('https://kovan.etherscan.io'),
|
||||
blockExplorer: makeExplorer('Etherscan', 'https://kovan.etherscan.io'),
|
||||
tokens: require('config/tokens/ropsten.json'),
|
||||
contracts: require('config/contracts/ropsten.json'),
|
||||
isTestnet: true,
|
||||
|
@ -69,7 +69,7 @@ const expectedInitialState = {
|
|||
chainId: 4,
|
||||
isCustom: false,
|
||||
color: '#adc101',
|
||||
blockExplorer: makeExplorer('https://rinkeby.etherscan.io'),
|
||||
blockExplorer: makeExplorer('Etherscan', 'https://rinkeby.etherscan.io'),
|
||||
tokens: require('config/tokens/rinkeby.json'),
|
||||
contracts: require('config/contracts/rinkeby.json'),
|
||||
isTestnet: true,
|
||||
|
@ -85,7 +85,7 @@ const expectedInitialState = {
|
|||
chainId: 61,
|
||||
isCustom: false,
|
||||
color: '#669073',
|
||||
blockExplorer: makeExplorer('https://gastracker.io'),
|
||||
blockExplorer: makeExplorer('GasTracker', 'https://gastracker.io'),
|
||||
tokens: require('config/tokens/etc.json'),
|
||||
contracts: require('config/contracts/etc.json'),
|
||||
dPathFormats: {
|
||||
|
@ -100,7 +100,7 @@ const expectedInitialState = {
|
|||
chainId: 8,
|
||||
isCustom: false,
|
||||
color: '#b37aff',
|
||||
blockExplorer: makeExplorer('https://ubiqscan.io/en'),
|
||||
blockExplorer: makeExplorer('Ubiqscan', 'https://ubiqscan.io/en'),
|
||||
tokens: require('config/tokens/ubq.json'),
|
||||
contracts: require('config/contracts/ubq.json'),
|
||||
dPathFormats: {
|
||||
|
@ -115,7 +115,7 @@ const expectedInitialState = {
|
|||
chainId: 2,
|
||||
isCustom: false,
|
||||
color: '#673ab7',
|
||||
blockExplorer: makeExplorer('https://www.gander.tech'),
|
||||
blockExplorer: makeExplorer('Gander', 'https://www.gander.tech'),
|
||||
tokens: require('config/tokens/exp.json'),
|
||||
contracts: require('config/contracts/exp.json'),
|
||||
dPathFormats: {
|
||||
|
|
|
@ -23,14 +23,14 @@ const expectedInitialState = {
|
|||
network: 'ETH',
|
||||
isCustom: false,
|
||||
service: 'infura.io',
|
||||
lib: new InfuraNode('https://mainnet.infura.io/mew'),
|
||||
lib: new InfuraNode('https://mainnet.infura.io/mycrypto'),
|
||||
estimateGas: false
|
||||
},
|
||||
rop_infura: {
|
||||
network: 'Ropsten',
|
||||
isCustom: false,
|
||||
service: 'infura.io',
|
||||
lib: new InfuraNode('https://ropsten.infura.io/mew'),
|
||||
lib: new InfuraNode('https://ropsten.infura.io/mycrypto'),
|
||||
estimateGas: false
|
||||
},
|
||||
kov_ethscan: {
|
||||
|
@ -51,7 +51,7 @@ const expectedInitialState = {
|
|||
network: 'Rinkeby',
|
||||
isCustom: false,
|
||||
service: 'infura.io',
|
||||
lib: new InfuraNode('https://rinkeby.infura.io/mew'),
|
||||
lib: new InfuraNode('https://rinkeby.infura.io/mycrypto'),
|
||||
estimateGas: false
|
||||
},
|
||||
etc_epool: {
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { gas, INITIAL_STATE } from 'reducers/gas';
|
||||
import { fetchGasEstimates, setGasEstimates } from 'actions/gas';
|
||||
import { GasEstimates } from 'api/gas';
|
||||
|
||||
describe('gas reducer', () => {
|
||||
it('should handle GAS_FETCH_ESTIMATES', () => {
|
||||
const state = gas(undefined, fetchGasEstimates());
|
||||
expect(state).toEqual({
|
||||
...INITIAL_STATE,
|
||||
isEstimating: true
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle GAS_SET_ESTIMATES', () => {
|
||||
const estimates: GasEstimates = {
|
||||
safeLow: 1,
|
||||
standard: 1,
|
||||
fast: 4,
|
||||
fastest: 20,
|
||||
time: Date.now(),
|
||||
isDefault: false
|
||||
};
|
||||
const state = gas(undefined, setGasEstimates(estimates));
|
||||
expect(state).toEqual({
|
||||
...INITIAL_STATE,
|
||||
estimates,
|
||||
isEstimating: false
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,94 @@
|
|||
import { fetchEstimates, setDefaultEstimates } from 'sagas/gas';
|
||||
import { call, put, select } from 'redux-saga/effects';
|
||||
import { cloneableGenerator } from 'redux-saga/utils';
|
||||
import { fetchGasEstimates, GasEstimates } from 'api/gas';
|
||||
import { setGasEstimates } from 'actions/gas';
|
||||
import { getEstimates } from 'selectors/gas';
|
||||
import { getOffline } from 'selectors/config';
|
||||
import { gasPriceDefaults, gasEstimateCacheTime } from 'config';
|
||||
|
||||
describe('fetchEstimates*', () => {
|
||||
const gen = cloneableGenerator(fetchEstimates)();
|
||||
const offline = false;
|
||||
const oldEstimates: GasEstimates = {
|
||||
safeLow: 1,
|
||||
standard: 1,
|
||||
fast: 4,
|
||||
fastest: 20,
|
||||
time: Date.now() - gasEstimateCacheTime - 1000,
|
||||
isDefault: false
|
||||
};
|
||||
const newEstimates: GasEstimates = {
|
||||
safeLow: 2,
|
||||
standard: 2,
|
||||
fast: 8,
|
||||
fastest: 80,
|
||||
time: Date.now(),
|
||||
isDefault: false
|
||||
};
|
||||
|
||||
it('Should select getOffline', () => {
|
||||
expect(gen.next().value).toEqual(select(getOffline));
|
||||
});
|
||||
|
||||
it('Should use default estimates if offline', () => {
|
||||
const offlineGen = gen.clone();
|
||||
expect(offlineGen.next(true).value).toEqual(call(setDefaultEstimates));
|
||||
expect(offlineGen.next().done).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Should select getEstimates', () => {
|
||||
expect(gen.next(offline).value).toEqual(select(getEstimates));
|
||||
});
|
||||
|
||||
it('Should use cached estimates if they’re recent', () => {
|
||||
const cachedGen = gen.clone();
|
||||
const cacheEstimate = {
|
||||
...oldEstimates,
|
||||
time: Date.now() - gasEstimateCacheTime + 1000
|
||||
};
|
||||
expect(cachedGen.next(cacheEstimate).value).toEqual(put(setGasEstimates(cacheEstimate)));
|
||||
expect(cachedGen.next().done).toBeTruthy();
|
||||
});
|
||||
|
||||
it('Should fetch new estimates', () => {
|
||||
expect(gen.next(oldEstimates).value).toEqual(call(fetchGasEstimates));
|
||||
});
|
||||
|
||||
it('Should use default estimates if request fails', () => {
|
||||
const failedReqGen = gen.clone();
|
||||
// Not sure why, but typescript seems to think throw might be missing.
|
||||
if (failedReqGen.throw) {
|
||||
expect(failedReqGen.throw('test').value).toEqual(call(setDefaultEstimates));
|
||||
expect(failedReqGen.next().done).toBeTruthy();
|
||||
} else {
|
||||
throw new Error('SagaIterator didn’t have throw');
|
||||
}
|
||||
});
|
||||
|
||||
it('Should use fetched estimates', () => {
|
||||
expect(gen.next(newEstimates).value).toEqual(put(setGasEstimates(newEstimates)));
|
||||
expect(gen.next().done).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setDefaultEstimates*', () => {
|
||||
const gen = cloneableGenerator(setDefaultEstimates)();
|
||||
|
||||
it('Should put setGasEstimates with config defaults', () => {
|
||||
const time = Date.now();
|
||||
gen.next();
|
||||
expect(gen.next(time).value).toEqual(
|
||||
put(
|
||||
setGasEstimates({
|
||||
safeLow: gasPriceDefaults.minGwei,
|
||||
standard: gasPriceDefaults.default,
|
||||
fast: gasPriceDefaults.default,
|
||||
fastest: gasPriceDefaults.maxGwei,
|
||||
isDefault: true,
|
||||
time
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue