diff --git a/common/components/CurrentCustomMessage.tsx b/common/components/CurrentCustomMessage.tsx new file mode 100644 index 00000000..4b3f734e --- /dev/null +++ b/common/components/CurrentCustomMessage.tsx @@ -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 { + 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 ( +
+
{message.message}
+
+ ); + } 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 = ( + +

+ + A message regarding {address}: + +

+

{msg.msg}

+
+ ); + 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 = ` + You’re 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 = 'You’re 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); diff --git a/common/components/CurrentCustomMessage/CurrentCustomMessage.tsx b/common/components/CurrentCustomMessage/CurrentCustomMessage.tsx deleted file mode 100644 index b56f70bd..00000000 --- a/common/components/CurrentCustomMessage/CurrentCustomMessage.tsx +++ /dev/null @@ -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 { - public render() { - return m.to === this.props.currentTo.raw)} />; - } -} - -export const CurrentCustomMessage = connect((state: AppState) => ({ - currentTo: getCurrentTo(state) -}))(CurrentCustomMessageClass); diff --git a/common/components/CurrentCustomMessage/components/CustomMessage.tsx b/common/components/CurrentCustomMessage/components/CustomMessage.tsx deleted file mode 100644 index 7613e768..00000000 --- a/common/components/CurrentCustomMessage/components/CustomMessage.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; - -interface Props { - message?: { - to: string; - msg: string; - }; -} - -export const CustomMessage = (props: Props) => { - return ( -
- {!!props.message && ( -
-

- A message from {props.message.to} -

-

- {props.message.msg} -

-
- )} -
- ); -}; diff --git a/common/components/CurrentCustomMessage/components/index.ts b/common/components/CurrentCustomMessage/components/index.ts deleted file mode 100644 index 66b7865b..00000000 --- a/common/components/CurrentCustomMessage/components/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './CustomMessage'; -export * from './messages'; diff --git a/common/components/CurrentCustomMessage/components/messages.ts b/common/components/CurrentCustomMessage/components/messages.ts deleted file mode 100644 index b7dc8b29..00000000 --- a/common/components/CurrentCustomMessage/components/messages.ts +++ /dev/null @@ -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.' - } -]; diff --git a/common/components/CurrentCustomMessage/index.ts b/common/components/CurrentCustomMessage/index.ts deleted file mode 100644 index 178f694d..00000000 --- a/common/components/CurrentCustomMessage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './CurrentCustomMessage'; diff --git a/common/config/addressMessages.ts b/common/config/addressMessages.ts new file mode 100644 index 00000000..5b85697c --- /dev/null +++ b/common/config/addressMessages.ts @@ -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]; +} diff --git a/common/config/index.ts b/common/config/index.ts index d64aae8e..d487c767 100644 --- a/common/config/index.ts +++ b/common/config/index.ts @@ -1,3 +1,4 @@ export * from './networks'; export * from './data'; export * from './bity'; +export * from './addressMessages'; diff --git a/common/sagas/transaction/network/gas.ts b/common/sagas/transaction/network/gas.ts index fdfd5565..1ef2a414 100644 --- a/common/sagas/transaction/network/gas.ts +++ b/common/sagas/transaction/network/gas.ts @@ -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) +]; diff --git a/common/sass/styles/overrides/type.scss b/common/sass/styles/overrides/type.scss index 8b44a535..f8ec20de 100644 --- a/common/sass/styles/overrides/type.scss +++ b/common/sass/styles/overrides/type.scss @@ -144,5 +144,5 @@ li { // Blockquotes blockquote { - padding: $space-sm $line-height-computed; + padding: $space-sm #{$line-height-computed}rem; } diff --git a/common/selectors/transaction/current.ts b/common/selectors/transaction/current.ts index 3d7ea625..b51ba522 100644 --- a/common/selectors/transaction/current.ts +++ b/common/selectors/transaction/current.ts @@ -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 }; diff --git a/spec/sagas/transaction/network/gas.spec.ts b/spec/sagas/transaction/network/gas.spec.ts index 4469b659..df4e0a85 100644 --- a/spec/sagas/transaction/network/gas.spec.ts +++ b/spec/sagas/transaction/network/gas.spec.ts @@ -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(); + }); +});