Address messages, warnings and gas limits (#930)

* Move address messages to config folder, add some other messages for common pitfalls.

* Fix checksum vs lowercase issues.

* Use gas limit if an address message specified one. Allow messages to have a custom severity. Add a function for getting message to reduce complexity.

* Handle address message gas limit on all actions, make separate saga fn.

* Apparently I used the wrong takeEvery?
This commit is contained in:
William O'Beirne 2018-01-29 15:00:43 -05:00 committed by Daniel Ternyak
parent d1a2c885a2
commit 9ee30b957d
12 changed files with 393 additions and 87 deletions

View File

@ -0,0 +1,103 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getCurrentTo, ICurrentTo } from 'selectors/transaction';
import { getAllTokens } from 'selectors/config';
import { getWalletInst } from 'selectors/wallet';
import { getAddressMessage, Token } from 'config';
interface ReduxProps {
currentTo: ICurrentTo;
tokens: Token[];
wallet: AppState['wallet']['inst'];
}
interface State {
walletAddress: string | null;
}
class CurrentCustomMessageClass extends PureComponent<ReduxProps, State> {
public state: State = {
walletAddress: null
};
public async componentDidMount() {
if (this.props.wallet) {
const walletAddress = await this.props.wallet.getAddressString();
this.setState({ walletAddress });
}
}
public render() {
const message = this.getMessage();
if (message) {
return (
<div className="clearfix form-group">
<div className={`alert alert-${message.severity} col-xs-12`}>{message.message}</div>
</div>
);
} else {
return null;
}
}
private getMessage() {
const { currentTo, tokens } = this.props;
const { walletAddress } = this.state;
// Make sure all comparisons are lower-cased.
const address = currentTo.raw.toLowerCase();
let message;
let severity;
// First check against our hard-coded messages
const msg = getAddressMessage(address);
if (msg) {
message = (
<React.Fragment>
<p>
<small>
A message regarding <strong>{address}</strong>:
</small>
</p>
<p>{msg.msg}</p>
</React.Fragment>
);
severity = msg.severity || 'info';
}
// Otherwise check if any of our tokens match the address
if (!message) {
const token = tokens.find(tk => tk.address.toLowerCase() === address);
if (token) {
message = `
Youre currently sending to the ${token.symbol} contract. If you
wanted to send ${token.symbol} to an address, change the To Address to
where you want it to go, make sure you have a positive ${token.symbol}
balance in your wallet, and select it from the dropdown next to the
Amount field.
`;
severity = 'warning';
}
}
// Finally check if they're sending to themselves (lol)
if (walletAddress === address) {
message = 'Youre sending to yourself. Are you sure you want to do that?';
severity = 'warning';
}
if (message) {
return {
message,
severity
};
}
}
}
export const CurrentCustomMessage = connect((state: AppState): ReduxProps => ({
currentTo: getCurrentTo(state),
tokens: getAllTokens(state),
wallet: getWalletInst(state)
}))(CurrentCustomMessageClass);

View File

@ -1,18 +0,0 @@
import { CustomMessage, messages } from './components';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { AppState } from 'reducers';
import { getCurrentTo, ICurrentTo } from 'selectors/transaction';
interface StateProps {
currentTo: ICurrentTo;
}
class CurrentCustomMessageClass extends Component<StateProps> {
public render() {
return <CustomMessage message={messages.find(m => m.to === this.props.currentTo.raw)} />;
}
}
export const CurrentCustomMessage = connect((state: AppState) => ({
currentTo: getCurrentTo(state)
}))(CurrentCustomMessageClass);

View File

@ -1,25 +0,0 @@
import React from 'react';
interface Props {
message?: {
to: string;
msg: string;
};
}
export const CustomMessage = (props: Props) => {
return (
<div className="clearfix form-group">
{!!props.message && (
<div className="alert alert-info col-xs-12 clearfix">
<p>
<small>A message from {props.message.to}</small>
</p>
<p>
<strong>{props.message.msg}</strong>
</p>
</div>
)}
</div>
);
};

View File

@ -1,2 +0,0 @@
export * from './CustomMessage';
export * from './messages';

View File

@ -1,30 +0,0 @@
export const messages = [
{
// donation address example
to: '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8',
gasLimit: 21000,
data: '',
msg: 'Thank you for donating to MyEtherWallet. TO THE MOON!'
},
{
// BAT
to: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF',
gasLimit: 200000,
data: '0xb4427263',
msg: 'BAT. THE SALE IS OVER. STOP CLOGGING THE BLOCKCHAIN PLEASE'
},
{
// BANCOR
to: '0x00000',
gasLimit: 200000,
data: '',
msg: 'Bancor. Starts June XX, 2017.'
},
{
// Moeda
to: '0x4870E705a3def9DDa6da7A953D1cd3CCEDD08573',
gasLimit: 200000,
data: '',
msg: 'Moeda. Ends at block 4,111,557.'
}
];

View File

@ -1 +0,0 @@
export * from './CurrentCustomMessage';

View File

@ -0,0 +1,189 @@
import { toChecksumAddress } from 'ethereumjs-util';
export interface AddressMessage {
msg: string;
gasLimit?: number;
data?: string;
severity?: 'warning' | 'danger' | 'success' | 'info';
}
// MAKE SURE THE ADDRESS KEY IS EITHER LOWER CASED OR CHECKSUMMED.
export const ADDRESS_MESSAGES: { [key: string]: AddressMessage } = {
'0x7cb57b5a97eabe94205c07890be4c1ad31e486a8': {
msg: 'Thank you for donating to MyEtherWallet. TO THE MOON!'
},
'0x75aa7b0d02532f3833b66c7f0ad35376d373ddf8': {
gasLimit: 300000,
msg: 'Accord (ARD) ERC20 token sale - http://accordtoken.com'
},
'0x16b0e62ac13a2faed36d18bce2356d25ab3cfad3': {
gasLimit: 200000,
msg:
"BTQ ICO ends February 1, 2018. btc btq is your exclusive bitcoin boutique and world's premier cryptocurrency lifestyle brand. https://thebtcbtq.com/btq"
},
'0xa9877b1e05d035899131dbd1e403825166d09f92': {
gasLimit: 200000,
msg: 'MNT Token Sale - http://mnt.coinjoker.com'
},
'0xef6b4ce8c9bc83744fbcde2657b32ec18790458a': {
gasLimit: 930000,
msg: 'PUC Token Sale - http://price-s.info'
},
'0x13f11c9905a08ca76e3e853be63d4f0944326c72': {
gasLimit: 300000,
data: '0xb4427263',
msg: 'DIVX Token Sale - www.diviproject.org'
},
'0x4b0712de9b75bc68a566215acca876ea5e55c172': {
gasLimit: 114293,
msg: 'NOX Token Sale'
},
'0xf5dffdeaea54bb56156b47de1c7b4346c7dba69c': {
gasLimit: 180000,
msg: 'GEE Token Sale'
},
'0xc88c7e1aebd89187d13bd42e1ff814d32f492bf6': {
gasLimit: 250000,
msg:
'STORM token sale: gamified micro-tasks - Earn anywhere, anytime, from any device. https://www.stormtoken.com, NOV 7, 2017'
},
'0xdd64ef0c8a41d8a17f09ce2279d79b3397184a10': {
gasLimit: 200000,
msg:
'RVL token sale: PRE-ICO SUPER SALE, SHARING ECONOMY PIATTAFORM, https://www.R-EVOLUTIONCOIN.COM.com, DIC 15, 2017'
},
' 0xea0c348a297084bffbddad7f89216f24a2106e58': {
gasLimit: 300000,
msg:
'Aigang token sale contract. Autonomous insurance network - fully automated insurance for IoT devices and a platform for insurance innovation built around data: https://aigang.network . Ends 12/15/2017'
},
'0x17681500757628c7aa56d7e6546e119f94dd9479': {
gasLimit: 170000,
msg:
'Confideal token sale. Confideal is a platform for making deals. https://confideal.io, ends Jan 31, 2018'
},
'0x5454af9d2ba75a60fa5b0419c251810544cea21d': {
gasLimit: 200000,
msg: 'WeBetCrypto ICO Sale. Thank you for your support!'
},
'0x882448f83d90b2bf477af2ea79327fdea1335d93': {
gasLimit: 200000,
msg: 'Vibehub ICO Sale. Thank you for your support!'
},
'0xdea6d29da64bba5ab86a7424ca894756e7ae8ed3': {
gasLimit: 200000,
msg: 'Trade.io ICO Sale. Thank you for your support!'
},
'0xaf518d65f84e4695a4da0450ec02c1248f56b668': {
gasLimit: 200000,
msg: 'Substratum Network ICO Sale. Thank you for your support!'
},
'0x0f33bb20a282a7649c7b3aff644f084a9348e933': {
gasLimit: 400000,
msg: 'YUPIE (YUPIE) ICO'
},
'0xbd2ed3e85faa3433c068c7b3f9c8c7d839ce88d7': {
gasLimit: 69153,
msg: 'Horizon State Token Sale. Thank you for your support. '
},
'0x8aec8f09a840faea966f4b0e29a497d8f5b5a6b4': {
gasLimit: 200000,
msg: 'DataBrokerDAO. https://databrokerdao.com'
},
'0xeaaf270436a0ed397ed23bbf64df7b1dcaff142f': {
gasLimit: 85000,
msg: 'BattleDrome ICO/Crowdsale. Thanks for your support!'
},
'0x58b7056deb51ed292614f0da1e94e7e9c589828d': {
gasLimit: 150000,
msg: 'Simple Token — the cryptocurrency that powers digital communities.'
},
'0x5fb3d432bae33fcd418ede263d98d7440e7fa3ea': {
gasLimit: 200000,
msg: 'SunContract ICO address - suncontract.org'
},
'0xd88755197e107603c139df6e709ed09eec6b6bb3': {
gasLimit: 200000,
msg: 'NVC Fund'
},
'0x2a8a7afa955d8616e2e60e454e5a9c6b6c0a60fc': {
gasLimit: 200000,
msg: 'OHNI ICO. Restoration of our communities!'
},
'0xf9f0fc7167c311dd2f1e21e9204f87eba9012fb2': {
gasLimit: 200000,
msg: 'Easy Homes ICO. Thank you!'
},
'0x7fc408011165760ee31be2bf20daf450356692af': {
gasLimit: 200000,
msg: 'Mitrav ICO Sale. Thank you for your support!'
},
'0xa5dd8cde486436f0cfd62652952e1fcec5a61cae': {
gasLimit: 300000,
msg: 'WinBitcoin ICO Sale. Thank you for your support!'
},
'0x19d7a9ad3b49252fd2ef640d0e43dfd651168499': {
gasLimit: 100000,
msg: 'BMChain ICO - Platform of digital reputation - Official site https://bmchain.io'
},
'0xafe60511341a37488de25bef351952562e31fcc1': {
gasLimit: 200000,
msg: 'Tbot ICO Sale.'
},
'0xe386b139ed3715ca4b18fd52671bdcea1cdfe4b1': {
gasLimit: 200000,
msg:
'Zeus Exchange - The First Hybrid Trading Platform for Traditional Stock Investors and Crypto Traders. Official site https://zeus.exchange'
},
'0xb70835d7822ebb9426b56543e391846c107bd32c': {
gasLimit: 200000,
msg: 'Game Token Sale'
},
'0x5f53f7a8075614b699baad0bc2c899f4bad8fbbf': {
gasLimit: 200000,
msg: 'Rebellious Token'
},
'0xd5e3036d5ce7ec222379d16f6ffc38c38c55bf7f': {
gasLimit: 200000,
msg:
'Ethereum High HIG is a robust and feather-light cryptocurrency designed to hedge the risk of your portfolio'
},
'0x2a3aa9eca41e720ed46b5a70d6c37efa47f768ac': {
gasLimit: 200000,
msg: 'REAL CHAIN TOKEN!'
},
'0x7705faa34b16eb6d77dfc7812be2367ba6b0248e': {
gasLimit: 200000,
msg: 'Artex - Art Provenance Blockchain. Official site https://artex.global'
},
'0x29afa3443f752eb29d814d9042fd88a4a2dc0f1e': {
gasLimit: 200000,
msg: 'SIRIN LABS official crowdsale address. Official website https://sirinlabs.com'
},
'0xa671f2914ba0e73979ffc47cd350801d1714b18f': {
gasLimit: 150000,
msg: 'TRV Ongoing Sale.'
},
'0xdee3bfae40ac2ae9c69ebf30ecaf67a499a9dd5e': {
gasLimit: 150000,
msg: 'The World News Pre-ICO.'
},
'0x92685e93956537c25bb75d5d47fca4266dd628b8': {
gasLimit: 200000,
msg: 'Bitlle Token. Official website https://bitlle.com'
},
'0x2097175d0abb8258f2468e3487f8db776e29d076': {
gasLimit: 200000,
msg: 'LiveEdu EDU token sale. Official website: https://tokensale.liveedu.tv/'
},
'0x4f8b6ca78711207e1b281db63e8d6eaa1ce2f63e': {
gasLimit: 230000,
msg: 'HEdpAY (Hdp.ф) sale. Official sale website: https://ibiginvestments.com/hedpay'
}
};
export function getAddressMessage(address: string): AddressMessage | undefined {
const lowerAddr = address.toLowerCase();
const checksumAddr = toChecksumAddress(address);
return ADDRESS_MESSAGES[lowerAddr] || ADDRESS_MESSAGES[checksumAddr];
}

View File

@ -1,3 +1,4 @@
export * from './networks';
export * from './data';
export * from './bity';
export * from './addressMessages';

View File

@ -1,9 +1,20 @@
import { SagaIterator, buffers, delay } from 'redux-saga';
import { apply, put, select, take, actionChannel, call, fork, race } from 'redux-saga/effects';
import {
apply,
put,
select,
take,
actionChannel,
call,
fork,
race,
takeEvery
} from 'redux-saga/effects';
import BN from 'bn.js';
import { INode } from 'libs/nodes/INode';
import { getNodeLib, getOffline, getAutoGasLimitEnabled } from 'selectors/config';
import { getWalletInst } from 'selectors/wallet';
import { getTransaction, IGetTransaction } from 'selectors/transaction';
import { getTransaction, IGetTransaction, getCurrentToAddressMessage } from 'selectors/transaction';
import {
EstimateGasRequestedAction,
setGasLimitField,
@ -21,6 +32,7 @@ import {
import { TypeKeys as ConfigTypeKeys, ToggleAutoGasLimitAction } from 'actions/config';
import { IWallet } from 'libs/wallet';
import { makeTransaction, getTransactionFields, IHexStrTransaction } from 'libs/transaction';
import { AddressMessage } from 'config';
export function* shouldEstimateGas(): SagaIterator {
while (true) {
@ -38,16 +50,17 @@ export function* shouldEstimateGas(): SagaIterator {
TypeKeys.TOKEN_TO_ETHER_SWAP,
ConfigTypeKeys.CONFIG_TOGGLE_AUTO_GAS_LIMIT
]);
// invalid field is a field that the value is null and the input box isnt empty
// reason being is an empty field is valid because it'll be null
const isOffline: boolean = yield select(getOffline);
const autoGasLimitEnabled: boolean = yield select(getAutoGasLimitEnabled);
const message: AddressMessage | undefined = yield select(getCurrentToAddressMessage);
if (isOffline || !autoGasLimitEnabled) {
if (isOffline || !autoGasLimitEnabled || (message && message.gasLimit)) {
continue;
}
// invalid field is a field that the value is null and the input box isnt empty
// reason being is an empty field is valid because it'll be null
const invalidField =
(action.type === TypeKeys.TO_FIELD_SET || action.type === TypeKeys.DATA_FIELD_SET) &&
!action.payload.value &&
@ -110,4 +123,21 @@ export function* localGasEstimation(payload: EstimateGasRequestedAction['payload
yield put(setGasLimitField({ raw: gasLimit.toString(), value: gasLimit }));
}
export const gas = [fork(shouldEstimateGas), fork(estimateGas)];
export function* setAddressMessageGasLimit() {
const autoGasLimitEnabled: boolean = yield select(getAutoGasLimitEnabled);
const message: AddressMessage | undefined = yield select(getCurrentToAddressMessage);
if (autoGasLimitEnabled && message && message.gasLimit) {
yield put(
setGasLimitField({
raw: message.gasLimit.toString(),
value: new BN(message.gasLimit)
})
);
}
}
export const gas = [
fork(shouldEstimateGas),
fork(estimateGas),
takeEvery(TypeKeys.TO_FIELD_SET, setAddressMessageGasLimit)
];

View File

@ -144,5 +144,5 @@ li {
// Blockquotes
blockquote {
padding: $space-sm $line-height-computed;
padding: $space-sm #{$line-height-computed}rem;
}

View File

@ -4,6 +4,7 @@ import { AppState } from 'reducers';
import { isEtherUnit, TokenValue, Wei, Address } from 'libs/units';
import { gasPriceValidator, gasLimitValidator } from 'libs/validators';
import { getDataExists, getGasPrice, getGasLimit } from 'selectors/transaction';
import { getAddressMessage, AddressMessage } from 'config';
interface ICurrentValue {
raw: string;
@ -42,6 +43,11 @@ const isValidGasPrice = (state: AppState): boolean => gasPriceValidator(getGasPr
const isValidGasLimit = (state: AppState): boolean => gasLimitValidator(getGasLimit(state).raw);
function getCurrentToAddressMessage(state: AppState): AddressMessage | undefined {
const to = getCurrentTo(state);
return getAddressMessage(to.raw);
}
export {
getCurrentValue,
getCurrentTo,
@ -50,5 +56,6 @@ export {
isEtherTransaction,
isValidCurrentTo,
isValidGasPrice,
isValidGasLimit
isValidGasLimit,
getCurrentToAddressMessage
};

View File

@ -1,8 +1,9 @@
import { buffers, delay } from 'redux-saga';
import { apply, put, select, take, actionChannel, call, race } from 'redux-saga/effects';
import BN from 'bn.js';
import { getNodeLib, getOffline, getAutoGasLimitEnabled } from 'selectors/config';
import { getWalletInst } from 'selectors/wallet';
import { getTransaction } from 'selectors/transaction';
import { getTransaction, getCurrentToAddressMessage } from 'selectors/transaction';
import {
setGasLimitField,
estimateGasFailed,
@ -12,7 +13,12 @@ import {
estimateGasTimedout
} from 'actions/transaction';
import { makeTransaction, getTransactionFields } from 'libs/transaction';
import { shouldEstimateGas, estimateGas, localGasEstimation } from 'sagas/transaction/network/gas';
import {
shouldEstimateGas,
estimateGas,
localGasEstimation,
setAddressMessageGasLimit
} from 'sagas/transaction/network/gas';
import { cloneableGenerator } from 'redux-saga/utils';
import { Wei } from 'libs/units';
import { TypeKeys as ConfigTypeKeys } from 'actions/config';
@ -20,6 +26,7 @@ import { TypeKeys as ConfigTypeKeys } from 'actions/config';
describe('shouldEstimateGas*', () => {
const offline = false;
const autoGasLimitEnabled = true;
const addressMessage = undefined;
const transaction: any = 'transaction';
const tx = { transaction };
const rest: any = {
@ -64,8 +71,12 @@ describe('shouldEstimateGas*', () => {
expect(gen.next(offline).value).toEqual(select(getAutoGasLimitEnabled));
});
it('should select getCurrentToAddressMessage', () => {
expect(gen.next(autoGasLimitEnabled).value).toEqual(select(getCurrentToAddressMessage));
});
it('should select getTransaction', () => {
expect(gen.next(autoGasLimitEnabled).value).toEqual(select(getTransaction));
expect(gen.next(addressMessage).value).toEqual(select(getTransaction));
});
it('should call getTransactionFields with transaction', () => {
@ -236,3 +247,44 @@ describe('localGasEstimation', () => {
);
});
});
describe('setAddressMessageGasLimit*', () => {
const gens = cloneableGenerator(setAddressMessageGasLimit)();
const gen = gens.clone();
let noAutoGen;
let noMessageGen;
const addressMessage = {
gasLimit: 123456,
msg: 'Thanks for donating, er, investing in SCAM'
};
it('should select getAutoGasLimitEnabled', () => {
expect(gen.next().value).toEqual(select(getAutoGasLimitEnabled));
});
it('should select getCurrentToAddressMessage', () => {
noAutoGen = gen.clone();
expect(gen.next(true).value).toEqual(select(getCurrentToAddressMessage));
});
it('should put setGasLimitField', () => {
noMessageGen = gen.clone();
expect(gen.next(addressMessage).value).toEqual(
put(
setGasLimitField({
raw: addressMessage.gasLimit.toString(),
value: new BN(addressMessage.gasLimit)
})
)
);
});
it('should do nothing if getAutoGasLimitEnabled is false', () => {
noAutoGen.next(false);
expect(noAutoGen.next(addressMessage).done).toBeTruthy();
});
it('should do nothing if getCurrentToAddressMessage is undefined', () => {
expect(noMessageGen.next(undefined).done).toBeTruthy();
});
});