Transaction confirmation modal (#108)
* Add a little arrow icon. * Replaced toEther function with toUnit to reduce the number of conversion functions wed need. Add tests for conversion functions. * First pass at a styled confirm transaction modal. * More data about data * Hook up generated transaction with modal * Fix modal position * Add from address. Restyle a bit. * Only show textareas and button if transaction has been generated. * Remove need for param. * Copy. * Use non-relative path. * Initial crack at transaction token support. * Fix flow type * Unit tests for contracts and erc20 * Convert contract class to ethereumjs-abi, caught a bug * Add decodeArgs for contracts, decodeTransfer for erc20 * Show token value in modal * Show value from transaction data in confirmation. * Show address of receiver, not token contract * Flow type * Only accept bigs * Unlog * Use ethereumjs-abis method ID function * Get transaction stuff out of state. Leave todo notes. * Intuit token from transaction to address. * Move generate transaction out of node and into libs/transaction. * timeout -> interval * Promise.reject -> throw * Get default currency from network. * Add more unit tests for decoding. Adopt the $ prefix for decoding calls. * Use signed transaction in confirmation modal.
This commit is contained in:
parent
318a42ccf3
commit
bf4171dfbd
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="77px" height="42px" viewBox="0 0 77 42" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 46 (44423) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>Artboard</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Artboard" fill="#9A9A9A">
|
||||||
|
<circle id="Oval" cx="6" cy="21" r="6"></circle>
|
||||||
|
<circle id="Oval" cx="24" cy="21" r="6"></circle>
|
||||||
|
<circle id="Oval" cx="42" cy="21" r="6"></circle>
|
||||||
|
<rect id="Rectangle" transform="translate(64.000000, 13.000000) rotate(45.000000) translate(-64.000000, -13.000000) " x="48" y="8" width="32" height="10" rx="5"></rect>
|
||||||
|
<rect id="Rectangle" transform="translate(64.000000, 29.000000) rotate(-45.000000) translate(-64.000000, -29.000000) " x="48" y="24" width="32" height="10" rx="5"></rect>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -1,6 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import closeIcon from 'assets/images/icon-x.svg';
|
import closeIcon from 'assets/images/icon-x.svg';
|
||||||
|
|
||||||
import './Modal.scss';
|
import './Modal.scss';
|
||||||
|
@ -18,6 +17,8 @@ type Props = {
|
||||||
| 'warning'
|
| 'warning'
|
||||||
| 'danger'
|
| 'danger'
|
||||||
| 'link',
|
| 'link',
|
||||||
|
disabled?: boolean,
|
||||||
|
// $FlowFixMe - Why the fuck doesn't this like onClick?
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
}[],
|
}[],
|
||||||
handleClose: () => void,
|
handleClose: () => void,
|
||||||
|
@ -26,27 +27,6 @@ type Props = {
|
||||||
|
|
||||||
export default class Modal extends Component {
|
export default class Modal extends Component {
|
||||||
props: Props;
|
props: Props;
|
||||||
static propTypes = {
|
|
||||||
isOpen: PropTypes.bool,
|
|
||||||
title: PropTypes.node.isRequired,
|
|
||||||
children: PropTypes.node,
|
|
||||||
buttons: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
text: PropTypes.node.isRequired,
|
|
||||||
type: PropTypes.oneOf([
|
|
||||||
'default',
|
|
||||||
'primary',
|
|
||||||
'success',
|
|
||||||
'info',
|
|
||||||
'warning',
|
|
||||||
'danger',
|
|
||||||
'link'
|
|
||||||
]),
|
|
||||||
onClick: PropTypes.func.isRequired
|
|
||||||
})
|
|
||||||
),
|
|
||||||
handleClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.updateBodyClass();
|
this.updateBodyClass();
|
||||||
|
@ -95,7 +75,12 @@ export default class Modal extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={btnClass} onClick={btn.onClick} key={idx}>
|
<button
|
||||||
|
className={btnClass}
|
||||||
|
onClick={btn.onClick}
|
||||||
|
key={idx}
|
||||||
|
disabled={btn.disabled}
|
||||||
|
>
|
||||||
{btn.text}
|
{btn.text}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
|
@ -7,6 +7,7 @@ $m-header-padding: 15px;
|
||||||
$m-header-height: 62px;
|
$m-header-height: 62px;
|
||||||
$m-content-padding: 20px;
|
$m-content-padding: 20px;
|
||||||
$m-footer-height: 58px;
|
$m-footer-height: 58px;
|
||||||
|
$m-close-size: 26px;
|
||||||
$m-anim-speed: 400ms;
|
$m-anim-speed: 400ms;
|
||||||
|
|
||||||
@keyframes modalshade-open {
|
@keyframes modalshade-open {
|
||||||
|
@ -23,11 +24,11 @@ $m-anim-speed: 400ms;
|
||||||
0%,
|
0%,
|
||||||
30% {
|
30% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translate(-50%, -50%) scale(0.88);
|
transform: translateX(-50%) scale(0.88);
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translate(-50%, -50%) scale(1);
|
transform: translateX(-50%) scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +50,7 @@ $m-anim-speed: 400ms;
|
||||||
|
|
||||||
.Modal {
|
.Modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50%;
|
top: 30px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
max-width: 95%;
|
max-width: 95%;
|
||||||
max-width: calc(100% - #{$m-window-padding * 2});
|
max-width: calc(100% - #{$m-window-padding * 2});
|
||||||
|
@ -57,7 +58,7 @@ $m-anim-speed: 400ms;
|
||||||
max-height: calc(100% - #{$m-window-padding * 2});
|
max-height: calc(100% - #{$m-window-padding * 2});
|
||||||
background: $m-background;
|
background: $m-background;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transform: translate(-50%, -50%);
|
transform: translateX(-50%);
|
||||||
z-index: $zindex-modal;
|
z-index: $zindex-modal;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -77,8 +78,13 @@ $m-anim-speed: 400ms;
|
||||||
|
|
||||||
&-title {
|
&-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
padding-right: $m-close-size;
|
||||||
font-size: $font-size-large;
|
font-size: $font-size-large;
|
||||||
line-height: $m-header-height;
|
line-height: $m-header-height;
|
||||||
|
height: $m-header-height;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-close {
|
&-close {
|
||||||
|
@ -86,8 +92,8 @@ $m-anim-speed: 400ms;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
right: $m-header-padding;
|
right: $m-header-padding;
|
||||||
height: 26px;
|
height: $m-close-size;
|
||||||
width: 26px;
|
width: $m-close-size;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
transform: translateY(-50%) translateZ(0);
|
transform: translateY(-50%) translateZ(0);
|
||||||
|
|
||||||
|
|
|
@ -140,6 +140,13 @@ export type NetworkConfig = {
|
||||||
contracts: ?Array<NetworkContract>
|
contracts: ?Array<NetworkContract>
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type NodeConfig = {
|
||||||
|
network: string,
|
||||||
|
lib: RPCNode,
|
||||||
|
service: string,
|
||||||
|
estimateGas: ?boolean
|
||||||
|
};
|
||||||
|
|
||||||
export const NETWORKS: { [key: string]: NetworkConfig } = {
|
export const NETWORKS: { [key: string]: NetworkConfig } = {
|
||||||
ETH: {
|
ETH: {
|
||||||
name: 'ETH',
|
name: 'ETH',
|
||||||
|
@ -159,7 +166,7 @@ export const NETWORKS: { [key: string]: NetworkConfig } = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NODES = {
|
export const NODES: { [key: string]: NodeConfig } = {
|
||||||
eth_mew: {
|
eth_mew: {
|
||||||
network: 'ETH',
|
network: 'ETH',
|
||||||
lib: new RPCNode('https://api.myetherapi.com/eth'),
|
lib: new RPCNode('https://api.myetherapi.com/eth'),
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import translate from "translations";
|
import translate from 'translations';
|
||||||
import UnitDropdown from "./UnitDropdown";
|
import UnitDropdown from './UnitDropdown';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
value: string,
|
value: string,
|
||||||
|
@ -19,23 +19,23 @@ export default class AmountField extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<label>
|
||||||
{translate("SEND_amount")}
|
{translate('SEND_amount')}
|
||||||
</label>
|
</label>
|
||||||
<div className="input-group col-sm-11">
|
<div className="input-group col-sm-11">
|
||||||
<input
|
<input
|
||||||
className={`form-control ${isFinite(Number(value)) &&
|
className={`form-control ${isFinite(Number(value)) &&
|
||||||
Number(value) > 0
|
Number(value) > 0
|
||||||
? "is-valid"
|
? 'is-valid'
|
||||||
: "is-invalid"}`}
|
: 'is-invalid'}`}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={translate("SEND_amount_short")}
|
placeholder={translate('SEND_amount_short')}
|
||||||
value={value}
|
value={value}
|
||||||
disabled={isReadonly}
|
disabled={isReadonly}
|
||||||
onChange={isReadonly ? void 0 : this.onValueChange}
|
onChange={isReadonly ? void 0 : this.onValueChange}
|
||||||
/>
|
/>
|
||||||
<UnitDropdown
|
<UnitDropdown
|
||||||
value={unit}
|
value={unit}
|
||||||
options={["ether"].concat(this.props.tokens)}
|
options={['ether'].concat(this.props.tokens)}
|
||||||
onChange={isReadonly ? void 0 : this.onUnitChange}
|
onChange={isReadonly ? void 0 : this.onUnitChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -43,7 +43,7 @@ export default class AmountField extends React.Component {
|
||||||
<p>
|
<p>
|
||||||
<a onClick={this.onSendEverything}>
|
<a onClick={this.onSendEverything}>
|
||||||
<span className="strong">
|
<span className="strong">
|
||||||
{translate("SEND_TransferTotal")}
|
{translate('SEND_TransferTotal')}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</p>}
|
</p>}
|
||||||
|
@ -65,7 +65,7 @@ export default class AmountField extends React.Component {
|
||||||
|
|
||||||
onSendEverything = () => {
|
onSendEverything = () => {
|
||||||
if (this.props.onChange) {
|
if (this.props.onChange) {
|
||||||
this.props.onChange("everything", this.props.unit);
|
this.props.onChange('everything', this.props.unit);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,218 @@
|
||||||
|
// @flow
|
||||||
|
import './ConfirmationModal.scss';
|
||||||
|
import React from 'react';
|
||||||
|
import translate from 'translations';
|
||||||
|
import Big from 'bignumber.js';
|
||||||
|
import EthTx from 'ethereumjs-tx';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import BaseWallet from 'libs/wallet/base';
|
||||||
|
import { toUnit, toTokenDisplay } from 'libs/units';
|
||||||
|
import ERC20 from 'libs/erc20';
|
||||||
|
import { getTransactionFields } from 'libs/transaction';
|
||||||
|
import { getTokens } from 'selectors/wallet';
|
||||||
|
import { getNetworkConfig } from 'selectors/config';
|
||||||
|
import type { NodeConfig } from 'config/data';
|
||||||
|
import type { Token, NetworkConfig } from 'config/data';
|
||||||
|
|
||||||
|
import Modal from 'components/ui/Modal';
|
||||||
|
import Identicon from 'components/ui/Identicon';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
signedTransaction: string,
|
||||||
|
transaction: EthTx,
|
||||||
|
wallet: BaseWallet,
|
||||||
|
node: NodeConfig,
|
||||||
|
token: ?Token,
|
||||||
|
network: NetworkConfig,
|
||||||
|
onConfirm: (string, EthTx) => void,
|
||||||
|
onCancel: () => void
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
fromAddress: string,
|
||||||
|
timeToRead: number
|
||||||
|
};
|
||||||
|
|
||||||
|
class ConfirmationModal extends React.Component {
|
||||||
|
props: Props;
|
||||||
|
state: State;
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
fromAddress: '',
|
||||||
|
timeToRead: 5
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(newProps: Props) {
|
||||||
|
// Reload address if the wallet changes
|
||||||
|
if (newProps.wallet !== this.props.wallet) {
|
||||||
|
this._setWalletAddress(this.props.wallet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count down 5 seconds before allowing them to confirm
|
||||||
|
readTimer = 0;
|
||||||
|
componentDidMount() {
|
||||||
|
this.readTimer = setInterval(() => {
|
||||||
|
if (this.state.timeToRead > 0) {
|
||||||
|
this.setState({ timeToRead: this.state.timeToRead - 1 });
|
||||||
|
} else {
|
||||||
|
clearInterval(this.readTimer);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
this._setWalletAddress(this.props.wallet);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
clearInterval(this.readTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
_setWalletAddress(wallet: BaseWallet) {
|
||||||
|
wallet.getAddress().then(fromAddress => {
|
||||||
|
this.setState({ fromAddress });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_decodeTransaction() {
|
||||||
|
const { transaction, token } = this.props;
|
||||||
|
const { to, value, data, gasPrice } = getTransactionFields(transaction);
|
||||||
|
let fixedValue;
|
||||||
|
let toAddress;
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
// $FlowFixMe - If you have a token prop, you have data
|
||||||
|
const tokenData = ERC20.$transfer(data);
|
||||||
|
fixedValue = toTokenDisplay(new Big(tokenData.value), token).toString();
|
||||||
|
toAddress = tokenData.to;
|
||||||
|
} else {
|
||||||
|
fixedValue = toUnit(new Big(value, 16), 'wei', 'ether').toString();
|
||||||
|
toAddress = to;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: fixedValue,
|
||||||
|
gasPrice: toUnit(new Big(gasPrice, 16), 'wei', 'gwei').toString(),
|
||||||
|
data,
|
||||||
|
toAddress
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
_confirm() {
|
||||||
|
if (this.state.timeToRead < 1) {
|
||||||
|
const { signedTransaction, transaction } = this.props;
|
||||||
|
this.props.onConfirm(signedTransaction, transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { node, token, network, onCancel } = this.props;
|
||||||
|
const { fromAddress, timeToRead } = this.state;
|
||||||
|
const { toAddress, value, gasPrice, data } = this._decodeTransaction();
|
||||||
|
|
||||||
|
const buttonPrefix = timeToRead > 0 ? `(${timeToRead}) ` : '';
|
||||||
|
const buttons = [
|
||||||
|
{
|
||||||
|
text: buttonPrefix + translate('SENDModal_Yes'),
|
||||||
|
type: 'primary',
|
||||||
|
disabled: timeToRead > 0,
|
||||||
|
onClick: this._confirm()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: translate('SENDModal_No'),
|
||||||
|
type: 'default',
|
||||||
|
onClick: onCancel
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const symbol = token ? token.symbol : network.unit;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title="Confirm Your Transaction"
|
||||||
|
buttons={buttons}
|
||||||
|
handleClose={onCancel}
|
||||||
|
isOpen={true}
|
||||||
|
>
|
||||||
|
<div className="ConfModal">
|
||||||
|
<div className="ConfModal-summary">
|
||||||
|
<div className="ConfModal-summary-icon ConfModal-summary-icon--from">
|
||||||
|
<Identicon size="100%" address={fromAddress} />
|
||||||
|
</div>
|
||||||
|
<div className="ConfModal-summary-amount">
|
||||||
|
<div className="ConfModal-summary-amount-arrow" />
|
||||||
|
<div className="ConfModal-summary-amount-currency">
|
||||||
|
{value} {symbol}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ConfModal-summary-icon ConfModal-summary-icon--to">
|
||||||
|
<Identicon size="100%" address={toAddress} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="ConfModal-details">
|
||||||
|
<li className="ConfModal-details-detail">
|
||||||
|
You are sending from <code>{fromAddress}</code>
|
||||||
|
</li>
|
||||||
|
<li className="ConfModal-details-detail">
|
||||||
|
You are sending to <code>{toAddress}</code>
|
||||||
|
</li>
|
||||||
|
<li className="ConfModal-details-detail">
|
||||||
|
You are sending{' '}
|
||||||
|
<strong>
|
||||||
|
{value} {symbol}
|
||||||
|
</strong>{' '}
|
||||||
|
with a gas price of <strong>{gasPrice} gwei</strong>
|
||||||
|
</li>
|
||||||
|
<li className="ConfModal-details-detail">
|
||||||
|
You are interacting with the <strong>{node.network}</strong>{' '}
|
||||||
|
network provided by <strong>{node.service}</strong>
|
||||||
|
</li>
|
||||||
|
{!token &&
|
||||||
|
<li className="ConfModal-details-detail">
|
||||||
|
{data
|
||||||
|
? <span>
|
||||||
|
You are sending the following data:{' '}
|
||||||
|
<textarea
|
||||||
|
className="form-control"
|
||||||
|
value={data}
|
||||||
|
rows="3"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
: 'There is no data attached to this transaction'}
|
||||||
|
</li>}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="ConfModal-confirm">
|
||||||
|
{translate('SENDModal_Content_3')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state, props) {
|
||||||
|
// Convert the signedTransaction to an EthTx transaction
|
||||||
|
const transaction = new EthTx(props.signedTransaction);
|
||||||
|
|
||||||
|
// Network config for defaults
|
||||||
|
const network = getNetworkConfig(state);
|
||||||
|
|
||||||
|
// Determine if we're sending to a token from the transaction to address
|
||||||
|
const { to, data } = getTransactionFields(transaction);
|
||||||
|
const tokens = getTokens(state);
|
||||||
|
const token = data && tokens.find(t => t.address === to);
|
||||||
|
|
||||||
|
return {
|
||||||
|
transaction,
|
||||||
|
token,
|
||||||
|
network
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(ConfirmationModal);
|
|
@ -0,0 +1,47 @@
|
||||||
|
@import "common/sass/variables";
|
||||||
|
|
||||||
|
$summary-height: 54px;
|
||||||
|
|
||||||
|
.ConfModal {
|
||||||
|
&-summary {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
|
||||||
|
&-icon {
|
||||||
|
height: $summary-height;
|
||||||
|
width: $summary-height;
|
||||||
|
margin: 0 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-amount {
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: center;
|
||||||
|
line-height: $summary-height / 2;
|
||||||
|
|
||||||
|
&-arrow {
|
||||||
|
display: inline-block;
|
||||||
|
height: $summary-height / 3;
|
||||||
|
width: 100%;
|
||||||
|
background-image: url('~assets/images/icon-dot-arrow.svg');
|
||||||
|
background-size: auto 100%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-currency {
|
||||||
|
color: $brand-danger;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-details {
|
||||||
|
padding-left: 30px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-confirm {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: $font-size-medium-bump;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
|
|
||||||
class Option extends React.Component {
|
class Option extends React.Component {
|
||||||
props: {
|
props: {
|
||||||
|
@ -11,7 +11,7 @@ class Option extends React.Component {
|
||||||
const { value, active } = this.props;
|
const { value, active } = this.props;
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
<a className={active ? "active" : ""} onClick={this.onChange}>
|
<a className={active ? 'active' : ''} onClick={this.onChange}>
|
||||||
{value}
|
{value}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -5,3 +5,4 @@ export { default as GasField } from './GasField';
|
||||||
export { default as CustomMessage } from './CustomMessage';
|
export { default as CustomMessage } from './CustomMessage';
|
||||||
export { default as AmountField } from './AmountField';
|
export { default as AmountField } from './AmountField';
|
||||||
export { default as AddressField } from './AddressField';
|
export { default as AddressField } from './AddressField';
|
||||||
|
export { default as ConfirmationModal } from './ConfirmationModal';
|
||||||
|
|
|
@ -9,7 +9,8 @@ import {
|
||||||
CustomMessage,
|
CustomMessage,
|
||||||
GasField,
|
GasField,
|
||||||
AmountField,
|
AmountField,
|
||||||
AddressField
|
AddressField,
|
||||||
|
ConfirmationModal
|
||||||
} from './components';
|
} from './components';
|
||||||
import { BalanceSidebar } from 'components';
|
import { BalanceSidebar } from 'components';
|
||||||
import pickBy from 'lodash/pickBy';
|
import pickBy from 'lodash/pickBy';
|
||||||
|
@ -38,10 +39,13 @@ import type {
|
||||||
BroadcastTransaction
|
BroadcastTransaction
|
||||||
} from 'libs/transaction';
|
} from 'libs/transaction';
|
||||||
import type { UNIT } from 'libs/units';
|
import type { UNIT } from 'libs/units';
|
||||||
import { toWei } from 'libs/units';
|
import { toWei, toTokenUnit } from 'libs/units';
|
||||||
import { formatGasLimit } from 'utils/formatters';
|
import { formatGasLimit } from 'utils/formatters';
|
||||||
import { showNotification } from 'actions/notifications';
|
import { showNotification } from 'actions/notifications';
|
||||||
import type { ShowNotificationAction } from 'actions/notifications';
|
import type { ShowNotificationAction } from 'actions/notifications';
|
||||||
|
import type { NodeConfig } from 'config/data';
|
||||||
|
import { getNodeConfig } from 'selectors/config';
|
||||||
|
import { generateTransaction } from 'libs/transaction';
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
hasQueryString: boolean,
|
hasQueryString: boolean,
|
||||||
|
@ -50,10 +54,12 @@ type State = {
|
||||||
value: string,
|
value: string,
|
||||||
// $FlowFixMe - Comes from getParam not validating unit
|
// $FlowFixMe - Comes from getParam not validating unit
|
||||||
unit: UNIT,
|
unit: UNIT,
|
||||||
|
token: ?Token,
|
||||||
gasLimit: string,
|
gasLimit: string,
|
||||||
data: string,
|
data: string,
|
||||||
gasChanged: boolean,
|
gasChanged: boolean,
|
||||||
transaction: ?BroadcastTransaction
|
transaction: ?BroadcastTransaction,
|
||||||
|
showTxConfirm: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
function getParam(query: { [string]: string }, key: string) {
|
function getParam(query: { [string]: string }, key: string) {
|
||||||
|
@ -77,6 +83,7 @@ type Props = {
|
||||||
},
|
},
|
||||||
wallet: BaseWallet,
|
wallet: BaseWallet,
|
||||||
balance: Big,
|
balance: Big,
|
||||||
|
node: NodeConfig,
|
||||||
nodeLib: RPCNode,
|
nodeLib: RPCNode,
|
||||||
network: NetworkConfig,
|
network: NetworkConfig,
|
||||||
tokens: Token[],
|
tokens: Token[],
|
||||||
|
@ -98,9 +105,11 @@ export class SendTransaction extends React.Component {
|
||||||
to: '',
|
to: '',
|
||||||
value: '',
|
value: '',
|
||||||
unit: 'ether',
|
unit: 'ether',
|
||||||
|
token: null,
|
||||||
gasLimit: '21000',
|
gasLimit: '21000',
|
||||||
data: '',
|
data: '',
|
||||||
gasChanged: false,
|
gasChanged: false,
|
||||||
|
showTxConfirm: false,
|
||||||
transaction: null
|
transaction: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -141,13 +150,11 @@ export class SendTransaction extends React.Component {
|
||||||
data,
|
data,
|
||||||
readOnly,
|
readOnly,
|
||||||
hasQueryString,
|
hasQueryString,
|
||||||
|
showTxConfirm,
|
||||||
transaction
|
transaction
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const customMessage = customMessages.find(m => m.to === to);
|
const customMessage = customMessages.find(m => m.to === to);
|
||||||
|
|
||||||
// tokens
|
|
||||||
// ng-show="token.balance!=0 && token.balance!='loading' || token.type!=='default' || tokenVisibility=='shown'"
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="container" style={{ minHeight: '50%' }}>
|
<section className="container" style={{ minHeight: '50%' }}>
|
||||||
<div className="tab-content">
|
<div className="tab-content">
|
||||||
|
@ -227,52 +234,55 @@ export class SendTransaction extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="row form-group">
|
{transaction &&
|
||||||
<div className="col-sm-6">
|
<div>
|
||||||
<label>
|
<div className="row form-group">
|
||||||
{translate('SEND_raw')}
|
<div className="col-sm-6">
|
||||||
</label>
|
<label>
|
||||||
<textarea
|
{translate('SEND_raw')}
|
||||||
className="form-control"
|
</label>
|
||||||
value={transaction ? transaction.rawTx : ''}
|
<textarea
|
||||||
rows="4"
|
className="form-control"
|
||||||
readOnly
|
value={transaction.rawTx}
|
||||||
/>
|
rows="4"
|
||||||
</div>
|
readOnly
|
||||||
<div className="col-sm-6">
|
/>
|
||||||
<label>
|
</div>
|
||||||
{translate('SEND_signed')}
|
<div className="col-sm-6">
|
||||||
</label>
|
<label>
|
||||||
<textarea
|
{translate('SEND_signed')}
|
||||||
className="form-control"
|
</label>
|
||||||
value={transaction ? transaction.signedTx : ''}
|
<textarea
|
||||||
rows="4"
|
className="form-control"
|
||||||
readOnly
|
value={transaction.signedTx}
|
||||||
/>
|
rows="4"
|
||||||
</div>
|
readOnly
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<a
|
<a
|
||||||
className="btn btn-primary btn-block col-sm-11"
|
className="btn btn-primary btn-block col-sm-11"
|
||||||
data-toggle="modal"
|
onClick={this.openTxModal}
|
||||||
data-target="#sendTransaction"
|
>
|
||||||
>
|
{translate('SEND_trans')}
|
||||||
{translate('SEND_trans')}
|
</a>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
</div>}
|
||||||
</section>
|
</section>
|
||||||
{'' /* <!-- / Content --> */}
|
|
||||||
{
|
|
||||||
'' /* @@if (site === 'mew' ) { @@include( './sendTx-content.tpl', { "site": "mew" } ) }
|
|
||||||
@@if (site === 'cx' ) { @@include( './sendTx-content.tpl', { "site": "cx" } ) }
|
|
||||||
|
|
||||||
@@if (site === 'mew' ) { @@include( './sendTx-modal.tpl', { "site": "mew" } ) }
|
|
||||||
@@if (site === 'cx' ) { @@include( './sendTx-modal.tpl', { "site": "cx" } ) } */
|
|
||||||
}
|
|
||||||
</article>}
|
</article>}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
{transaction &&
|
||||||
|
showTxConfirm &&
|
||||||
|
<ConfirmationModal
|
||||||
|
wallet={this.props.wallet}
|
||||||
|
node={this.props.node}
|
||||||
|
signedTransaction={transaction.signedTx}
|
||||||
|
onCancel={this.cancelTx}
|
||||||
|
onConfirm={this.confirmTx}
|
||||||
|
/>}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -304,34 +314,36 @@ export class SendTransaction extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTransactionFromState(): Promise<TransactionWithoutGas> {
|
async getTransactionInfoFromState(): Promise<TransactionWithoutGas> {
|
||||||
const { wallet } = this.props;
|
const { wallet } = this.props;
|
||||||
|
const { token } = this.state;
|
||||||
|
|
||||||
if (this.state.unit === 'ether') {
|
if (this.state.unit === 'ether') {
|
||||||
return {
|
return {
|
||||||
to: this.state.to,
|
to: this.state.to,
|
||||||
from: await wallet.getAddress(),
|
from: await wallet.getAddress(),
|
||||||
value: valueToHex(this.state.value)
|
value: valueToHex(this.state.value),
|
||||||
|
data: this.state.data
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('No matching token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
to: token.address,
|
||||||
|
from: await wallet.getAddress(),
|
||||||
|
value: '0x0',
|
||||||
|
data: ERC20.transfer(
|
||||||
|
this.state.to,
|
||||||
|
toTokenUnit(new Big(this.state.value), token)
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const token = this.props.tokens.find(x => x.symbol === this.state.unit);
|
|
||||||
if (!token) {
|
|
||||||
throw new Error('No matching token');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
to: token.address,
|
|
||||||
from: await wallet.getAddress(),
|
|
||||||
value: '0x0',
|
|
||||||
data: ERC20.transfer(
|
|
||||||
this.state.to,
|
|
||||||
new Big(this.state.value).times(new Big(10).pow(token.decimal))
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async estimateGas() {
|
async estimateGas() {
|
||||||
const trans = await this.getTransactionFromState();
|
const trans = await this.getTransactionInfoFromState();
|
||||||
if (!trans) {
|
if (!trans) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -399,28 +411,32 @@ export class SendTransaction extends React.Component {
|
||||||
}
|
}
|
||||||
value = token.balance.toString();
|
value = token.balance.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let token = this.props.tokens.find(x => x.symbol === unit);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
value,
|
value,
|
||||||
unit
|
unit,
|
||||||
|
token
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
generateTx = async () => {
|
generateTx = async () => {
|
||||||
const { nodeLib, wallet } = this.props;
|
const { nodeLib, wallet } = this.props;
|
||||||
const address = await wallet.getAddress();
|
const { token } = this.state;
|
||||||
|
const stateTxInfo = await this.getTransactionInfoFromState();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const transaction = await nodeLib.generateTransaction(
|
const transaction = await generateTransaction(
|
||||||
|
nodeLib,
|
||||||
{
|
{
|
||||||
to: this.state.to,
|
...stateTxInfo,
|
||||||
from: address,
|
|
||||||
value: this.state.value,
|
|
||||||
gasLimit: this.state.gasLimit,
|
gasLimit: this.state.gasLimit,
|
||||||
gasPrice: this.props.gasPrice,
|
gasPrice: this.props.gasPrice,
|
||||||
data: this.state.data,
|
|
||||||
chainId: this.props.network.chainId
|
chainId: this.props.network.chainId
|
||||||
},
|
},
|
||||||
wallet
|
wallet,
|
||||||
|
token
|
||||||
);
|
);
|
||||||
|
|
||||||
this.setState({ transaction });
|
this.setState({ transaction });
|
||||||
|
@ -428,6 +444,21 @@ export class SendTransaction extends React.Component {
|
||||||
this.props.showNotification('danger', err.message, 5000);
|
this.props.showNotification('danger', err.message, 5000);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
openTxModal = () => {
|
||||||
|
if (this.state.transaction) {
|
||||||
|
this.setState({ showTxConfirm: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
cancelTx = () => {
|
||||||
|
this.setState({ showTxConfirm: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
confirmTx = () => {
|
||||||
|
// TODO: Broadcast transaction
|
||||||
|
console.log(this.state.transaction);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps(state: AppState) {
|
function mapStateToProps(state: AppState) {
|
||||||
|
@ -435,6 +466,7 @@ function mapStateToProps(state: AppState) {
|
||||||
wallet: state.wallet.inst,
|
wallet: state.wallet.inst,
|
||||||
balance: state.wallet.balance,
|
balance: state.wallet.balance,
|
||||||
tokenBalances: getTokenBalances(state),
|
tokenBalances: getTokenBalances(state),
|
||||||
|
node: getNodeConfig(state),
|
||||||
nodeLib: getNodeLib(state),
|
nodeLib: getNodeLib(state),
|
||||||
network: getNetworkConfig(state),
|
network: getNetworkConfig(state),
|
||||||
tokens: getTokens(state),
|
tokens: getTokens(state),
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
// @flow
|
// @flow
|
||||||
// TODO support events, constructors, fallbacks, array slots, types
|
// TODO support events, constructors, fallbacks, array slots, types
|
||||||
import { sha3, setLengthLeft, toBuffer } from 'ethereumjs-util';
|
import abi from 'ethereumjs-abi';
|
||||||
import Big from 'bignumber.js';
|
|
||||||
|
|
||||||
type ABIType = 'address' | 'uint256' | 'bool';
|
// There are too many to enumerate since they're somewhat dynamic, list here
|
||||||
|
// https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI#types
|
||||||
|
type ABIType = string;
|
||||||
|
|
||||||
type ABITypedSlot = {
|
type ABITypedSlot = {
|
||||||
name: string,
|
name: string,
|
||||||
|
@ -22,16 +23,17 @@ type ABIMethod = {
|
||||||
|
|
||||||
export type ABI = ABIMethod[];
|
export type ABI = ABIMethod[];
|
||||||
|
|
||||||
function assertString(arg: any) {
|
export type DecodedCall = {
|
||||||
if (typeof arg !== 'string') {
|
method: ABIMethod,
|
||||||
throw new Error('Expected string');
|
// TODO: Type this to be an array of BNs when we switch
|
||||||
}
|
args: Array<any>
|
||||||
}
|
};
|
||||||
|
|
||||||
// Contract helper, returns data for given call
|
// Contract helper, returns data for given call
|
||||||
export default class Contract {
|
export default class Contract {
|
||||||
abi: ABI;
|
abi: ABI;
|
||||||
constructor(abi: ABI) {
|
constructor(abi: ABI) {
|
||||||
|
// TODO: Check ABI, throw if it's malformed
|
||||||
this.abi = abi;
|
this.abi = abi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,18 +48,37 @@ export default class Contract {
|
||||||
return method;
|
return method;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getMethodTypes(method: ABIMethod): string[] {
|
||||||
|
return method.inputs.map(i => i.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMethodSelector(method: ABIMethod): string {
|
||||||
|
return abi
|
||||||
|
.methodID(method.name, this.getMethodTypes(method))
|
||||||
|
.toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
call(name: string, args: any[]): string {
|
call(name: string, args: any[]): string {
|
||||||
const method = this.getMethodAbi(name);
|
const method = this.getMethodAbi(name);
|
||||||
const selector = sha3(
|
|
||||||
`${name}(${method.inputs.map(i => i.type).join(',')})`
|
return (
|
||||||
|
'0x' + this.getMethodSelector(method) + this.encodeArgs(method, args)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$call(data: string): DecodedCall {
|
||||||
|
const method = this.abi.find(
|
||||||
|
mth => data.indexOf(this.getMethodSelector(mth)) !== -1
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: Add explanation, why slice the first 8?
|
if (!method) {
|
||||||
return (
|
throw new Error('Unknown method');
|
||||||
'0x' +
|
}
|
||||||
selector.toString('hex').slice(0, 8) +
|
|
||||||
this.encodeArgs(method, args)
|
return {
|
||||||
);
|
method,
|
||||||
|
args: this.decodeArgs(method, data)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
encodeArgs(method: ABIMethod, args: any[]): string {
|
encodeArgs(method: ABIMethod, args: any[]): string {
|
||||||
|
@ -65,25 +86,17 @@ export default class Contract {
|
||||||
throw new Error('Invalid number of arguments');
|
throw new Error('Invalid number of arguments');
|
||||||
}
|
}
|
||||||
|
|
||||||
return method.inputs
|
const inputTypes = method.inputs.map(input => input.type);
|
||||||
.map((input, idx) => this.encodeArg(input, args[idx]))
|
return abi.rawEncode(inputTypes, args).toString('hex');
|
||||||
.join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
encodeArg(input: ABITypedSlot, arg: any): string {
|
// TODO: Type this return to be an array of BNs when we switch
|
||||||
switch (input.type) {
|
decodeArgs(method: ABIMethod, argData: string): Array<any> {
|
||||||
case 'address':
|
// Remove method selector from data, if present
|
||||||
case 'uint160':
|
argData = argData.replace(`0x${this.getMethodSelector(method)}`, '');
|
||||||
assertString(arg);
|
// Convert argdata to a hex buffer for ethereumjs-abi
|
||||||
return setLengthLeft(toBuffer(arg), 32).toString('hex');
|
const argBuffer = new Buffer(argData, 'hex');
|
||||||
case 'uint256':
|
// Decode!
|
||||||
if (arg instanceof Big) {
|
return abi.rawDecode(this.getMethodTypes(method), argBuffer);
|
||||||
arg = '0x' + arg.toString(16);
|
|
||||||
}
|
|
||||||
assertString(arg);
|
|
||||||
return setLengthLeft(toBuffer(arg), 32).toString('hex');
|
|
||||||
default:
|
|
||||||
throw new Error(`Dont know how to handle abi type ${input.type}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
// @flow
|
// @flow
|
||||||
import Contract from 'libs/contract';
|
import Contract from 'libs/contract';
|
||||||
import type { ABI } from 'libs/contract';
|
import type { ABI } from 'libs/contract';
|
||||||
import type Big from 'bignumber.js';
|
import Big from 'bignumber.js';
|
||||||
|
import { toChecksumAddress } from 'ethereumjs-util';
|
||||||
|
|
||||||
|
type Transfer = {
|
||||||
|
to: string,
|
||||||
|
value: string
|
||||||
|
};
|
||||||
|
|
||||||
const erc20Abi: ABI = [
|
const erc20Abi: ABI = [
|
||||||
{
|
{
|
||||||
|
@ -51,12 +57,20 @@ class ERC20 extends Contract {
|
||||||
super(erc20Abi);
|
super(erc20Abi);
|
||||||
}
|
}
|
||||||
|
|
||||||
balanceOf(address: string) {
|
balanceOf(address: string): string {
|
||||||
return this.call('balanceOf', [address]);
|
return this.call('balanceOf', [address]);
|
||||||
}
|
}
|
||||||
|
|
||||||
transfer(to: string, value: Big) {
|
transfer(to: string, value: Big): string {
|
||||||
return this.call('transfer', [to, value]);
|
return this.call('transfer', [to, value.toString()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$transfer(data: string): Transfer {
|
||||||
|
const decodedArgs = this.decodeArgs(this.getMethodAbi('transfer'), data);
|
||||||
|
return {
|
||||||
|
to: toChecksumAddress(`0x${decodedArgs[0].toString(16)}`),
|
||||||
|
value: decodedArgs[1].toString(10)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
// @flow
|
// @flow
|
||||||
import Big from 'bignumber.js';
|
import Big from 'bignumber.js';
|
||||||
import type {
|
import type { TransactionWithoutGas } from 'libs/transaction';
|
||||||
TransactionWithoutGas,
|
|
||||||
Transaction,
|
|
||||||
BroadcastTransaction
|
|
||||||
} from 'libs/transaction';
|
|
||||||
import type { Token } from 'config/data';
|
import type { Token } from 'config/data';
|
||||||
import type { BaseWallet } from 'libs/wallet';
|
|
||||||
|
|
||||||
export default class BaseNode {
|
export default class BaseNode {
|
||||||
async getBalance(_address: string): Promise<Big> {
|
async getBalance(_address: string): Promise<Big> {
|
||||||
throw new Error('Implement me');
|
throw new Error('Implement me');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTokenBalance(_address: string, _token: Token): Promise<Big> {
|
||||||
|
throw new Error('Implement me');
|
||||||
|
}
|
||||||
|
|
||||||
async getTokenBalances(_address: string, _tokens: Token[]): Promise<Big[]> {
|
async getTokenBalances(_address: string, _tokens: Token[]): Promise<Big[]> {
|
||||||
throw new Error('Implement me');
|
throw new Error('Implement me');
|
||||||
}
|
}
|
||||||
|
@ -21,10 +20,7 @@ export default class BaseNode {
|
||||||
throw new Error('Implement me');
|
throw new Error('Implement me');
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateTransaction(
|
async getTransactionCount(_address: string): Promise<string> {
|
||||||
_tx: Transaction,
|
|
||||||
_wallet: BaseWallet
|
|
||||||
): Promise<BroadcastTransaction> {
|
|
||||||
throw new Error('Implement me');
|
throw new Error('Implement me');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
|
import ERC20 from 'libs/erc20';
|
||||||
import { hexEncodeData } from './utils';
|
import { hexEncodeData } from './utils';
|
||||||
import type {
|
import type {
|
||||||
RPCRequest,
|
RPCRequest,
|
||||||
JsonRpcResponse,
|
JsonRpcResponse,
|
||||||
CallRequest,
|
CallRequest,
|
||||||
GetBalanceRequest,
|
GetBalanceRequest,
|
||||||
|
GetTokenBalanceRequest,
|
||||||
EstimateGasRequest,
|
EstimateGasRequest,
|
||||||
GetTransactionCountRequest
|
GetTransactionCountRequest
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import type { Token } from 'config/data';
|
||||||
|
|
||||||
// FIXME is it safe to generate that much entropy?
|
// FIXME is it safe to generate that much entropy?
|
||||||
function id(): string {
|
function id(): string {
|
||||||
|
@ -53,6 +56,24 @@ export function getTransactionCount(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTokenBalance(
|
||||||
|
address: string,
|
||||||
|
token: Token
|
||||||
|
): GetTokenBalanceRequest {
|
||||||
|
return {
|
||||||
|
id: id(),
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
method: 'eth_call',
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
to: token.address,
|
||||||
|
data: ERC20.balanceOf(address)
|
||||||
|
},
|
||||||
|
'pending'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default class RPCClient {
|
export default class RPCClient {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
constructor(endpoint: string) {
|
constructor(endpoint: string) {
|
||||||
|
|
|
@ -1,24 +1,14 @@
|
||||||
// @flow
|
// @flow
|
||||||
import Big from 'bignumber.js';
|
import Big from 'bignumber.js';
|
||||||
import { addHexPrefix } from 'ethereumjs-util';
|
|
||||||
import translate from 'translations';
|
|
||||||
import BaseNode from '../base';
|
import BaseNode from '../base';
|
||||||
import type {
|
import type { TransactionWithoutGas } from 'libs/transaction';
|
||||||
TransactionWithoutGas,
|
|
||||||
Transaction,
|
|
||||||
BroadcastTransaction
|
|
||||||
} from 'libs/transaction';
|
|
||||||
import RPCClient, {
|
import RPCClient, {
|
||||||
getBalance,
|
getBalance,
|
||||||
estimateGas,
|
estimateGas,
|
||||||
ethCall,
|
getTransactionCount,
|
||||||
getTransactionCount
|
getTokenBalance
|
||||||
} from './client';
|
} from './client';
|
||||||
import type { Token } from 'config/data';
|
import type { Token } from 'config/data';
|
||||||
import ERC20 from 'libs/erc20';
|
|
||||||
import type { BaseWallet } from 'libs/wallet';
|
|
||||||
import { toWei } from 'libs/units';
|
|
||||||
import { isValidETHAddress } from 'libs/validators';
|
|
||||||
|
|
||||||
export default class RpcNode extends BaseNode {
|
export default class RpcNode extends BaseNode {
|
||||||
client: RPCClient;
|
client: RPCClient;
|
||||||
|
@ -45,17 +35,20 @@ export default class RpcNode extends BaseNode {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getTokenBalance(address: string, token: Token): Promise<Big> {
|
||||||
|
return this.client.call(getTokenBalance(address, token)).then(response => {
|
||||||
|
if (response.error) {
|
||||||
|
return Big(0);
|
||||||
|
}
|
||||||
|
return new Big(Number(response.result)).div(
|
||||||
|
new Big(10).pow(token.decimal)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getTokenBalances(address: string, tokens: Token[]): Promise<Big[]> {
|
async getTokenBalances(address: string, tokens: Token[]): Promise<Big[]> {
|
||||||
const data = ERC20.balanceOf(address);
|
|
||||||
return this.client
|
return this.client
|
||||||
.batch(
|
.batch(tokens.map(t => getTokenBalance(address, t)))
|
||||||
tokens.map(t =>
|
|
||||||
ethCall({
|
|
||||||
to: t.address,
|
|
||||||
data
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.then(response => {
|
.then(response => {
|
||||||
return response.map((item, idx) => {
|
return response.map((item, idx) => {
|
||||||
// FIXME wrap in maybe-like
|
// FIXME wrap in maybe-like
|
||||||
|
@ -69,87 +62,12 @@ export default class RpcNode extends BaseNode {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateTransaction(
|
async getTransactionCount(address: string): Promise<string> {
|
||||||
tx: Transaction,
|
return this.client.call(getTransactionCount(address)).then(response => {
|
||||||
wallet: BaseWallet
|
if (response.error) {
|
||||||
): Promise<BroadcastTransaction> {
|
throw new Error('getTransactionCount error');
|
||||||
// Reject bad addresses
|
|
||||||
if (!isValidETHAddress(tx.to)) {
|
|
||||||
return Promise.reject(new Error(translate('ERROR_5')));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject gas limit under 21000 (Minimum for transaction)
|
|
||||||
// Reject if limit over 5000000
|
|
||||||
// TODO: Make this dynamic, the limit shifts
|
|
||||||
const limitBig = new Big(tx.gasLimit);
|
|
||||||
if (limitBig.lessThan(21000)) {
|
|
||||||
return Promise.reject(
|
|
||||||
new Error(
|
|
||||||
translate('Gas limit must be at least 21000 for transactions')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (limitBig.greaterThan(5000000)) {
|
|
||||||
return Promise.reject(new Error(translate('GETH_GasLimit')));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject gas over 1000gwei (1000000000000)
|
|
||||||
const priceBig = new Big(tx.gasPrice);
|
|
||||||
if (priceBig.greaterThan(new Big('1000000000000'))) {
|
|
||||||
return Promise.reject(
|
|
||||||
new Error(
|
|
||||||
'Gas price too high. Please contact support if this was not a mistake.'
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const calls = [getBalance(tx.from), getTransactionCount(tx.from)];
|
|
||||||
|
|
||||||
return this.client.batch(calls).then(async results => {
|
|
||||||
const [balance, txCount] = results;
|
|
||||||
|
|
||||||
if (balance.error) {
|
|
||||||
throw new Error(`Failed to retrieve balance for ${tx.from}`);
|
|
||||||
}
|
}
|
||||||
|
return response.result;
|
||||||
if (txCount.error) {
|
|
||||||
throw new Error(`Failed to retrieve transaction count for ${tx.from}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Handle token values
|
|
||||||
const valueWei = new Big(toWei(new Big(tx.value), 'ether'));
|
|
||||||
const balanceWei = new Big(balance.result);
|
|
||||||
if (valueWei.gte(balanceWei)) {
|
|
||||||
throw new Error(translate('GETH_Balance'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawTx = {
|
|
||||||
nonce: addHexPrefix(txCount.result),
|
|
||||||
gasPrice: addHexPrefix(new Big(tx.gasPrice).toString(16)),
|
|
||||||
gasLimit: addHexPrefix(new Big(tx.gasLimit).toString(16)),
|
|
||||||
to: addHexPrefix(tx.to),
|
|
||||||
value: addHexPrefix(valueWei.toString(16)),
|
|
||||||
data: tx.data ? addHexPrefix(tx.data) : '',
|
|
||||||
chainId: tx.chainId || 1
|
|
||||||
};
|
|
||||||
|
|
||||||
const rawTxJson = JSON.stringify(rawTx);
|
|
||||||
const signedTx = await wallet.signRawTransaction(rawTx);
|
|
||||||
|
|
||||||
// Repeat all of this shit for Flow typechecking. Sealed objects don't
|
|
||||||
// like spreads, so we have to be explicit.
|
|
||||||
return {
|
|
||||||
nonce: rawTx.nonce,
|
|
||||||
gasPrice: rawTx.gasPrice,
|
|
||||||
gasLimit: rawTx.gasLimit,
|
|
||||||
to: rawTx.to,
|
|
||||||
value: rawTx.value,
|
|
||||||
data: rawTx.data,
|
|
||||||
chainId: rawTx.chainId,
|
|
||||||
rawTx: rawTxJson,
|
|
||||||
signedTx: signedTx
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import type { TransactionWithoutGas } from 'libs/transaction';
|
||||||
|
|
||||||
type DATA = string;
|
type DATA = string;
|
||||||
type QUANTITY = string;
|
type QUANTITY = string;
|
||||||
|
@ -30,6 +31,17 @@ export type GetBalanceRequest = RPCRequestBase & {
|
||||||
params: [DATA, DEFAULT_BLOCK]
|
params: [DATA, DEFAULT_BLOCK]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetTokenBalanceRequest = RPCRequestBase & {
|
||||||
|
method: 'eth_call',
|
||||||
|
params: [
|
||||||
|
{
|
||||||
|
to: string,
|
||||||
|
data: string
|
||||||
|
},
|
||||||
|
DEFAULT_BLOCK
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
export type CallRequest = RPCRequestBase & {
|
export type CallRequest = RPCRequestBase & {
|
||||||
method: 'eth_call',
|
method: 'eth_call',
|
||||||
params: [
|
params: [
|
||||||
|
@ -47,16 +59,7 @@ export type CallRequest = RPCRequestBase & {
|
||||||
|
|
||||||
export type EstimateGasRequest = RPCRequestBase & {
|
export type EstimateGasRequest = RPCRequestBase & {
|
||||||
method: 'eth_estimateGas',
|
method: 'eth_estimateGas',
|
||||||
params: [
|
params: [TransactionWithoutGas]
|
||||||
{
|
|
||||||
from?: DATA,
|
|
||||||
to?: DATA,
|
|
||||||
gas?: QUANTITY,
|
|
||||||
gasPrice?: QUANTITY,
|
|
||||||
value?: QUANTITY,
|
|
||||||
data?: DATA
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetTransactionCountRequest = RPCRequestBase & {
|
export type GetTransactionCountRequest = RPCRequestBase & {
|
||||||
|
@ -66,6 +69,7 @@ export type GetTransactionCountRequest = RPCRequestBase & {
|
||||||
|
|
||||||
export type RPCRequest =
|
export type RPCRequest =
|
||||||
| GetBalanceRequest
|
| GetBalanceRequest
|
||||||
|
| GetTokenBalanceRequest
|
||||||
| CallRequest
|
| CallRequest
|
||||||
| EstimateGasRequest
|
| EstimateGasRequest
|
||||||
| GetTransactionCountRequest;
|
| GetTransactionCountRequest;
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import Big from 'bignumber.js';
|
||||||
|
import translate from 'translations';
|
||||||
|
import { addHexPrefix, toChecksumAddress } from 'ethereumjs-util';
|
||||||
|
import { isValidETHAddress } from 'libs/validators';
|
||||||
|
import ERC20 from 'libs/erc20';
|
||||||
|
import { toTokenUnit } from 'libs/units';
|
||||||
|
import type BaseNode from 'libs/nodes/base';
|
||||||
|
import type { BaseWallet } from 'libs/wallet';
|
||||||
|
import type { Token } from 'config/data';
|
||||||
|
import type EthTx from 'ethereumjs-tx';
|
||||||
|
|
||||||
|
// TODO: Enforce more bigs, or find better way to avoid ether vs wei for value
|
||||||
export type TransactionWithoutGas = {|
|
export type TransactionWithoutGas = {|
|
||||||
from: string,
|
from: string,
|
||||||
to: string,
|
to: string,
|
||||||
|
@ -29,3 +40,112 @@ export type BroadcastTransaction = {|
|
||||||
rawTx: string,
|
rawTx: string,
|
||||||
signedTx: string
|
signedTx: string
|
||||||
|};
|
|};
|
||||||
|
|
||||||
|
// Get useable fields from an EthTx object.
|
||||||
|
export function getTransactionFields(tx: EthTx) {
|
||||||
|
// For some crazy reason, toJSON spits out an array, not keyed values.
|
||||||
|
const [nonce, gasPrice, gasLimit, to, value, data, v, r, s] = tx.toJSON();
|
||||||
|
|
||||||
|
return {
|
||||||
|
// No value comes back as '0x', but most things expect '0x0'
|
||||||
|
value: value === '0x' ? '0x0' : value,
|
||||||
|
// If data is 0x, it might as well not be there
|
||||||
|
data: data === '0x' ? null : data,
|
||||||
|
// To address is unchecksummed, which could cause mismatches in comparisons
|
||||||
|
to: toChecksumAddress(to),
|
||||||
|
// Everything else is as-is
|
||||||
|
nonce,
|
||||||
|
gasPrice,
|
||||||
|
gasLimit,
|
||||||
|
v,
|
||||||
|
r,
|
||||||
|
s
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateTransaction(
|
||||||
|
node: BaseNode,
|
||||||
|
tx: Transaction,
|
||||||
|
wallet: BaseWallet,
|
||||||
|
token: ?Token
|
||||||
|
): Promise<BroadcastTransaction> {
|
||||||
|
// Reject bad addresses
|
||||||
|
if (!isValidETHAddress(tx.to)) {
|
||||||
|
throw new Error(translate('ERROR_5'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject token transactions without data
|
||||||
|
if (token && !tx.data) {
|
||||||
|
throw new Error('Tokens must be sent with data');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject gas limit under 21000 (Minimum for transaction)
|
||||||
|
// Reject if limit over 5000000
|
||||||
|
// TODO: Make this dynamic, the limit shifts
|
||||||
|
const limitBig = new Big(tx.gasLimit);
|
||||||
|
if (limitBig.lessThan(21000)) {
|
||||||
|
throw new Error(
|
||||||
|
translate('Gas limit must be at least 21000 for transactions')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (limitBig.greaterThan(5000000)) {
|
||||||
|
throw new Error(translate('GETH_GasLimit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject gas over 1000gwei (1000000000000)
|
||||||
|
const gasPriceBig = new Big(tx.gasPrice);
|
||||||
|
if (gasPriceBig.greaterThan(new Big('1000000000000'))) {
|
||||||
|
throw new Error(
|
||||||
|
'Gas price too high. Please contact support if this was not a mistake.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure their balance exceeds the amount they're sending
|
||||||
|
// TODO: Include gas price too, tokens should probably check ETH too
|
||||||
|
let value;
|
||||||
|
let balance;
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
// $FlowFixMe - We reject above if tx has no data for token
|
||||||
|
value = new Big(ERC20.$transfer(tx.data).value);
|
||||||
|
balance = toTokenUnit(await node.getTokenBalance(tx.from, token), token);
|
||||||
|
} else {
|
||||||
|
value = new Big(tx.value);
|
||||||
|
balance = await node.getBalance(tx.from);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.gte(balance)) {
|
||||||
|
throw new Error(translate('GETH_Balance'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the raw transaction
|
||||||
|
const txCount = await node.getTransactionCount(tx.from);
|
||||||
|
const rawTx = {
|
||||||
|
nonce: addHexPrefix(txCount),
|
||||||
|
gasPrice: addHexPrefix(new Big(tx.gasPrice).toString(16)),
|
||||||
|
gasLimit: addHexPrefix(new Big(tx.gasLimit).toString(16)),
|
||||||
|
to: addHexPrefix(tx.to),
|
||||||
|
value: token ? '0x0' : addHexPrefix(value.toString(16)),
|
||||||
|
data: tx.data ? addHexPrefix(tx.data) : '',
|
||||||
|
chainId: tx.chainId || 1
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sign the transaction
|
||||||
|
const rawTxJson = JSON.stringify(rawTx);
|
||||||
|
const signedTx = await wallet.signRawTransaction(rawTx);
|
||||||
|
|
||||||
|
// Repeat all of this shit for Flow typechecking. Sealed objects don't
|
||||||
|
// like spreads, so we have to be explicit.
|
||||||
|
return {
|
||||||
|
nonce: rawTx.nonce,
|
||||||
|
gasPrice: rawTx.gasPrice,
|
||||||
|
gasLimit: rawTx.gasLimit,
|
||||||
|
to: rawTx.to,
|
||||||
|
value: rawTx.value,
|
||||||
|
data: rawTx.data,
|
||||||
|
chainId: rawTx.chainId,
|
||||||
|
rawTx: rawTxJson,
|
||||||
|
signedTx: signedTx
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
import Big from 'bignumber.js';
|
import Big from 'bignumber.js';
|
||||||
|
import type { Token } from 'config/data';
|
||||||
|
|
||||||
const UNITS = {
|
const UNITS = {
|
||||||
wei: '1',
|
wei: '1',
|
||||||
|
@ -35,10 +35,18 @@ function getValueOfUnit(unit: UNIT) {
|
||||||
return new Big(UNITS[unit]);
|
return new Big(UNITS[unit]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toEther(number: Big, unit: UNIT) {
|
|
||||||
return toWei(number, unit).div(getValueOfUnit('ether'));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toWei(number: Big, unit: UNIT): Big {
|
export function toWei(number: Big, unit: UNIT): Big {
|
||||||
return number.times(getValueOfUnit(unit));
|
return number.times(getValueOfUnit(unit));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toUnit(number: Big, fromUnit: UNIT, toUnit: UNIT): Big {
|
||||||
|
return toWei(number, fromUnit).div(getValueOfUnit(toUnit));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toTokenUnit(number: Big, token: Token): Big {
|
||||||
|
return number.times(new Big(10).pow(token.decimal));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toTokenDisplay(number: Big, token: Token): Big {
|
||||||
|
return number.times(new Big(10).pow(-token.decimal));
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import type {
|
||||||
SetTokenBalancesAction
|
SetTokenBalancesAction
|
||||||
} from 'actions/wallet';
|
} from 'actions/wallet';
|
||||||
import { BaseWallet } from 'libs/wallet';
|
import { BaseWallet } from 'libs/wallet';
|
||||||
import { toEther } from 'libs/units';
|
import { toUnit } from 'libs/units';
|
||||||
import Big from 'bignumber.js';
|
import Big from 'bignumber.js';
|
||||||
|
|
||||||
export type State = {
|
export type State = {
|
||||||
|
@ -29,7 +29,7 @@ function setWallet(state: State, action: SetWalletAction): State {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBalance(state: State, action: SetBalanceAction): State {
|
function setBalance(state: State, action: SetBalanceAction): State {
|
||||||
const ethBalance = toEther(action.payload, 'wei');
|
const ethBalance = toUnit(action.payload, 'wei', 'ether');
|
||||||
return { ...state, balance: ethBalance };
|
return { ...state, balance: ethBalance };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,12 +2,16 @@
|
||||||
import type { State } from 'reducers';
|
import type { State } from 'reducers';
|
||||||
import { BaseNode } from 'libs/nodes';
|
import { BaseNode } from 'libs/nodes';
|
||||||
import { NODES, NETWORKS } from 'config/data';
|
import { NODES, NETWORKS } from 'config/data';
|
||||||
import type { NetworkConfig, NetworkContract } from 'config/data';
|
import type { NodeConfig, NetworkConfig, NetworkContract } from 'config/data';
|
||||||
|
|
||||||
export function getNode(state: State): string {
|
export function getNode(state: State): string {
|
||||||
return state.config.nodeSelection;
|
return state.config.nodeSelection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getNodeConfig(state: State): NodeConfig {
|
||||||
|
return NODES[state.config.nodeSelection];
|
||||||
|
}
|
||||||
|
|
||||||
export function getNodeLib(state: State): BaseNode {
|
export function getNodeLib(state: State): BaseNode {
|
||||||
return NODES[state.config.nodeSelection].lib;
|
return NODES[state.config.nodeSelection].lib;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3776,6 +3776,29 @@
|
||||||
"resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz",
|
"resolved": "https://registry.npmjs.org/ethereum-common/-/ethereum-common-0.0.18.tgz",
|
||||||
"integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8="
|
"integrity": "sha1-L9w1dvIykDNYl26znaeDIT/5Uj8="
|
||||||
},
|
},
|
||||||
|
"ethereumjs-abi": {
|
||||||
|
"version": "0.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/ethereumjs-abi/-/ethereumjs-abi-0.6.4.tgz",
|
||||||
|
"integrity": "sha1-m6G7BWSS0AwnJ59uzNTVgnWRLBo=",
|
||||||
|
"requires": {
|
||||||
|
"bn.js": "4.11.7",
|
||||||
|
"ethereumjs-util": "4.5.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ethereumjs-util": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz",
|
||||||
|
"integrity": "sha1-PpQosxfuvaPXJg2FT93alUsfG8Y=",
|
||||||
|
"requires": {
|
||||||
|
"bn.js": "4.11.7",
|
||||||
|
"create-hash": "1.1.3",
|
||||||
|
"keccakjs": "0.2.1",
|
||||||
|
"rlp": "2.0.0",
|
||||||
|
"secp256k1": "3.3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"ethereumjs-tx": {
|
"ethereumjs-tx": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/ethereumjs-tx/-/ethereumjs-tx-1.3.3.tgz",
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bignumber.js": "^4.0.2",
|
"bignumber.js": "^4.0.2",
|
||||||
"ethereum-blockies": "git+https://github.com/MyEtherWallet/blockies.git",
|
"ethereum-blockies": "git+https://github.com/MyEtherWallet/blockies.git",
|
||||||
|
"ethereumjs-abi": "^0.6.4",
|
||||||
"ethereumjs-tx": "^1.3.3",
|
"ethereumjs-tx": "^1.3.3",
|
||||||
"ethereumjs-util": "^5.1.2",
|
"ethereumjs-util": "^5.1.2",
|
||||||
"ethereumjs-wallet": "^0.6.0",
|
"ethereumjs-wallet": "^0.6.0",
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
import Contract from 'libs/contract';
|
||||||
|
import Big from 'bignumber.js';
|
||||||
|
|
||||||
|
describe('Contract', () => {
|
||||||
|
// From the ABI docs
|
||||||
|
// https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI#json
|
||||||
|
const testAbi = [
|
||||||
|
{
|
||||||
|
type: 'event',
|
||||||
|
inputs: [
|
||||||
|
{ name: 'a', type: 'uint256', indexed: true },
|
||||||
|
{ name: 'b', type: 'bytes32', indexed: false }
|
||||||
|
],
|
||||||
|
name: 'Event'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
inputs: [{ name: 'a', type: 'uint256' }],
|
||||||
|
name: 'foo',
|
||||||
|
outputs: []
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const testContract = new Contract(testAbi);
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should create an instance given a valid ABI', () => {
|
||||||
|
expect(testContract).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('getMethodAbi', () => {
|
||||||
|
it('should return the a method, given the right name', () => {
|
||||||
|
const method = testContract.getMethodAbi('foo');
|
||||||
|
expect(method.name).toBe('foo');
|
||||||
|
expect(method.type).toBe('function');
|
||||||
|
expect(method.inputs.constructor).toBe(Array);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if given an unknown method name', () => {
|
||||||
|
expect(() => {
|
||||||
|
testContract.getMethodAbi('gnjwakgnawk');
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw if given a method isn’t a function', () => {
|
||||||
|
expect(() => {
|
||||||
|
testContract.getMethodAbi('Event');
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('call / encodeArgs', () => {
|
||||||
|
it('should return hex data for the method', () => {
|
||||||
|
const result = testContract.call('foo', ['1337']);
|
||||||
|
expect(result).toBe(
|
||||||
|
'0x2fbebd380000000000000000000000000000000000000000000000000000000000000539'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw, if given too few method args', () => {
|
||||||
|
expect(() => {
|
||||||
|
testContract.call('foo', []);
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw, if given too many method args', () => {
|
||||||
|
expect(() => {
|
||||||
|
testContract.call('foo', ['1', '2']);
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw, if given invalid args', () => {
|
||||||
|
expect(() => {
|
||||||
|
testContract.call('foo', [{ some: 'object?' }]);
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe('$call / decodeArgs', () => {
|
||||||
|
it('should decode some data', () => {
|
||||||
|
const decoded = testContract.$call(
|
||||||
|
'0x2fbebd380000000000000000000000000000000000000000000000000000000000000539'
|
||||||
|
);
|
||||||
|
expect(decoded.method.name).toBe('foo');
|
||||||
|
expect(decoded.args[0].toString(10)).toBe('1337');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return identical data from a call return', () => {
|
||||||
|
const callMethod = 'foo';
|
||||||
|
const callArgs = ['42891048912084012480129'];
|
||||||
|
const callData = testContract.call(callMethod, callArgs);
|
||||||
|
const decoded = testContract.$call(callData);
|
||||||
|
|
||||||
|
expect(decoded.method.name).toBe(callMethod);
|
||||||
|
expect(decoded.args[0].toString(10)).toBe(callArgs[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw, if given invalid data', () => {
|
||||||
|
expect(() => {
|
||||||
|
// ETH address
|
||||||
|
testContract.$call('0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8');
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw, if given an unknown method’s data', () => {
|
||||||
|
expect(() => {
|
||||||
|
// GNT token send data
|
||||||
|
testContract.$call(
|
||||||
|
'0xa9059cbb0000000000000000000000007cb57b5a97eabe94205c07890be4c1ad31e486a8000000000000000000000000000000000000000000000000130d2a539ba80000'
|
||||||
|
);
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,40 @@
|
||||||
|
import Big from 'bignumber.js';
|
||||||
|
import ERC20 from 'libs/erc20';
|
||||||
|
import abi from 'ethereumjs-abi';
|
||||||
|
const MEW_ADDRESS = '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8';
|
||||||
|
|
||||||
|
describe('ERC20', () => {
|
||||||
|
describe('balanceOf', () => {
|
||||||
|
it('should generate the correct data for checking the balance', () => {
|
||||||
|
const data = ERC20.balanceOf(MEW_ADDRESS);
|
||||||
|
expect(data).toBe(
|
||||||
|
'0x70a082310000000000000000000000007cb57b5a97eabe94205c07890be4c1ad31e486a8'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('transfer', () => {
|
||||||
|
it('should generate the correct data for a transfer', () => {
|
||||||
|
// Test data generated by sending 1 GNT to the MEW address
|
||||||
|
const value = new Big('1').times(new Big(10).pow(18));
|
||||||
|
const data = ERC20.transfer(MEW_ADDRESS, value);
|
||||||
|
expect(data).toBe(
|
||||||
|
'0xa9059cbb0000000000000000000000007cb57b5a97eabe94205c07890be4c1ad31e486a80000000000000000000000000000000000000000000000000de0b6b3a7640000'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('$transfer', () => {
|
||||||
|
// Test data generated by sending 0.001 GNT to the MEW address
|
||||||
|
it('should return the correct transaction given some data', () => {
|
||||||
|
const tx = ERC20.$transfer(
|
||||||
|
'0xa9059cbb0000000000000000000000007cb57b5a97eabe94205c07890be4c1ad31e486a800000000000000000000000000000000000000000000000000038d7ea4c68000'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tx.to).toBe(MEW_ADDRESS);
|
||||||
|
expect(tx.value).toBe(
|
||||||
|
new Big('0.001').times(new Big(10).pow(18)).toString()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,50 @@
|
||||||
|
import Big from 'bignumber.js';
|
||||||
|
import { toWei, toUnit } from '../../common/libs/units';
|
||||||
|
|
||||||
|
describe('Units', () => {
|
||||||
|
describe('toWei', () => {
|
||||||
|
const conversions = [
|
||||||
|
{
|
||||||
|
value: '0.001371',
|
||||||
|
unit: 'ether',
|
||||||
|
wei: '1371000000000000'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '9',
|
||||||
|
unit: 'gwei',
|
||||||
|
wei: '9000000000'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
conversions.forEach(c => {
|
||||||
|
it(`should return '${c.wei}' given ${c.value} ${c.unit}`, () => {
|
||||||
|
const big = new Big(c.value);
|
||||||
|
expect(toWei(big, c.unit).toString()).toEqual(c.wei);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toUnit', () => {
|
||||||
|
const conversions = [
|
||||||
|
{
|
||||||
|
value: '.41849',
|
||||||
|
fromUnit: 'ether',
|
||||||
|
toUnit: 'gwei',
|
||||||
|
output: '418490000'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '4924.71',
|
||||||
|
fromUnit: 'nanoether',
|
||||||
|
toUnit: 'szabo',
|
||||||
|
output: '4.92471'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
conversions.forEach(c => {
|
||||||
|
it(`should return '${c.output}' when converting ${c.value} ${c.fromUnit} to ${c.toUnit}`, () => {
|
||||||
|
const big = new Big(c.value);
|
||||||
|
expect(toUnit(big, c.fromUnit, c.toUnit).toString()).toEqual(c.output);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue