-
Notifications
You must be signed in to change notification settings - Fork 5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix splitting of transaction nonce groups in state #11103
Changes from 3 commits
d6c9039
f6f222d
8f60118
284c1b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,9 @@ | ||
import { strict as assert } from 'assert'; | ||
import sinon from 'sinon'; | ||
import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction'; | ||
import { | ||
TRANSACTION_STATUSES, | ||
TRANSACTION_TYPES, | ||
} from '../../../../shared/constants/transaction'; | ||
import { | ||
KOVAN_CHAIN_ID, | ||
KOVAN_NETWORK_ID, | ||
|
@@ -10,6 +13,36 @@ import { snapshotFromTxMeta } from './lib/tx-state-history-helpers'; | |
|
||
const VALID_ADDRESS = '0x0000000000000000000000000000000000000000'; | ||
const VALID_ADDRESS_TWO = '0x0000000000000000000000000000000000000001'; | ||
|
||
function generateTransactions( | ||
numToGen, | ||
{ | ||
chainId, | ||
to, | ||
from, | ||
status, | ||
type = TRANSACTION_TYPES.SENT_ETHER, | ||
nonce = (i) => `${i}`, | ||
}, | ||
) { | ||
const txs = []; | ||
for (let i = 0; i < numToGen; i++) { | ||
const tx = { | ||
id: i, | ||
time: new Date() * i, | ||
status: typeof status === 'function' ? status(i) : status, | ||
chainId: typeof chainId === 'function' ? chainId(i) : chainId, | ||
txParams: { | ||
nonce: nonce(i), | ||
to, | ||
from, | ||
}, | ||
type: typeof type === 'function' ? type(i) : type, | ||
}; | ||
txs.push(tx); | ||
} | ||
return txs; | ||
} | ||
describe('TransactionStateManager', function () { | ||
let txStateManager; | ||
const currentNetworkId = KOVAN_NETWORK_ID; | ||
|
@@ -540,72 +573,56 @@ describe('TransactionStateManager', function () { | |
|
||
it('cuts off early txs beyond a limit', function () { | ||
const limit = txStateManager.txHistoryLimit; | ||
for (let i = 0; i < limit + 1; i++) { | ||
const tx = { | ||
id: i, | ||
time: new Date(), | ||
status: TRANSACTION_STATUSES.CONFIRMED, | ||
metamaskNetworkId: currentNetworkId, | ||
txParams: { | ||
to: VALID_ADDRESS, | ||
from: VALID_ADDRESS, | ||
}, | ||
}; | ||
txStateManager.addTransaction(tx); | ||
} | ||
const txs = generateTransactions(limit + 1, { | ||
chainId: currentChainId, | ||
to: VALID_ADDRESS, | ||
from: VALID_ADDRESS_TWO, | ||
status: TRANSACTION_STATUSES.CONFIRMED, | ||
}); | ||
txs.forEach((tx) => txStateManager.addTransaction(tx)); | ||
const result = txStateManager.getTransactions(); | ||
assert.equal(result.length, limit, `limit of ${limit} txs enforced`); | ||
assert.equal(result[0].id, 1, 'early txs truncated'); | ||
}); | ||
|
||
it('cuts off early txs beyond a limit whether or not it is confirmed or rejected', function () { | ||
const limit = txStateManager.txHistoryLimit; | ||
for (let i = 0; i < limit + 1; i++) { | ||
const tx = { | ||
id: i, | ||
time: new Date(), | ||
status: TRANSACTION_STATUSES.REJECTED, | ||
metamaskNetworkId: currentNetworkId, | ||
txParams: { | ||
to: VALID_ADDRESS, | ||
from: VALID_ADDRESS, | ||
}, | ||
}; | ||
txStateManager.addTransaction(tx); | ||
} | ||
const txs = generateTransactions(limit + 1, { | ||
chainId: currentChainId, | ||
to: VALID_ADDRESS, | ||
from: VALID_ADDRESS_TWO, | ||
status: TRANSACTION_STATUSES.REJECTED, | ||
}); | ||
txs.forEach((tx) => txStateManager.addTransaction(tx)); | ||
const result = txStateManager.getTransactions(); | ||
assert.equal(result.length, limit, `limit of ${limit} txs enforced`); | ||
assert.equal(result[0].id, 1, 'early txs truncated'); | ||
}); | ||
|
||
it('cuts off early txs beyond a limit but does not cut unapproved txs', function () { | ||
const unconfirmedTx = { | ||
id: 0, | ||
time: new Date(), | ||
status: TRANSACTION_STATUSES.UNAPPROVED, | ||
metamaskNetworkId: currentNetworkId, | ||
txParams: { | ||
const limit = txStateManager.txHistoryLimit; | ||
const txs = generateTransactions( | ||
// we add two transactions over limit here to first insert the must be always present | ||
// unapproved tx, then another to force the original logic of adding | ||
// one more beyond the first additional. | ||
limit + 2, | ||
{ | ||
chainId: currentChainId, | ||
to: VALID_ADDRESS, | ||
from: VALID_ADDRESS, | ||
from: VALID_ADDRESS_TWO, | ||
status: (i) => | ||
i === 0 | ||
? TRANSACTION_STATUSES.UNAPPROVED | ||
: TRANSACTION_STATUSES.CONFIRMED, | ||
}, | ||
}; | ||
txStateManager.addTransaction(unconfirmedTx); | ||
const limit = txStateManager.txHistoryLimit; | ||
for (let i = 1; i < limit + 1; i++) { | ||
const tx = { | ||
id: i, | ||
time: new Date(), | ||
status: TRANSACTION_STATUSES.CONFIRMED, | ||
metamaskNetworkId: currentNetworkId, | ||
txParams: { | ||
to: VALID_ADDRESS, | ||
from: VALID_ADDRESS, | ||
}, | ||
}; | ||
txStateManager.addTransaction(tx); | ||
} | ||
); | ||
txs.forEach((tx) => txStateManager.addTransaction(tx)); | ||
const result = txStateManager.getTransactions(); | ||
assert.equal(result.length, limit, `limit of ${limit} txs enforced`); | ||
assert.equal( | ||
result.length, | ||
limit + 1, | ||
`limit of ${limit} + 1 for the unapproved tx is enforced`, | ||
); | ||
assert.equal(result[0].id, 0, 'first tx should still be there'); | ||
assert.equal( | ||
result[0].status, | ||
|
@@ -614,6 +631,136 @@ describe('TransactionStateManager', function () { | |
); | ||
assert.equal(result[1].id, 2, 'early txs truncated'); | ||
}); | ||
|
||
it('cuts off entire groups of transactions by nonce when adding new transaction', function () { | ||
const limit = txStateManager.txHistoryLimit; | ||
// In this test case the earliest two transactions are a dropped attempted ether send and a | ||
// following cancel transaction with the same nonce. these two transactions should be dropped | ||
// together as soon as the 11th unique nonce is attempted to be added, mocked here with limit + 1 | ||
const txs = generateTransactions(limit + 2, { | ||
Gudahtt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
chainId: currentChainId, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes! must have dropped during code cleanup There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
to: VALID_ADDRESS, | ||
from: VALID_ADDRESS_TWO, | ||
nonce: (i) => (i === 1 ? `0` : `${i}`), | ||
status: (i) => | ||
i === 0 | ||
? TRANSACTION_STATUSES.DROPPED | ||
: TRANSACTION_STATUSES.CONFIRMED, | ||
type: (i) => | ||
i === 1 ? TRANSACTION_TYPES.CANCEL : TRANSACTION_STATUSES.SENT_ETHER, | ||
}); | ||
txs.forEach((tx) => txStateManager.addTransaction(tx)); | ||
const result = txStateManager.getTransactions(); | ||
assert.equal(result.length, limit, `limit of ${limit} is enforced`); | ||
assert.notEqual(result[0].id, 0, 'first tx should be removed'); | ||
assert.notEqual( | ||
result[0].status, | ||
TRANSACTION_STATUSES.DROPPED, | ||
'first tx should not be dropped', | ||
); | ||
assert.notEqual( | ||
result[1].type, | ||
TRANSACTION_TYPES.CANCEL, | ||
'Cancel tx should be dropped', | ||
Gudahtt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
); | ||
}); | ||
|
||
it('cuts off entire groups of transactions by nonce + network when adding new transaction', function () { | ||
const limit = txStateManager.txHistoryLimit; | ||
// In this test case the earliest two transactions are a dropped attempted ether send and a | ||
// following cancel transaction with the same nonce. Then, a bit later the same scenario on a | ||
// different network. The first two transactions should be dropped after adding even another | ||
// single transaction but the other shouldn't be dropped until adding the fifth additional | ||
// transaction | ||
const txs = generateTransactions(limit + 5, { | ||
chainId: (i) => ([0, 1, 4, 5].includes(i) ? currentChainId : '0x1'), | ||
to: VALID_ADDRESS, | ||
from: VALID_ADDRESS_TWO, | ||
nonce: (i) => { | ||
if (i === 1) return '0'; | ||
else if (i === 5) return '4'; | ||
Gudahtt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return `${i}`; | ||
}, | ||
status: (i) => | ||
i === 0 || i === 4 | ||
? TRANSACTION_STATUSES.DROPPED | ||
: TRANSACTION_STATUSES.CONFIRMED, | ||
type: (i) => | ||
i === 1 || i === 5 | ||
? TRANSACTION_TYPES.CANCEL | ||
: TRANSACTION_STATUSES.SENT_ETHER, | ||
}); | ||
txs.forEach((tx) => txStateManager.addTransaction(tx)); | ||
const result = txStateManager.getTransactions({ | ||
filterToCurrentNetwork: false, | ||
}); | ||
assert.equal( | ||
result.length, | ||
limit + 1, | ||
`limit of ${limit} + 1 for the grouped transactions is enforced`, | ||
); | ||
assert.notEqual(result[0].id, 0, 'first tx should be removed'); | ||
assert.equal( | ||
result[0].status, | ||
TRANSACTION_STATUSES.DROPPED, | ||
'first tx should be dropped', | ||
); | ||
assert.equal( | ||
result[0].txParams.nonce, | ||
'0x4', | ||
'the first tx should be from the second group', | ||
); | ||
assert.equal( | ||
result[1].type, | ||
TRANSACTION_TYPES.CANCEL, | ||
'second transaction should be a cancel', | ||
); | ||
assert.equal( | ||
result[1].txParams.nonce, | ||
'0x4', | ||
'the second tx should be from the second group', | ||
); | ||
}); | ||
|
||
it('does not cut off entire groups of transactions when adding new transaction when under limit', function () { | ||
// In this test case the earliest two transactions are a dropped attempted ether send and a | ||
// following cancel transaction with the same nonce. Then, a bit later the same scenario on a | ||
// different network. None of these should be dropped because we haven't yet reached the limit | ||
const txs = generateTransactions(9, { | ||
Gudahtt marked this conversation as resolved.
Show resolved
Hide resolved
|
||
chainId: (i) => ([0, 1, 4, 5].includes(i) ? currentChainId : '0x1'), | ||
to: VALID_ADDRESS, | ||
from: VALID_ADDRESS_TWO, | ||
nonce: (i) => { | ||
if (i === 1) return '0'; | ||
else if (i === 5) return '4'; | ||
return `${i}`; | ||
}, | ||
status: (i) => | ||
i === 0 || i === 4 | ||
? TRANSACTION_STATUSES.DROPPED | ||
: TRANSACTION_STATUSES.CONFIRMED, | ||
type: (i) => | ||
i === 1 || i === 5 | ||
? TRANSACTION_TYPES.CANCEL | ||
: TRANSACTION_STATUSES.SENT_ETHER, | ||
}); | ||
txs.forEach((tx) => txStateManager.addTransaction(tx)); | ||
const result = txStateManager.getTransactions({ | ||
filterToCurrentNetwork: false, | ||
}); | ||
assert.equal(result.length, 9, `all nine transactions should be present`); | ||
assert.equal(result[0].id, 0, 'first tx should be present'); | ||
assert.equal( | ||
result[0].status, | ||
TRANSACTION_STATUSES.DROPPED, | ||
'first tx should be dropped', | ||
); | ||
assert.equal( | ||
result[1].type, | ||
TRANSACTION_TYPES.CANCEL, | ||
'second transaction should be a cancel', | ||
); | ||
}); | ||
}); | ||
|
||
describe('#updateTransaction', function () { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to make sure I understand this.
So the goal is to ensure that if there are two or more transactions with the same nonce in the transaction list, that they don't get deleted.
So we iterate through the transactions. If a transaction is either counted as below
txHistoryLimit - 1
or is not in a final state, then it is NOT deleted. If a transaction is counted higher thantxHistoryLimit - 1
or is in a final state BUT has the same nonce as a transaction already flagged as one that shouldn't be deleted, then it is NOT deleted. If a transaction is above the txHistoryLimit count or is in a final state and does not match the nonce of a transaction already identified as one that shouldn't be deleted, then it is deleted...So something about this isn't adding up for me... probably me misunderstanding, but I want to check.
Suppose that there are only 10 transactions and they are all "final states". Wouldn't they all be deleted according to this logic?
Should the
if (txCount > txHistoryLimit - 1) {
that was removed above wrap this tx deletion code?And this is in a function that only adds a single tx. Before it only removed a single tx. Why now attempt to remove multiple within this function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe I forget how
filter
works...There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe not
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on my reading of the tests I think I did misunderstand the goal though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can the purpose of this code change be stated as:
If the addition of a transaction brings the transaction count above the txHistoryLimit then all transactions that share a nonce with the oldest "finalized" transaction should be deleted.
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's to ensure that one transaction in a group isn't deleted without deleting the entire group.
We can't let all groups of more than 1 transaction persist indefinitely - then we'd have no limit on storage. But we can't break groups apart either without breaking the UI.
So the solution we went with here was to interpret the transaction history limit of 50 as meaning 50 groups of transactions. If there are over 50 groups, we delete any groups beyond that 50 (which should always be a single group if this is the only means of adding transactions, but might be more than 1 group if transactions are added elsewhere).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when we return false from this method we are Not including it, when we return true we are including it. So when return true we are saying "this is an item we want to delete". if there are only ten transactions non will be deleted because they will never hit the limit and thus will all be added to the nonceSet
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
basically yes as stated that will work