1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

Show users a warning when they are sending directly to a token contract (#13588)

* Fix warning dialog when sending tokens to a known token contract address

Fixing after rebase

Covering missed cases

Rebased and ran yarn setup

Rebased

Fix checkContractAddress condition

Lint fix

Applied requested changes

Fix unit tests

Applying requested changes

Applied requested changes

Refactor and update

Lint fix

Use V2 of ActionableMessage component

Adding Learn More Link

Updating warning copy

Addressing review feedback

Fix up copy changes

Simplify validation of pasted addresses

Improve detection of whether this is a token contract

Refactor to leave updateRecipient unchanged, and to prevent the double calling of update recipient

Update tests

fix

* Fix unit tests

* Fix e2e tests

* Ensure next button is disabled while recipient type is loading

* Add optional chaining and a fallback to getRecipientWarningAcknowledgement

* Fix lint

* Don't reset recipient warning on asset change, because we should show recipient warnings regardless of asset

* Update unit tests

* Update unit tests

Co-authored-by: Filip Sekulic <filip.sekulic@consensys.net>
This commit is contained in:
Dan J Miller 2022-07-13 19:45:38 -02:30 committed by GitHub
parent 75ac87e487
commit 0c163dd8aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 375 additions and 103 deletions

View File

@ -2891,6 +2891,10 @@
"message": "Sending $1",
"description": "$1 represents the native currency symbol for the current network (e.g. ETH or BNB)"
},
"sendingToTokenContractWarning": {
"message": "Warning: you are about to send to a token contract which could result in a loss of funds. $1",
"description": "$1 is a clickable link with text defined by the 'learnMoreUpperCase' key. The link will open to a support article regarding the known contract address warning"
},
"setAdvancedPrivacySettings": {
"message": "Set advanced privacy settings"
},

View File

@ -198,6 +198,7 @@ describe('Send ERC20 to a 40 character hexadecimal address', function () {
);
return sendDialogMsgs.length === 1;
}, 10000);
await driver.delay(2000);
await driver.clickElement({ text: 'Next', tag: 'button' });
// Confirm transaction
@ -296,6 +297,7 @@ describe('Send ERC20 to a 40 character hexadecimal address', function () {
);
return sendDialogMsgs.length === 1;
}, 10000);
await driver.delay(2000);
await driver.clickElement({ text: 'Next', tag: 'button' });
// Confirm transaction

View File

@ -69,6 +69,7 @@ import {
calcTokenAmount,
getTokenAddressParam,
getTokenValueParam,
getTokenMetadata,
} from '../../helpers/utils/token-util';
import {
checkExistingAddresses,
@ -382,6 +383,7 @@ export const draftTransactionInitialState = {
error: null,
nickname: '',
warning: null,
recipientWarningAcknowledged: false,
},
status: SEND_STATUSES.VALID,
transactionType: TRANSACTION_ENVELOPE_TYPES.LEGACY,
@ -952,14 +954,6 @@ const slice = createSlice({
// are no longer valid when sending native currency.
draftTransaction.recipient.error = null;
}
if (
draftTransaction.recipient.warning === KNOWN_RECIPIENT_ADDRESS_WARNING
) {
// Warning related to sending tokens to a known contract address
// are no longer valid when sending native currency.
draftTransaction.recipient.warning = null;
}
}
// if amount mode is MAX update amount to max of new asset, otherwise set
// to zero. This will revalidate the send amount field.
@ -1154,6 +1148,26 @@ const slice = createSlice({
state.recipientInput = '';
state.recipientMode = action.payload;
},
updateRecipientWarning: (state, action) => {
const draftTransaction =
state.draftTransactions[state.currentTransactionUUID];
draftTransaction.recipient.warning = action.payload;
},
updateDraftTransactionStatus: (state, action) => {
const draftTransaction =
state.draftTransactions[state.currentTransactionUUID];
draftTransaction.status = action.payload;
},
acknowledgeRecipientWarning: (state) => {
const draftTransaction =
state.draftTransactions[state.currentTransactionUUID];
draftTransaction.recipient.recipientWarningAcknowledged = true;
slice.caseReducers.validateSendState(state);
},
/**
* Updates the value of the recipientInput key with what the user has
* typed into the recipient input field in the UI.
@ -1316,10 +1330,13 @@ const slice = createSlice({
draftTransaction.recipient.error = null;
draftTransaction.recipient.warning = null;
} else {
const isSendingToken =
draftTransaction.asset.type === ASSET_TYPES.TOKEN ||
draftTransaction.asset.type === ASSET_TYPES.COLLECTIBLE;
const { chainId, tokens, tokenAddressList } = action.payload;
const {
chainId,
tokens,
tokenAddressList,
isProbablyAnAssetContract,
} = action.payload;
if (
isBurnAddress(state.recipientInput) ||
(!isValidHexAddress(state.recipientInput, {
@ -1331,10 +1348,9 @@ const slice = createSlice({
? INVALID_RECIPIENT_ADDRESS_ERROR
: INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR;
} else if (
isSendingToken &&
isOriginContractAddress(
state.recipientInput,
draftTransaction.asset.details.address,
draftTransaction.asset?.details?.address,
)
) {
draftTransaction.recipient.error = CONTRACT_ADDRESS_ERROR;
@ -1342,12 +1358,12 @@ const slice = createSlice({
draftTransaction.recipient.error = null;
}
if (
isSendingToken &&
isValidHexAddress(state.recipientInput) &&
(tokenAddressList.find((address) =>
isEqualCaseInsensitive(address, state.recipientInput),
) ||
checkExistingAddresses(state.recipientInput, tokens))
(isValidHexAddress(state.recipientInput) &&
(tokenAddressList.find((address) =>
isEqualCaseInsensitive(address, state.recipientInput),
) ||
checkExistingAddresses(state.recipientInput, tokens))) ||
isProbablyAnAssetContract
) {
draftTransaction.recipient.warning = KNOWN_RECIPIENT_ADDRESS_WARNING;
} else {
@ -1355,6 +1371,7 @@ const slice = createSlice({
}
}
}
slice.caseReducers.validateSendState(state);
},
/**
* Checks if the draftTransaction is currently valid. The following list of
@ -1392,6 +1409,12 @@ const slice = createSlice({
):
draftTransaction.status = SEND_STATUSES.INVALID;
break;
case draftTransaction.recipient.warning === 'loading':
case draftTransaction.recipient.warning ===
KNOWN_RECIPIENT_ADDRESS_WARNING &&
draftTransaction.recipient.recipientWarningAcknowledged === false:
draftTransaction.status = SEND_STATUSES.INVALID;
break;
default:
draftTransaction.status = SEND_STATUSES.VALID;
}
@ -1589,9 +1612,16 @@ const {
validateRecipientUserInput,
updateRecipientSearchMode,
addHistoryEntry,
acknowledgeRecipientWarning,
} = actions;
export { useDefaultGas, useCustomGas, updateGasLimit, addHistoryEntry };
export {
useDefaultGas,
useCustomGas,
updateGasLimit,
addHistoryEntry,
acknowledgeRecipientWarning,
};
// Action Creators
@ -1601,14 +1631,18 @@ export { useDefaultGas, useCustomGas, updateGasLimit, addHistoryEntry };
* passing in both the dispatch method and the payload to dispatch, which makes
* it only applicable for use within action creators.
*/
const debouncedValidateRecipientUserInput = debounce((dispatch, payload) => {
dispatch(
addHistoryEntry(
`sendFlow - user typed ${payload.userInput} into recipient input field`,
),
);
dispatch(validateRecipientUserInput(payload));
}, 300);
const debouncedValidateRecipientUserInput = debounce(
(dispatch, payload, resolve) => {
dispatch(
addHistoryEntry(
`sendFlow - user typed ${payload.userInput} into recipient input field`,
),
);
dispatch(validateRecipientUserInput(payload));
resolve();
},
300,
);
/**
* Begins a new draft transaction, derived from the txParams of an existing
@ -1799,18 +1833,55 @@ export function updateRecipient({ address, nickname }) {
*/
export function updateRecipientUserInput(userInput) {
return async (dispatch, getState) => {
dispatch(actions.updateRecipientWarning('loading'));
dispatch(actions.updateDraftTransactionStatus(SEND_STATUSES.INVALID));
await dispatch(actions.updateRecipientUserInput(userInput));
const state = getState();
const draftTransaction =
state[name].draftTransactions[state[name].currentTransactionUUID];
const sendingAddress =
draftTransaction.fromAccount?.address ??
state[name].selectedAccount.address ??
getSelectedAddress(state);
const chainId = getCurrentChainId(state);
const tokens = getTokens(state);
const useTokenDetection = getUseTokenDetection(state);
const tokenAddressList = Object.keys(getTokenList(state));
debouncedValidateRecipientUserInput(dispatch, {
userInput,
chainId,
tokens,
useTokenDetection,
tokenAddressList,
const tokenMap = getTokenList(state);
const tokenAddressList = Object.keys(tokenMap);
const inputIsValidHexAddress = isValidHexAddress(userInput);
let isProbablyAnAssetContract = false;
if (inputIsValidHexAddress) {
const { symbol, decimals } = getTokenMetadata(userInput, tokenMap) || {};
isProbablyAnAssetContract = symbol && decimals !== undefined;
if (!isProbablyAnAssetContract) {
try {
const { standard } = await getTokenStandardAndDetails(
userInput,
sendingAddress,
);
isProbablyAnAssetContract = Boolean(standard);
} catch (e) {
console.log(e);
}
}
}
return new Promise((resolve) => {
debouncedValidateRecipientUserInput(
dispatch,
{
userInput,
chainId,
tokens,
useTokenDetection,
tokenAddressList,
isProbablyAnAssetContract,
},
resolve,
);
});
};
}
@ -2008,6 +2079,7 @@ export function updateSendHexData(hexData) {
await dispatch(
addHistoryEntry(`sendFlow - user added custom hexData ${hexData}`),
);
await dispatch(actions.updateUserInputHexData(hexData));
const state = getState();
const draftTransaction =
@ -2486,6 +2558,13 @@ export function getRecipientUserInput(state) {
return state[name].recipientInput;
}
export function getRecipientWarningAcknowledgement(state) {
return (
getCurrentDraftTransaction(state).recipient?.recipientWarningAcknowledged ??
false
);
}
// Overall validity and stage selectors
/**

View File

@ -87,6 +87,11 @@ jest.mock('./send', () => {
};
});
jest.mock('lodash', () => ({
...jest.requireActual('lodash'),
debounce: (fn) => fn,
}));
setBackgroundConnection({
addPollingTokenToAppState: jest.fn(),
addUnapprovedTransaction: jest.fn((_w, _x, _y, _z, cb) => {
@ -495,33 +500,6 @@ describe('Send Slice', () => {
expect(draftTransaction.recipient.error).toBeNull();
});
it('should nullify old known address error when asset types is not TOKEN', () => {
const recipientErrorState = getInitialSendStateWithExistingTxState({
recipient: {
warning: KNOWN_RECIPIENT_ADDRESS_WARNING,
},
asset: {
type: ASSET_TYPES.TOKEN,
},
});
const action = {
type: 'send/updateAsset',
payload: {
type: 'New Type',
},
};
const result = sendReducer(recipientErrorState, action);
const draftTransaction = getTestUUIDTx(result);
expect(draftTransaction.recipient.warning).not.toStrictEqual(
KNOWN_RECIPIENT_ADDRESS_WARNING,
);
expect(draftTransaction.recipient.warning).toBeNull();
});
it('should update asset type and details to TOKEN payload', () => {
const action = {
type: 'send/updateAsset',
@ -626,6 +604,12 @@ describe('Send Slice', () => {
error: 'someError',
warning: 'someWarning',
},
amount: {},
gas: {
gasLimit: '0x0',
minimumGasLimit: '0x0',
},
asset: {},
}),
recipientInput: '',
recipientMode: RECIPIENT_SEARCH_MODES.MY_ACCOUNTS,
@ -746,6 +730,82 @@ describe('Send Slice', () => {
'contractAddressError',
);
});
it('should set a warning when sending to a token address in the token address list', () => {
const tokenAssetTypeState = {
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
recipientInput: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
};
const action = {
type: 'send/validateRecipientUserInput',
payload: {
chainId: '0x4',
tokens: [],
useTokenDetection: true,
tokenAddressList: ['0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'],
},
};
const result = sendReducer(tokenAssetTypeState, action);
const draftTransaction = getTestUUIDTx(result);
expect(draftTransaction.recipient.warning).toStrictEqual(
KNOWN_RECIPIENT_ADDRESS_WARNING,
);
});
it('should set a warning when sending to a token address in the token list', () => {
const tokenAssetTypeState = {
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
recipientInput: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
};
const action = {
type: 'send/validateRecipientUserInput',
payload: {
chainId: '0x4',
tokens: [{ address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' }],
useTokenDetection: true,
tokenAddressList: [],
},
};
const result = sendReducer(tokenAssetTypeState, action);
const draftTransaction = getTestUUIDTx(result);
expect(draftTransaction.recipient.warning).toStrictEqual(
KNOWN_RECIPIENT_ADDRESS_WARNING,
);
});
it('should set a warning when sending to an address that is probably a token contract', () => {
const tokenAssetTypeState = {
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
recipientInput: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
};
const action = {
type: 'send/validateRecipientUserInput',
payload: {
chainId: '0x4',
tokens: [{ address: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' }],
useTokenDetection: true,
tokenAddressList: ['0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'],
isProbablyAnAssetContract: true,
},
};
const result = sendReducer(tokenAssetTypeState, action);
const draftTransaction = getTestUUIDTx(result);
expect(draftTransaction.recipient.warning).toStrictEqual(
KNOWN_RECIPIENT_ADDRESS_WARNING,
);
});
});
describe('updateRecipientSearchMode', () => {
@ -1643,11 +1703,10 @@ describe('Send Slice', () => {
},
},
},
send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
};
it('should create actions for updateRecipientUserInput and checks debounce for validation', async () => {
const clock = sinon.useFakeTimers();
const store = mockStore(updateRecipientUserInputState);
const newUserRecipientInput = 'newUserRecipientInput';
@ -1655,29 +1714,35 @@ describe('Send Slice', () => {
const actionResult = store.getActions();
expect(actionResult).toHaveLength(1);
expect(actionResult).toHaveLength(5);
expect(actionResult[0].type).toStrictEqual(
'send/updateRecipientWarning',
);
expect(actionResult[0].payload).toStrictEqual('loading');
expect(actionResult[1].type).toStrictEqual(
'send/updateDraftTransactionStatus',
);
expect(actionResult[2].type).toStrictEqual(
'send/updateRecipientUserInput',
);
expect(actionResult[0].payload).toStrictEqual(newUserRecipientInput);
expect(actionResult[2].payload).toStrictEqual(newUserRecipientInput);
clock.tick(300); // debounce
const actionResultAfterDebounce = store.getActions();
expect(actionResultAfterDebounce).toHaveLength(3);
expect(actionResultAfterDebounce[1]).toMatchObject({
expect(actionResult[3]).toMatchObject({
type: 'send/addHistoryEntry',
payload: `sendFlow - user typed ${newUserRecipientInput} into recipient input field`,
});
expect(actionResultAfterDebounce[2].type).toStrictEqual(
expect(actionResult[4].type).toStrictEqual(
'send/validateRecipientUserInput',
);
expect(actionResultAfterDebounce[2].payload).toStrictEqual({
expect(actionResult[4].payload).toStrictEqual({
chainId: '',
tokens: [],
useTokenDetection: true,
isProbablyAnAssetContract: false,
userInput: newUserRecipientInput,
tokenAddressList: ['0x514910771af9ca656af840dff83e8264ecf986ca'],
});
@ -1934,21 +1999,7 @@ describe('Send Slice', () => {
},
},
},
send: {
asset: {
type: '',
},
recipient: {
address: 'Address',
nickname: 'NickName',
},
gas: {
gasPrice: '0x1',
},
amount: {
value: '0x1',
},
},
send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
};
const store = mockStore(updateRecipientState);
@ -1956,24 +2007,36 @@ describe('Send Slice', () => {
await store.dispatch(resetRecipientInput());
const actionResult = store.getActions();
expect(actionResult).toHaveLength(7);
expect(actionResult).toHaveLength(11);
expect(actionResult[0]).toMatchObject({
type: 'send/addHistoryEntry',
payload: 'sendFlow - user cleared recipient input',
});
expect(actionResult[1].type).toStrictEqual(
'send/updateRecipientWarning',
);
expect(actionResult[2].type).toStrictEqual(
'send/updateDraftTransactionStatus',
);
expect(actionResult[3].type).toStrictEqual(
'send/updateRecipientUserInput',
);
expect(actionResult[1].payload).toStrictEqual('');
expect(actionResult[2].type).toStrictEqual('send/updateRecipient');
expect(actionResult[3].type).toStrictEqual(
expect(actionResult[4].payload).toStrictEqual(
'sendFlow - user typed into recipient input field',
);
expect(actionResult[5].type).toStrictEqual(
'send/validateRecipientUserInput',
);
expect(actionResult[6].type).toStrictEqual('send/updateRecipient');
expect(actionResult[7].type).toStrictEqual(
'send/computeEstimatedGasLimit/pending',
);
expect(actionResult[4].type).toStrictEqual(
expect(actionResult[8].type).toStrictEqual(
'send/computeEstimatedGasLimit/rejected',
);
expect(actionResult[5].type).toStrictEqual('ENS/resetEnsResolution');
expect(actionResult[6].type).toStrictEqual(
expect(actionResult[9].type).toStrictEqual('ENS/resetEnsResolution');
expect(actionResult[10].type).toStrictEqual(
'send/validateRecipientUserInput',
);
});
@ -2326,6 +2389,7 @@ describe('Send Slice', () => {
error: null,
nickname: '',
warning: null,
recipientWarningAcknowledged: false,
},
status: SEND_STATUSES.VALID,
transactionType: '0x0',
@ -2468,6 +2532,7 @@ describe('Send Slice', () => {
error: null,
nickname: '',
warning: null,
recipientWarningAcknowledged: false,
},
status: SEND_STATUSES.VALID,
transactionType: '0x0',
@ -2653,6 +2718,7 @@ describe('Send Slice', () => {
error: null,
warning: null,
nickname: '',
recipientWarningAcknowledged: false,
},
status: SEND_STATUSES.VALID,
transactionType: '0x0',

View File

@ -47,6 +47,8 @@ export const GAS_ESTIMATE_TYPES = {
let _supportLink = 'https://support.metamask.io';
let _supportRequestLink = 'https://metamask.zendesk.com/hc/en-us/requests/new';
const _contractAddressLink =
'https://metamask.zendesk.com/hc/en-us/articles/360020028092-What-is-the-known-contract-address-warning-';
///: BEGIN:ONLY_INCLUDE_IN(flask)
_supportLink = 'https://metamask-flask.zendesk.com/hc';
@ -56,3 +58,4 @@ _supportRequestLink =
export const SUPPORT_LINK = _supportLink;
export const SUPPORT_REQUEST_LINK = _supportRequestLink;
export const CONTRACT_ADDRESS_LINK = _contractAddressLink;

View File

@ -43,7 +43,7 @@ async function getDecimalsFromContract(tokenAddress) {
}
}
function getTokenMetadata(tokenAddress, tokenList) {
export function getTokenMetadata(tokenAddress, tokenList) {
const casedTokenList = Object.keys(tokenList).reduce((acc, base) => {
return {
...acc,

View File

@ -32,6 +32,7 @@ export default class AddRecipient extends Component {
error: PropTypes.string,
warning: PropTypes.string,
}),
updateRecipientUserInput: PropTypes.func,
};
constructor(props) {
@ -70,6 +71,7 @@ export default class AddRecipient extends Component {
`sendFlow - User clicked recipient from ${type}. address: ${address}, nickname ${nickname}`,
);
this.props.updateRecipient({ address, nickname });
this.props.updateRecipientUserInput(address);
};
searchForContacts = () => {

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import PageContainerContent from '../../../components/ui/page-container/page-container-content.component';
import Dialog from '../../../components/ui/dialog';
import ActionableMessage from '../../../components/ui/actionable-message';
import NicknamePopovers from '../../../components/app/modals/nickname-popovers';
import {
ETH_GAS_PRICE_FETCH_WARNING_KEY,
@ -10,6 +11,7 @@ import {
INSUFFICIENT_FUNDS_FOR_GAS_ERROR_KEY,
} from '../../../helpers/constants/error-keys';
import { ASSET_TYPES } from '../../../../shared/constants/transaction';
import { CONTRACT_ADDRESS_LINK } from '../../../helpers/constants/common';
import SendAmountRow from './send-amount-row';
import SendHexDataRow from './send-hex-data-row';
import SendAssetRow from './send-asset-row';
@ -38,6 +40,9 @@ export default class SendContent extends Component {
asset: PropTypes.object,
to: PropTypes.string,
assetError: PropTypes.string,
recipient: PropTypes.object,
acknowledgeRecipientWarning: PropTypes.func,
recipientWarningAcknowledged: PropTypes.bool,
};
render() {
@ -51,6 +56,8 @@ export default class SendContent extends Component {
getIsBalanceInsufficient,
asset,
assetError,
recipient,
recipientWarningAcknowledged,
} = this.props;
let gasError;
@ -66,6 +73,10 @@ export default class SendContent extends Component {
asset.type !== ASSET_TYPES.TOKEN &&
asset.type !== ASSET_TYPES.COLLECTIBLE;
const showKnownRecipientWarning =
recipient.warning === 'knownAddressRecipient';
const hideAddContactDialog = recipient.warning === 'loading';
return (
<PageContainerContent>
<div className="send-v2__form">
@ -76,7 +87,12 @@ export default class SendContent extends Component {
: null}
{error ? this.renderError(error) : null}
{warning ? this.renderWarning() : null}
{this.maybeRenderAddContact()}
{showKnownRecipientWarning && !recipientWarningAcknowledged
? this.renderRecipientWarning()
: null}
{showKnownRecipientWarning || hideAddContactDialog
? null
: this.maybeRenderAddContact()}
<SendAssetRow />
<SendAmountRow />
{networkOrAccountNotSupports1559 ? <SendGasRow /> : null}
@ -104,6 +120,7 @@ export default class SendContent extends Component {
>
{t('newAccountDetectedDialogMessage')}
</Dialog>
{showNicknamePopovers ? (
<NicknamePopovers
onClose={() => this.setState({ showNicknamePopovers: false })}
@ -124,6 +141,36 @@ export default class SendContent extends Component {
);
}
renderRecipientWarning() {
const { acknowledgeRecipientWarning } = this.props;
const { t } = this.context;
return (
<div className="send__warning-container">
<ActionableMessage
type="danger"
useIcon
iconFillColor="#d73a49"
primaryActionV2={{
label: t('tooltipApproveButton'),
onClick: acknowledgeRecipientWarning,
}}
message={t('sendingToTokenContractWarning', [
<a
key="contractWarningSupport"
target="_blank"
rel="noopener noreferrer"
className="send__warning-container__link"
href={CONTRACT_ADDRESS_LINK}
>
{t('learnMoreUpperCase')}
</a>,
])}
roundedButtons
/>
</div>
);
}
renderError(error) {
const { t } = this.context;
return (

View File

@ -16,6 +16,32 @@ describe('SendContent Component', () => {
gasIsExcessive: false,
networkAndAccountSupports1559: true,
asset: { type: 'NATIVE' },
recipient: {
mode: 'CONTACT_LIST',
userInput: '0x31A2764925BD47CCBd57b2F277702dB46e9C5F66',
address: '0x31A2764925BD47CCBd57b2F277702dB46e9C5F66',
nickname: 'John Doe',
error: null,
warning: null,
},
tokenAddressList: {
'0x32e6c34cd57087abbd59b5a4aecc4cb495924356': {
name: 'BitBase',
symbol: 'BTBS',
decimals: 18,
address: '0x32E6C34Cd57087aBBD59B5A4AECC4cB495924356',
iconUrl: 'BTBS.svg',
occurrences: null,
},
'0x3fa400483487a489ec9b1db29c4129063eec4654': {
name: 'Cryptokek.com',
symbol: 'KEK',
decimals: 18,
address: '0x3fa400483487A489EC9b1dB29C4129063EEC4654',
iconUrl: 'cryptokek.svg',
occurrences: null,
},
},
};
beforeEach(() => {
@ -150,7 +176,7 @@ describe('SendContent Component', () => {
true,
);
expect(
PageContainerContentChild.childAt(1).find(
PageContainerContentChild.childAt(2).find(
'send-v2__asset-dropdown__single-asset',
),
).toHaveLength(0);

View File

@ -11,6 +11,9 @@ import {
getSendTo,
getSendAsset,
getAssetError,
getRecipient,
acknowledgeRecipientWarning,
getRecipientWarningAcknowledgement,
} from '../../../ducks/send';
import SendContent from './send-content.component';
@ -18,6 +21,10 @@ import SendContent from './send-content.component';
function mapStateToProps(state) {
const ownedAccounts = accountsWithSendEtherInfoSelector(state);
const to = getSendTo(state);
const recipient = getRecipient(state);
const recipientWarningAcknowledged = getRecipientWarningAcknowledgement(
state,
);
return {
isOwnedAccount: Boolean(
ownedAccounts.find(
@ -34,7 +41,15 @@ function mapStateToProps(state) {
getIsBalanceInsufficient: getIsBalanceInsufficient(state),
asset: getSendAsset(state),
assetError: getAssetError(state),
recipient,
recipientWarningAcknowledged,
};
}
export default connect(mapStateToProps)(SendContent);
function mapDispatchToProps(dispatch) {
return {
acknowledgeRecipientWarning: () => dispatch(acknowledgeRecipientWarning()),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(SendContent);

View File

@ -91,10 +91,11 @@ export default function SendTransactionScreen() {
userInput={userInput}
className="send__to-row"
onChange={(address) => dispatch(updateRecipientUserInput(address))}
onValidAddressTyped={(address) => {
onValidAddressTyped={async (address) => {
dispatch(
addHistoryEntry(`sendFlow - Valid address typed ${address}`),
);
await dispatch(updateRecipientUserInput(address));
dispatch(updateRecipient({ address, nickname: '' }));
}}
internalSearch={isUsingMyAccountsForRecipientSearch}
@ -106,7 +107,6 @@ export default function SendTransactionScreen() {
`sendFlow - User pasted ${text} into address field`,
),
);
return dispatch(updateRecipient({ address: text, nickname: '' }));
}}
onReset={() => dispatch(resetRecipientInput())}
scanQrCode={() => {

View File

@ -35,6 +35,15 @@
margin: 1rem;
}
&__warning-container {
padding-left: 16px;
padding-right: 16px;
&__link {
color: var(--primary-1);
}
}
&__to-row {
margin: 0;
padding: 0.5rem;

View File

@ -83,6 +83,25 @@ const baseStore = {
'0x0': { balance: '0x0', address: '0x0' },
},
identities: { '0x0': { address: '0x0' } },
tokenAddress: '0x32e6c34cd57087abbd59b5a4aecc4cb495924356',
tokenList: {
'0x32e6c34cd57087abbd59b5a4aecc4cb495924356': {
name: 'BitBase',
symbol: 'BTBS',
decimals: 18,
address: '0x32E6C34Cd57087aBBD59B5A4AECC4cB495924356',
iconUrl: 'BTBS.svg',
occurrences: null,
},
'0x3fa400483487a489ec9b1db29c4129063eec4654': {
name: 'Cryptokek.com',
symbol: 'KEK',
decimals: 18,
address: '0x3fa400483487A489EC9b1dB29C4129063EEC4654',
iconUrl: 'cryptokek.svg',
occurrences: null,
},
},
},
appState: {
sendInputCurrencySwitched: false,