mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-22 17:33:23 +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:
parent
5e12aae541
commit
c63714c4f2
4
app/_locales/en/messages.json
generated
4
app/_locales/en/messages.json
generated
@ -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"
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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 = () => {
|
||||
|
@ -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 (
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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={() => {
|
||||
|
@ -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;
|
||||
|
@ -80,6 +80,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,
|
||||
|
Loading…
Reference in New Issue
Block a user