mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
37209a7d2e
Remove the IncomingTransactionController and replace it with an internal helper class. Move incoming transactions into the central transactions object. Create a new RemoteTransactionSource interface to decouple incoming transaction support from Etherscan. Split the incoming transaction logic into multiple files for easier maintenance.
350 lines
8.7 KiB
JavaScript
350 lines
8.7 KiB
JavaScript
import { head, last } from 'lodash';
|
|
import { CHAIN_IDS } from '../../shared/constants/network';
|
|
import {
|
|
TransactionStatus,
|
|
TransactionType,
|
|
} from '../../shared/constants/transaction';
|
|
import { nonceSortedTransactionsSelector } from './transactions';
|
|
|
|
const RECIPIENTS = {
|
|
ONE: '0xRecipient1',
|
|
TWO: '0xRecipient2',
|
|
};
|
|
|
|
const SENDERS = {
|
|
ONE: '0xSender1',
|
|
TWO: '0xSender2',
|
|
};
|
|
|
|
const INCOMING_TX = {
|
|
id: '0-incoming',
|
|
type: TransactionType.incoming,
|
|
txParams: {
|
|
value: '0x0',
|
|
from: RECIPIENTS.ONE,
|
|
to: SENDERS.ONE,
|
|
},
|
|
};
|
|
|
|
const SIGNING_REQUEST = {
|
|
type: TransactionType.sign,
|
|
id: '0-signing',
|
|
status: TransactionStatus.unapproved,
|
|
};
|
|
|
|
const SIMPLE_SEND_TX = {
|
|
id: '0-simple',
|
|
txParams: {
|
|
from: SENDERS.ONE,
|
|
to: RECIPIENTS.ONE,
|
|
},
|
|
type: TransactionType.simpleSend,
|
|
};
|
|
|
|
const TOKEN_SEND_TX = {
|
|
id: '0-transfer',
|
|
txParams: {
|
|
from: SENDERS.ONE,
|
|
to: RECIPIENTS.TWO,
|
|
value: '0x0',
|
|
data: '0xdata',
|
|
},
|
|
type: TransactionType.tokenMethodTransfer,
|
|
};
|
|
|
|
const RETRY_TX = {
|
|
...SIMPLE_SEND_TX,
|
|
id: '0-retry',
|
|
type: TransactionType.retry,
|
|
};
|
|
|
|
const CANCEL_TX = {
|
|
id: '0-cancel',
|
|
txParams: {
|
|
value: '0x0',
|
|
from: SENDERS.ONE,
|
|
to: SENDERS.ONE,
|
|
},
|
|
type: TransactionType.cancel,
|
|
};
|
|
|
|
const getStateTree = ({
|
|
txList = [],
|
|
incomingTxList = [],
|
|
unapprovedMsgs = [],
|
|
} = {}) => ({
|
|
metamask: {
|
|
providerConfig: {
|
|
nickname: 'mainnet',
|
|
chainId: CHAIN_IDS.MAINNET,
|
|
},
|
|
unapprovedMsgs,
|
|
selectedAddress: SENDERS.ONE,
|
|
featureFlags: {
|
|
showIncomingTransactions: true,
|
|
},
|
|
transactions: [...incomingTxList],
|
|
currentNetworkTxList: [...txList],
|
|
},
|
|
});
|
|
|
|
const duplicateTx = (base, overrides) => {
|
|
const {
|
|
nonce = '0x0',
|
|
time = 0,
|
|
status = TransactionStatus.confirmed,
|
|
txReceipt,
|
|
} = overrides ?? {};
|
|
return {
|
|
...base,
|
|
txParams: {
|
|
...base.txParams,
|
|
nonce,
|
|
},
|
|
txReceipt,
|
|
time,
|
|
status,
|
|
};
|
|
};
|
|
|
|
describe('nonceSortedTransactionsSelector', () => {
|
|
it('should properly group a simple send that is superseded by a retry', () => {
|
|
const txList = [
|
|
duplicateTx(SIMPLE_SEND_TX, { status: TransactionStatus.dropped }),
|
|
duplicateTx(RETRY_TX, { time: 1 }),
|
|
];
|
|
|
|
const state = getStateTree({ txList });
|
|
|
|
const result = nonceSortedTransactionsSelector(state);
|
|
|
|
expect(result).toStrictEqual([
|
|
{
|
|
nonce: '0x0',
|
|
transactions: txList,
|
|
initialTransaction: head(txList),
|
|
primaryTransaction: last(txList),
|
|
hasRetried: true,
|
|
hasCancelled: false,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should properly group a failed off-chain simple send that is superseded by a retry', () => {
|
|
const txList = [
|
|
duplicateTx(SIMPLE_SEND_TX, { status: TransactionStatus.failed }),
|
|
duplicateTx(RETRY_TX, { time: 1 }),
|
|
];
|
|
|
|
const state = getStateTree({ txList });
|
|
|
|
const result = nonceSortedTransactionsSelector(state);
|
|
|
|
expect(result).toStrictEqual([
|
|
{
|
|
nonce: '0x0',
|
|
transactions: txList,
|
|
initialTransaction: head(txList),
|
|
primaryTransaction: last(txList),
|
|
hasRetried: true,
|
|
hasCancelled: false,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should properly group a simple send that is superseded by a cancel', () => {
|
|
const txList = [
|
|
duplicateTx(SIMPLE_SEND_TX, { status: TransactionStatus.dropped }),
|
|
duplicateTx(CANCEL_TX, { time: 1 }),
|
|
];
|
|
|
|
const state = getStateTree({ txList });
|
|
|
|
const result = nonceSortedTransactionsSelector(state);
|
|
|
|
expect(result).toStrictEqual([
|
|
{
|
|
nonce: '0x0',
|
|
transactions: txList,
|
|
initialTransaction: head(txList),
|
|
primaryTransaction: last(txList),
|
|
hasRetried: false,
|
|
hasCancelled: true,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should properly group a simple send and retry that is superseded by a cancel', () => {
|
|
const txList = [
|
|
duplicateTx(SIMPLE_SEND_TX, { status: TransactionStatus.dropped }),
|
|
duplicateTx(RETRY_TX, { time: 1, status: TransactionStatus.dropped }),
|
|
duplicateTx(CANCEL_TX, { time: 2 }),
|
|
];
|
|
|
|
const state = getStateTree({ txList });
|
|
|
|
const result = nonceSortedTransactionsSelector(state);
|
|
|
|
expect(result).toStrictEqual([
|
|
{
|
|
nonce: '0x0',
|
|
transactions: txList,
|
|
initialTransaction: head(txList),
|
|
primaryTransaction: last(txList),
|
|
hasRetried: true,
|
|
hasCancelled: true,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should group transactions created by an advance user attempting to manually supersede own txs', () => {
|
|
// We do not want this behavior longterm. This test just keeps us from
|
|
// changing expectations from today. It will also allow us to invert this
|
|
// test case when we move and change grouping logic.
|
|
const txList = [
|
|
duplicateTx(TOKEN_SEND_TX, { status: TransactionStatus.dropped }),
|
|
duplicateTx(SIMPLE_SEND_TX, { time: 1 }),
|
|
];
|
|
|
|
const state = getStateTree({ txList });
|
|
|
|
const result = nonceSortedTransactionsSelector(state);
|
|
|
|
expect(result).toStrictEqual([
|
|
{
|
|
nonce: '0x0',
|
|
transactions: txList,
|
|
initialTransaction: head(txList),
|
|
primaryTransaction: last(txList),
|
|
hasRetried: false,
|
|
hasCancelled: false,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should NOT group sent and incoming tx with same nonce', () => {
|
|
const txList = [duplicateTx(SIMPLE_SEND_TX)];
|
|
const incomingTxList = [duplicateTx(INCOMING_TX, { time: 1 })];
|
|
|
|
const state = getStateTree({ txList, incomingTxList });
|
|
|
|
const result = nonceSortedTransactionsSelector(state);
|
|
|
|
expect(result).toStrictEqual([
|
|
{
|
|
nonce: '0x0',
|
|
transactions: txList,
|
|
initialTransaction: head(txList),
|
|
primaryTransaction: head(txList),
|
|
hasRetried: false,
|
|
hasCancelled: false,
|
|
},
|
|
{
|
|
nonce: '0x0',
|
|
transactions: incomingTxList,
|
|
initialTransaction: head(incomingTxList),
|
|
primaryTransaction: head(incomingTxList),
|
|
hasRetried: false,
|
|
hasCancelled: false,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should display a signing request', () => {
|
|
const state = getStateTree({ unapprovedMsgs: [SIGNING_REQUEST] });
|
|
|
|
const result = nonceSortedTransactionsSelector(state);
|
|
|
|
expect(result).toStrictEqual([
|
|
{
|
|
nonce: undefined,
|
|
transactions: [SIGNING_REQUEST],
|
|
initialTransaction: SIGNING_REQUEST,
|
|
primaryTransaction: SIGNING_REQUEST,
|
|
hasRetried: false,
|
|
hasCancelled: false,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should not set a failed off-chain transaction as primary, allowing additional retries', () => {
|
|
const txList = [
|
|
duplicateTx(SIMPLE_SEND_TX, { status: TransactionStatus.submitted }),
|
|
duplicateTx(RETRY_TX, { status: TransactionStatus.failed, time: 1 }),
|
|
];
|
|
|
|
const state = getStateTree({ txList });
|
|
|
|
const result = nonceSortedTransactionsSelector(state);
|
|
|
|
expect(result).toStrictEqual([
|
|
{
|
|
nonce: '0x0',
|
|
transactions: txList,
|
|
initialTransaction: head(txList),
|
|
primaryTransaction: head(txList),
|
|
hasRetried: false,
|
|
hasCancelled: false,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should not set a failed off-chain transaction as primary or initial, regardless of tx order', () => {
|
|
// Scenario:
|
|
// 1. You submit transaction A.
|
|
// 2. Transaction A fails off-chain (the network rejects it).
|
|
// 3. You submit transaction B.
|
|
// 4. Transaction A no longer has any visual representation in the UI.
|
|
// This is desired because we have no way currently to tell the intent
|
|
// of the transactions.
|
|
const txList = [
|
|
duplicateTx(SIMPLE_SEND_TX, { status: TransactionStatus.failed }),
|
|
duplicateTx(SIMPLE_SEND_TX, {
|
|
status: TransactionStatus.submitted,
|
|
time: 1,
|
|
}),
|
|
];
|
|
|
|
const state = getStateTree({ txList });
|
|
|
|
const result = nonceSortedTransactionsSelector(state);
|
|
|
|
expect(result).toStrictEqual([
|
|
{
|
|
nonce: '0x0',
|
|
transactions: txList,
|
|
initialTransaction: last(txList),
|
|
primaryTransaction: last(txList),
|
|
hasRetried: false,
|
|
hasCancelled: false,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('should set a failed on-chain transaction as primary', () => {
|
|
const txList = [
|
|
duplicateTx(SIMPLE_SEND_TX, { status: TransactionStatus.submitted }),
|
|
duplicateTx(RETRY_TX, {
|
|
status: TransactionStatus.failed,
|
|
txReceipt: { status: '0x0' },
|
|
time: 1,
|
|
}),
|
|
];
|
|
|
|
const state = getStateTree({ txList });
|
|
|
|
const result = nonceSortedTransactionsSelector(state);
|
|
|
|
expect(result).toStrictEqual([
|
|
{
|
|
nonce: '0x0',
|
|
transactions: txList,
|
|
initialTransaction: head(txList),
|
|
primaryTransaction: last(txList),
|
|
hasRetried: false,
|
|
hasCancelled: false,
|
|
},
|
|
]);
|
|
});
|
|
});
|