mirror of
https://github.com/status-im/MyCrypto.git
synced 2025-01-11 03:26:14 +00:00
Check Transaction page (Pt. 1 - The Basics) (#1099)
* Component layer and routing for transaction status. * Initial start on redux for transactions. * Initial crack at reducer / actions / saga for transactions. * Finish off check transaction saga, reducer, component, and page.
This commit is contained in:
parent
f46df010db
commit
be61d804e0
@ -9,6 +9,7 @@ import SendTransaction from 'containers/Tabs/SendTransaction';
|
|||||||
import Swap from 'containers/Tabs/Swap';
|
import Swap from 'containers/Tabs/Swap';
|
||||||
import SignAndVerifyMessage from 'containers/Tabs/SignAndVerifyMessage';
|
import SignAndVerifyMessage from 'containers/Tabs/SignAndVerifyMessage';
|
||||||
import BroadcastTx from 'containers/Tabs/BroadcastTx';
|
import BroadcastTx from 'containers/Tabs/BroadcastTx';
|
||||||
|
import CheckTransaction from 'containers/Tabs/CheckTransaction';
|
||||||
import ErrorScreen from 'components/ErrorScreen';
|
import ErrorScreen from 'components/ErrorScreen';
|
||||||
import PageNotFound from 'components/PageNotFound';
|
import PageNotFound from 'components/PageNotFound';
|
||||||
import LogOutPrompt from 'components/LogOutPrompt';
|
import LogOutPrompt from 'components/LogOutPrompt';
|
||||||
@ -67,6 +68,7 @@ export default class Root extends Component<Props, State> {
|
|||||||
<Route path="/contracts" component={Contracts} />
|
<Route path="/contracts" component={Contracts} />
|
||||||
<Route path="/ens" component={ENS} exact={true} />
|
<Route path="/ens" component={ENS} exact={true} />
|
||||||
<Route path="/sign-and-verify-message" component={SignAndVerifyMessage} />
|
<Route path="/sign-and-verify-message" component={SignAndVerifyMessage} />
|
||||||
|
<Route path="/tx-status" component={CheckTransaction} exact={true} />
|
||||||
<Route path="/pushTx" component={BroadcastTx} />
|
<Route path="/pushTx" component={BroadcastTx} />
|
||||||
<RouteNotFound />
|
<RouteNotFound />
|
||||||
</Switch>
|
</Switch>
|
||||||
@ -120,8 +122,7 @@ const LegacyRoutes = withRouter(props => {
|
|||||||
history.push('/account/info');
|
history.push('/account/info');
|
||||||
break;
|
break;
|
||||||
case '#check-tx-status':
|
case '#check-tx-status':
|
||||||
history.push('/check-tx-status');
|
return <RedirectWithQuery from={pathname} to={'/tx-status'} />;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
20
common/actions/transactions/actionCreators.ts
Normal file
20
common/actions/transactions/actionCreators.ts
Normal file
@ -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
|
||||||
|
};
|
||||||
|
}
|
20
common/actions/transactions/actionTypes.ts
Normal file
20
common/actions/transactions/actionTypes.ts
Normal file
@ -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;
|
5
common/actions/transactions/constants.ts
Normal file
5
common/actions/transactions/constants.ts
Normal file
@ -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'
|
||||||
|
}
|
3
common/actions/transactions/index.ts
Normal file
3
common/actions/transactions/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './actionCreators';
|
||||||
|
export * from './actionTypes';
|
||||||
|
export * from './constants';
|
@ -34,6 +34,10 @@ const tabs: TabLink[] = [
|
|||||||
name: 'Sign & Verify Message',
|
name: 'Sign & Verify Message',
|
||||||
to: '/sign-and-verify-message'
|
to: '/sign-and-verify-message'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'NAV_TxStatus',
|
||||||
|
to: '/tx-status'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Broadcast Transaction',
|
name: 'Broadcast Transaction',
|
||||||
to: '/pushTx'
|
to: '/pushTx'
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
174
common/components/TransactionStatus/TransactionDataTable.tsx
Normal file
174
common/components/TransactionStatus/TransactionDataTable.tsx
Normal file
@ -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;
|
35
common/components/TransactionStatus/TransactionStatus.scss
Normal file
35
common/components/TransactionStatus/TransactionStatus.scss
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
88
common/components/TransactionStatus/TransactionStatus.tsx
Normal file
88
common/components/TransactionStatus/TransactionStatus.tsx
Normal file
@ -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);
|
2
common/components/TransactionStatus/index.tsx
Normal file
2
common/components/TransactionStatus/index.tsx
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import TransactionStatus from './TransactionStatus';
|
||||||
|
export default TransactionStatus;
|
@ -18,3 +18,4 @@ export { default as TXMetaDataPanel } from './TXMetaDataPanel';
|
|||||||
export { default as WalletDecrypt } from './WalletDecrypt';
|
export { default as WalletDecrypt } from './WalletDecrypt';
|
||||||
export { default as TogglablePassword } from './TogglablePassword';
|
export { default as TogglablePassword } from './TogglablePassword';
|
||||||
export { default as GenerateKeystoreModal } from './GenerateKeystoreModal';
|
export { default as GenerateKeystoreModal } from './GenerateKeystoreModal';
|
||||||
|
export { default as TransactionStatus } from './TransactionStatus';
|
||||||
|
@ -31,8 +31,8 @@ export interface AAttributes {
|
|||||||
|
|
||||||
interface NewTabLinkProps extends AAttributes {
|
interface NewTabLinkProps extends AAttributes {
|
||||||
href: string;
|
href: string;
|
||||||
content?: React.ReactElement<any> | string | string[];
|
content?: React.ReactElement<any> | string | string[] | number;
|
||||||
children?: React.ReactElement<any> | string | string[];
|
children?: React.ReactElement<any> | string | string[] | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class NewTabLink extends React.Component<NewTabLinkProps> {
|
export class NewTabLink extends React.Component<NewTabLinkProps> {
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
12
common/containers/Tabs/CheckTransaction/index.scss
Normal file
12
common/containers/Tabs/CheckTransaction/index.scss
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
@import 'common/sass/variables';
|
||||||
|
|
||||||
|
.CheckTransaction {
|
||||||
|
&-form {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&-desc {
|
||||||
|
max-width: 580px;
|
||||||
|
margin: 0 auto $space;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
69
common/containers/Tabs/CheckTransaction/index.tsx
Normal file
69
common/containers/Tabs/CheckTransaction/index.tsx
Normal file
@ -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);
|
@ -6,10 +6,39 @@ export interface TxObj {
|
|||||||
to: string;
|
to: string;
|
||||||
data: string;
|
data: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TokenBalanceResult {
|
interface TokenBalanceResult {
|
||||||
balance: TokenValue;
|
balance: TokenValue;
|
||||||
error: string | null;
|
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 {
|
export interface INode {
|
||||||
ping(): Promise<boolean>;
|
ping(): Promise<boolean>;
|
||||||
getBalance(address: string): Promise<Wei>;
|
getBalance(address: string): Promise<Wei>;
|
||||||
@ -17,6 +46,8 @@ export interface INode {
|
|||||||
getTokenBalances(address: string, tokens: Token[]): Promise<TokenBalanceResult[]>;
|
getTokenBalances(address: string, tokens: Token[]): Promise<TokenBalanceResult[]>;
|
||||||
estimateGas(tx: Partial<IHexStrTransaction>): Promise<Wei>;
|
estimateGas(tx: Partial<IHexStrTransaction>): Promise<Wei>;
|
||||||
getTransactionCount(address: string): Promise<string>;
|
getTransactionCount(address: string): Promise<string>;
|
||||||
|
getTransactionByHash(txhash: string): Promise<TransactionData>;
|
||||||
|
getTransactionReceipt(txhash: string): Promise<TransactionReceipt>;
|
||||||
sendRawTx(tx: string): Promise<string>;
|
sendRawTx(tx: string): Promise<string>;
|
||||||
sendCallRequest(txObj: TxObj): Promise<string>;
|
sendCallRequest(txObj: TxObj): Promise<string>;
|
||||||
getCurrentBlock(): Promise<string>;
|
getCurrentBlock(): Promise<string>;
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
GetBalanceRequest,
|
GetBalanceRequest,
|
||||||
GetTokenBalanceRequest,
|
GetTokenBalanceRequest,
|
||||||
GetTransactionCountRequest,
|
GetTransactionCountRequest,
|
||||||
|
GetTransactionByHashRequest,
|
||||||
SendRawTxRequest,
|
SendRawTxRequest,
|
||||||
GetCurrentBlockRequest
|
GetCurrentBlockRequest
|
||||||
} from './types';
|
} 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 {
|
public getTokenBalance(address: string, token: Token): GetTokenBalanceRequest {
|
||||||
return this.ethCall({
|
return this.ethCall({
|
||||||
to: token.address,
|
to: token.address,
|
||||||
|
@ -41,6 +41,12 @@ export interface GetTransactionCountRequest extends EtherscanReqBase {
|
|||||||
tag: 'latest';
|
tag: 'latest';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GetTransactionByHashRequest extends EtherscanReqBase {
|
||||||
|
module: 'proxy';
|
||||||
|
action: 'eth_getTransactionByHash';
|
||||||
|
txhash: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface GetCurrentBlockRequest extends EtherscanReqBase {
|
export interface GetCurrentBlockRequest extends EtherscanReqBase {
|
||||||
module: 'proxy';
|
module: 'proxy';
|
||||||
action: 'eth_blockNumber';
|
action: 'eth_blockNumber';
|
||||||
@ -53,4 +59,5 @@ export type EtherscanRequest =
|
|||||||
| GetTokenBalanceRequest
|
| GetTokenBalanceRequest
|
||||||
| EstimateGasRequest
|
| EstimateGasRequest
|
||||||
| GetTransactionCountRequest
|
| GetTransactionCountRequest
|
||||||
|
| GetTransactionByHashRequest
|
||||||
| GetCurrentBlockRequest;
|
| GetCurrentBlockRequest;
|
||||||
|
@ -3,3 +3,4 @@ export { default as InfuraNode } from './infura';
|
|||||||
export { default as EtherscanNode } from './etherscan';
|
export { default as EtherscanNode } from './etherscan';
|
||||||
export { default as CustomNode } from './custom';
|
export { default as CustomNode } from './custom';
|
||||||
export { default as Web3Node } from './web3';
|
export { default as Web3Node } from './web3';
|
||||||
|
export * from './INode';
|
||||||
|
@ -2,7 +2,8 @@ import BN from 'bn.js';
|
|||||||
import { IHexStrTransaction } from 'libs/transaction';
|
import { IHexStrTransaction } from 'libs/transaction';
|
||||||
import { Wei, TokenValue } from 'libs/units';
|
import { Wei, TokenValue } from 'libs/units';
|
||||||
import { stripHexPrefix } from 'libs/values';
|
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 RPCClient from './client';
|
||||||
import RPCRequests from './requests';
|
import RPCRequests from './requests';
|
||||||
import {
|
import {
|
||||||
@ -11,9 +12,11 @@ import {
|
|||||||
isValidCallRequest,
|
isValidCallRequest,
|
||||||
isValidTokenBalance,
|
isValidTokenBalance,
|
||||||
isValidTransactionCount,
|
isValidTransactionCount,
|
||||||
|
isValidTransactionByHash,
|
||||||
|
isValidTransactionReceipt,
|
||||||
isValidCurrentBlock,
|
isValidCurrentBlock,
|
||||||
isValidRawTxApi
|
isValidRawTxApi
|
||||||
} from '../../validators';
|
} from 'libs/validators';
|
||||||
import { Token } from 'types/network';
|
import { Token } from 'types/network';
|
||||||
|
|
||||||
export default class RpcNode implements INode {
|
export default class RpcNode implements INode {
|
||||||
@ -46,8 +49,6 @@ export default class RpcNode implements INode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public estimateGas(transaction: Partial<IHexStrTransaction>): Promise<Wei> {
|
public estimateGas(transaction: Partial<IHexStrTransaction>): Promise<Wei> {
|
||||||
// Timeout after 10 seconds
|
|
||||||
|
|
||||||
return this.client
|
return this.client
|
||||||
.call(this.requests.estimateGas(transaction))
|
.call(this.requests.estimateGas(transaction))
|
||||||
.then(isValidEstimateGas)
|
.then(isValidEstimateGas)
|
||||||
@ -106,6 +107,37 @@ export default class RpcNode implements INode {
|
|||||||
.then(({ result }) => result);
|
.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> {
|
public getCurrentBlock(): Promise<string> {
|
||||||
return this.client
|
return this.client
|
||||||
.call(this.requests.getCurrentBlock())
|
.call(this.requests.getCurrentBlock())
|
||||||
|
@ -6,7 +6,9 @@ import {
|
|||||||
GetTokenBalanceRequest,
|
GetTokenBalanceRequest,
|
||||||
GetTransactionCountRequest,
|
GetTransactionCountRequest,
|
||||||
SendRawTxRequest,
|
SendRawTxRequest,
|
||||||
GetCurrentBlockRequest
|
GetCurrentBlockRequest,
|
||||||
|
GetTransactionByHashRequest,
|
||||||
|
GetTransactionReceiptRequest
|
||||||
} from './types';
|
} from './types';
|
||||||
import { hexEncodeData } from './utils';
|
import { hexEncodeData } from './utils';
|
||||||
import { TxObj } from '../INode';
|
import { TxObj } from '../INode';
|
||||||
@ -17,6 +19,8 @@ export default class RPCRequests {
|
|||||||
return { method: 'net_version' };
|
return { method: 'net_version' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TODO: Fix `| any` on all of these */
|
||||||
|
|
||||||
public sendRawTx(signedTx: string): SendRawTxRequest | any {
|
public sendRawTx(signedTx: string): SendRawTxRequest | any {
|
||||||
return {
|
return {
|
||||||
method: 'eth_sendRawTransaction',
|
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 {
|
public getTokenBalance(address: string, token: Token): GetTokenBalanceRequest | any {
|
||||||
return {
|
return {
|
||||||
method: 'eth_call',
|
method: 'eth_call',
|
||||||
|
@ -69,6 +69,16 @@ export interface GetTransactionCountRequest extends RPCRequestBase {
|
|||||||
params: [DATA, DEFAULT_BLOCK];
|
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 {
|
export interface GetCurrentBlockRequest extends RPCRequestBase {
|
||||||
method: 'eth_blockNumber';
|
method: 'eth_blockNumber';
|
||||||
}
|
}
|
||||||
@ -80,4 +90,6 @@ export type RPCRequest =
|
|||||||
| CallRequest
|
| CallRequest
|
||||||
| EstimateGasRequest
|
| EstimateGasRequest
|
||||||
| GetTransactionCountRequest
|
| GetTransactionCountRequest
|
||||||
|
| GetTransactionByHashRequest
|
||||||
|
| GetTransactionReceiptRequest
|
||||||
| GetCurrentBlockRequest;
|
| GetCurrentBlockRequest;
|
||||||
|
@ -167,7 +167,9 @@ export const schema = {
|
|||||||
properties: {
|
properties: {
|
||||||
jsonrpc: { type: 'string' },
|
jsonrpc: { type: 'string' },
|
||||||
id: { oneOf: [{ type: 'string' }, { type: 'integer' }] },
|
id: { oneOf: [{ type: 'string' }, { type: 'integer' }] },
|
||||||
result: { oneOf: [{ type: 'string' }, { type: 'array' }] },
|
result: {
|
||||||
|
oneOf: [{ type: 'string' }, { type: 'array' }, { type: 'object' }]
|
||||||
|
},
|
||||||
status: { type: 'string' },
|
status: { type: 'string' },
|
||||||
message: { type: 'string', maxLength: 2 }
|
message: { type: 'string', maxLength: 2 }
|
||||||
}
|
}
|
||||||
@ -236,6 +238,12 @@ export const isValidTokenBalance = (response: JsonRpcResponse) =>
|
|||||||
export const isValidTransactionCount = (response: JsonRpcResponse) =>
|
export const isValidTransactionCount = (response: JsonRpcResponse) =>
|
||||||
isValidEthCall(response, schema.RpcNode)('Transaction Count');
|
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) =>
|
export const isValidCurrentBlock = (response: JsonRpcResponse) =>
|
||||||
isValidEthCall(response, schema.RpcNode)('Current Block');
|
isValidEthCall(response, schema.RpcNode)('Current Block');
|
||||||
|
|
||||||
@ -253,3 +261,6 @@ export const isValidGetAccounts = (response: JsonRpcResponse) =>
|
|||||||
|
|
||||||
export const isValidGetNetVersion = (response: JsonRpcResponse) =>
|
export const isValidGetNetVersion = (response: JsonRpcResponse) =>
|
||||||
isValidEthCall(response, schema.RpcNode)('Net Version');
|
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
|
// Must be a website that follows the ethplorer convention of /tx/[hash] and
|
||||||
// address/[address] to generate the correct functions.
|
// address/[address] to generate the correct functions.
|
||||||
// TODO: put this in utils / libs
|
// TODO: put this in utils / libs
|
||||||
export function makeExplorer(origin: string): BlockExplorerConfig {
|
export function makeExplorer(name: string, origin: string): BlockExplorerConfig {
|
||||||
return {
|
return {
|
||||||
|
name,
|
||||||
origin,
|
origin,
|
||||||
txUrl: hash => `${origin}/tx/${hash}`,
|
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,
|
chainId: 1,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
color: '#0e97c0',
|
color: '#0e97c0',
|
||||||
blockExplorer: makeExplorer('https://etherscan.io'),
|
blockExplorer: makeExplorer('Etherscan', 'https://etherscan.io'),
|
||||||
tokenExplorer: {
|
tokenExplorer: {
|
||||||
name: ethPlorer,
|
name: ethPlorer,
|
||||||
address: ETHTokenExplorer
|
address: ETHTokenExplorer
|
||||||
@ -51,7 +53,7 @@ const INITIAL_STATE: State = {
|
|||||||
chainId: 3,
|
chainId: 3,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
color: '#adc101',
|
color: '#adc101',
|
||||||
blockExplorer: makeExplorer('https://ropsten.etherscan.io'),
|
blockExplorer: makeExplorer('Etherscan', 'https://ropsten.etherscan.io'),
|
||||||
tokens: require('config/tokens/ropsten.json'),
|
tokens: require('config/tokens/ropsten.json'),
|
||||||
contracts: require('config/contracts/ropsten.json'),
|
contracts: require('config/contracts/ropsten.json'),
|
||||||
isTestnet: true,
|
isTestnet: true,
|
||||||
@ -67,7 +69,7 @@ const INITIAL_STATE: State = {
|
|||||||
chainId: 42,
|
chainId: 42,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
color: '#adc101',
|
color: '#adc101',
|
||||||
blockExplorer: makeExplorer('https://kovan.etherscan.io'),
|
blockExplorer: makeExplorer('Etherscan', 'https://kovan.etherscan.io'),
|
||||||
tokens: require('config/tokens/ropsten.json'),
|
tokens: require('config/tokens/ropsten.json'),
|
||||||
contracts: require('config/contracts/ropsten.json'),
|
contracts: require('config/contracts/ropsten.json'),
|
||||||
isTestnet: true,
|
isTestnet: true,
|
||||||
@ -83,7 +85,7 @@ const INITIAL_STATE: State = {
|
|||||||
chainId: 4,
|
chainId: 4,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
color: '#adc101',
|
color: '#adc101',
|
||||||
blockExplorer: makeExplorer('https://rinkeby.etherscan.io'),
|
blockExplorer: makeExplorer('Etherscan', 'https://rinkeby.etherscan.io'),
|
||||||
tokens: require('config/tokens/rinkeby.json'),
|
tokens: require('config/tokens/rinkeby.json'),
|
||||||
contracts: require('config/contracts/rinkeby.json'),
|
contracts: require('config/contracts/rinkeby.json'),
|
||||||
isTestnet: true,
|
isTestnet: true,
|
||||||
@ -99,7 +101,7 @@ const INITIAL_STATE: State = {
|
|||||||
chainId: 61,
|
chainId: 61,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
color: '#669073',
|
color: '#669073',
|
||||||
blockExplorer: makeExplorer('https://gastracker.io'),
|
blockExplorer: makeExplorer('GasTracker', 'https://gastracker.io'),
|
||||||
tokens: require('config/tokens/etc.json'),
|
tokens: require('config/tokens/etc.json'),
|
||||||
contracts: require('config/contracts/etc.json'),
|
contracts: require('config/contracts/etc.json'),
|
||||||
dPathFormats: {
|
dPathFormats: {
|
||||||
@ -114,7 +116,7 @@ const INITIAL_STATE: State = {
|
|||||||
chainId: 8,
|
chainId: 8,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
color: '#b37aff',
|
color: '#b37aff',
|
||||||
blockExplorer: makeExplorer('https://ubiqscan.io/en'),
|
blockExplorer: makeExplorer('Ubiqscan', 'https://ubiqscan.io/en'),
|
||||||
tokens: require('config/tokens/ubq.json'),
|
tokens: require('config/tokens/ubq.json'),
|
||||||
contracts: require('config/contracts/ubq.json'),
|
contracts: require('config/contracts/ubq.json'),
|
||||||
dPathFormats: {
|
dPathFormats: {
|
||||||
@ -129,7 +131,7 @@ const INITIAL_STATE: State = {
|
|||||||
chainId: 2,
|
chainId: 2,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
color: '#673ab7',
|
color: '#673ab7',
|
||||||
blockExplorer: makeExplorer('https://www.gander.tech'),
|
blockExplorer: makeExplorer('Gander', 'https://www.gander.tech'),
|
||||||
tokens: require('config/tokens/exp.json'),
|
tokens: require('config/tokens/exp.json'),
|
||||||
contracts: require('config/contracts/exp.json'),
|
contracts: require('config/contracts/exp.json'),
|
||||||
dPathFormats: {
|
dPathFormats: {
|
||||||
|
@ -10,6 +10,7 @@ import { State as SwapState, swap } from './swap';
|
|||||||
import { State as WalletState, wallet } from './wallet';
|
import { State as WalletState, wallet } from './wallet';
|
||||||
import { State as TransactionState, transaction } from './transaction';
|
import { State as TransactionState, transaction } from './transaction';
|
||||||
import { onboardStatus, State as OnboardStatusState } from './onboardStatus';
|
import { onboardStatus, State as OnboardStatusState } from './onboardStatus';
|
||||||
|
import { State as TransactionsState, transactions } from './transactions';
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
// Custom reducers
|
// Custom reducers
|
||||||
@ -21,11 +22,11 @@ export interface AppState {
|
|||||||
customTokens: CustomTokensState;
|
customTokens: CustomTokensState;
|
||||||
rates: RatesState;
|
rates: RatesState;
|
||||||
deterministicWallets: DeterministicWalletsState;
|
deterministicWallets: DeterministicWalletsState;
|
||||||
// Third party reducers (TODO: Fill these out)
|
|
||||||
form: any;
|
|
||||||
routing: any;
|
|
||||||
swap: SwapState;
|
swap: SwapState;
|
||||||
transaction: TransactionState;
|
transaction: TransactionState;
|
||||||
|
transactions: TransactionsState;
|
||||||
|
// Third party reducers (TODO: Fill these out)
|
||||||
|
routing: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default combineReducers<AppState>({
|
export default combineReducers<AppState>({
|
||||||
@ -38,6 +39,7 @@ export default combineReducers<AppState>({
|
|||||||
customTokens,
|
customTokens,
|
||||||
rates,
|
rates,
|
||||||
deterministicWallets,
|
deterministicWallets,
|
||||||
routing: routerReducer,
|
transaction,
|
||||||
transaction
|
transactions,
|
||||||
|
routing: routerReducer
|
||||||
});
|
});
|
||||||
|
63
common/reducers/transactions.ts
Normal file
63
common/reducers/transactions.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ import { getBityRatesSaga, getShapeShiftRatesSaga, swapProviderSaga } from './sw
|
|||||||
import wallet from './wallet';
|
import wallet from './wallet';
|
||||||
import { ens } from './ens';
|
import { ens } from './ens';
|
||||||
import { transaction } from './transaction';
|
import { transaction } from './transaction';
|
||||||
|
import transactions from './transactions';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
ens,
|
ens,
|
||||||
@ -33,5 +34,6 @@ export default {
|
|||||||
transaction,
|
transaction,
|
||||||
deterministicWallets,
|
deterministicWallets,
|
||||||
swapProviderSaga,
|
swapProviderSaga,
|
||||||
rates
|
rates,
|
||||||
|
transactions
|
||||||
};
|
};
|
||||||
|
39
common/sagas/transactions.ts
Normal file
39
common/sagas/transactions.ts
Normal file
@ -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
common/selectors/transactions.ts
Normal file
5
common/selectors/transactions.ts
Normal file
@ -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 😉"
|
"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 { Wei } from 'libs/units';
|
||||||
|
import { stripHexPrefix } from 'libs/values';
|
||||||
|
|
||||||
export function toFixedIfLarger(num: number, fixedSize: number = 6): string {
|
export function toFixedIfLarger(num: number, fixedSize: number = 6): string {
|
||||||
return parseFloat(num.toFixed(fixedSize)).toString();
|
return parseFloat(num.toFixed(fixedSize)).toString();
|
||||||
@ -113,3 +115,7 @@ export function bytesToHuman(bytes: number) {
|
|||||||
export function ensV3Url(name: string) {
|
export function ensV3Url(name: string) {
|
||||||
return `https://mycrypto.com/?ensname=${name}#ens`;
|
return `https://mycrypto.com/?ensname=${name}#ens`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hexToNumber(hex: string) {
|
||||||
|
return new BN(stripHexPrefix(hex)).toNumber();
|
||||||
|
}
|
||||||
|
4
shared/types/network.d.ts
vendored
4
shared/types/network.d.ts
vendored
@ -3,9 +3,11 @@ import { StaticNetworksState, CustomNetworksState } from 'reducers/config/networ
|
|||||||
type StaticNetworkIds = 'ETH' | 'Ropsten' | 'Kovan' | 'Rinkeby' | 'ETC' | 'UBQ' | 'EXP';
|
type StaticNetworkIds = 'ETH' | 'Ropsten' | 'Kovan' | 'Rinkeby' | 'ETC' | 'UBQ' | 'EXP';
|
||||||
|
|
||||||
interface BlockExplorerConfig {
|
interface BlockExplorerConfig {
|
||||||
|
name: string;
|
||||||
origin: string;
|
origin: string;
|
||||||
txUrl(txHash: string): string;
|
txUrl(txHash: string): string;
|
||||||
addressUrl(address: string): string;
|
addressUrl(address: string): string;
|
||||||
|
blockUrl(blockNum: string | number): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Token {
|
interface Token {
|
||||||
@ -32,7 +34,7 @@ interface StaticNetworkConfig {
|
|||||||
name: StaticNetworkIds;
|
name: StaticNetworkIds;
|
||||||
unit: string;
|
unit: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
blockExplorer?: BlockExplorerConfig;
|
blockExplorer: BlockExplorerConfig;
|
||||||
tokenExplorer?: {
|
tokenExplorer?: {
|
||||||
name: string;
|
name: string;
|
||||||
address(address: string): string;
|
address(address: string): string;
|
||||||
|
@ -18,7 +18,7 @@ const expectedInitialState = {
|
|||||||
chainId: 1,
|
chainId: 1,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
color: '#0e97c0',
|
color: '#0e97c0',
|
||||||
blockExplorer: makeExplorer('https://etherscan.io'),
|
blockExplorer: makeExplorer('Etherscan', 'https://etherscan.io'),
|
||||||
tokenExplorer: {
|
tokenExplorer: {
|
||||||
name: ethPlorer,
|
name: ethPlorer,
|
||||||
address: ETHTokenExplorer
|
address: ETHTokenExplorer
|
||||||
@ -37,7 +37,7 @@ const expectedInitialState = {
|
|||||||
chainId: 3,
|
chainId: 3,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
color: '#adc101',
|
color: '#adc101',
|
||||||
blockExplorer: makeExplorer('https://ropsten.etherscan.io'),
|
blockExplorer: makeExplorer('Etherscan', 'https://ropsten.etherscan.io'),
|
||||||
tokens: require('config/tokens/ropsten.json'),
|
tokens: require('config/tokens/ropsten.json'),
|
||||||
contracts: require('config/contracts/ropsten.json'),
|
contracts: require('config/contracts/ropsten.json'),
|
||||||
isTestnet: true,
|
isTestnet: true,
|
||||||
@ -53,7 +53,7 @@ const expectedInitialState = {
|
|||||||
chainId: 42,
|
chainId: 42,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
color: '#adc101',
|
color: '#adc101',
|
||||||
blockExplorer: makeExplorer('https://kovan.etherscan.io'),
|
blockExplorer: makeExplorer('Etherscan', 'https://kovan.etherscan.io'),
|
||||||
tokens: require('config/tokens/ropsten.json'),
|
tokens: require('config/tokens/ropsten.json'),
|
||||||
contracts: require('config/contracts/ropsten.json'),
|
contracts: require('config/contracts/ropsten.json'),
|
||||||
isTestnet: true,
|
isTestnet: true,
|
||||||
@ -69,7 +69,7 @@ const expectedInitialState = {
|
|||||||
chainId: 4,
|
chainId: 4,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
color: '#adc101',
|
color: '#adc101',
|
||||||
blockExplorer: makeExplorer('https://rinkeby.etherscan.io'),
|
blockExplorer: makeExplorer('Etherscan', 'https://rinkeby.etherscan.io'),
|
||||||
tokens: require('config/tokens/rinkeby.json'),
|
tokens: require('config/tokens/rinkeby.json'),
|
||||||
contracts: require('config/contracts/rinkeby.json'),
|
contracts: require('config/contracts/rinkeby.json'),
|
||||||
isTestnet: true,
|
isTestnet: true,
|
||||||
@ -85,7 +85,7 @@ const expectedInitialState = {
|
|||||||
chainId: 61,
|
chainId: 61,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
color: '#669073',
|
color: '#669073',
|
||||||
blockExplorer: makeExplorer('https://gastracker.io'),
|
blockExplorer: makeExplorer('GasTracker', 'https://gastracker.io'),
|
||||||
tokens: require('config/tokens/etc.json'),
|
tokens: require('config/tokens/etc.json'),
|
||||||
contracts: require('config/contracts/etc.json'),
|
contracts: require('config/contracts/etc.json'),
|
||||||
dPathFormats: {
|
dPathFormats: {
|
||||||
@ -100,7 +100,7 @@ const expectedInitialState = {
|
|||||||
chainId: 8,
|
chainId: 8,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
color: '#b37aff',
|
color: '#b37aff',
|
||||||
blockExplorer: makeExplorer('https://ubiqscan.io/en'),
|
blockExplorer: makeExplorer('Ubiqscan', 'https://ubiqscan.io/en'),
|
||||||
tokens: require('config/tokens/ubq.json'),
|
tokens: require('config/tokens/ubq.json'),
|
||||||
contracts: require('config/contracts/ubq.json'),
|
contracts: require('config/contracts/ubq.json'),
|
||||||
dPathFormats: {
|
dPathFormats: {
|
||||||
@ -115,7 +115,7 @@ const expectedInitialState = {
|
|||||||
chainId: 2,
|
chainId: 2,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
color: '#673ab7',
|
color: '#673ab7',
|
||||||
blockExplorer: makeExplorer('https://www.gander.tech'),
|
blockExplorer: makeExplorer('Gander', 'https://www.gander.tech'),
|
||||||
tokens: require('config/tokens/exp.json'),
|
tokens: require('config/tokens/exp.json'),
|
||||||
contracts: require('config/contracts/exp.json'),
|
contracts: require('config/contracts/exp.json'),
|
||||||
dPathFormats: {
|
dPathFormats: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user