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

Refactor Tx State Manager (#10672)

Co-authored-by: Mark Stacey <markjstacey@gmail.com>
This commit is contained in:
Brad Decker 2021-03-30 09:54:05 -05:00 committed by GitHub
parent e0b7d08ffb
commit 4080ed63a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1295 additions and 752 deletions

View File

@ -150,8 +150,8 @@ export default class TransactionController extends EventEmitter {
Adds a tx to the txlist Adds a tx to the txlist
@emits ${txMeta.id}:unapproved @emits ${txMeta.id}:unapproved
*/ */
addTx(txMeta) { addTransaction(txMeta) {
this.txStateManager.addTx(txMeta); this.txStateManager.addTransaction(txMeta);
this.emit(`${txMeta.id}:unapproved`, txMeta); this.emit(`${txMeta.id}:unapproved`, txMeta);
} }
@ -273,22 +273,28 @@ export default class TransactionController extends EventEmitter {
? addHexPrefix(txMeta.txParams.value) ? addHexPrefix(txMeta.txParams.value)
: '0x0'; : '0x0';
this.addTx(txMeta); this.addTransaction(txMeta);
this.emit('newUnapprovedTx', txMeta); this.emit('newUnapprovedTx', txMeta);
try { try {
txMeta = await this.addTxGasDefaults(txMeta, getCodeResponse); txMeta = await this.addTxGasDefaults(txMeta, getCodeResponse);
} catch (error) { } catch (error) {
log.warn(error); log.warn(error);
txMeta = this.txStateManager.getTx(txMeta.id); txMeta = this.txStateManager.getTransaction(txMeta.id);
txMeta.loadingDefaults = false; txMeta.loadingDefaults = false;
this.txStateManager.updateTx(txMeta, 'Failed to calculate gas defaults.'); this.txStateManager.updateTransaction(
txMeta,
'Failed to calculate gas defaults.',
);
throw error; throw error;
} }
txMeta.loadingDefaults = false; txMeta.loadingDefaults = false;
// save txMeta // save txMeta
this.txStateManager.updateTx(txMeta, 'Added new unapproved transaction.'); this.txStateManager.updateTransaction(
txMeta,
'Added new unapproved transaction.',
);
return txMeta; return txMeta;
} }
@ -306,7 +312,7 @@ export default class TransactionController extends EventEmitter {
} = await this._getDefaultGasLimit(txMeta, getCodeResponse); } = await this._getDefaultGasLimit(txMeta, getCodeResponse);
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
txMeta = this.txStateManager.getTx(txMeta.id); txMeta = this.txStateManager.getTransaction(txMeta.id);
if (simulationFails) { if (simulationFails) {
txMeta.simulationFails = simulationFails; txMeta.simulationFails = simulationFails;
} }
@ -386,7 +392,7 @@ export default class TransactionController extends EventEmitter {
* @returns {txMeta} * @returns {txMeta}
*/ */
async createCancelTransaction(originalTxId, customGasPrice, customGasLimit) { async createCancelTransaction(originalTxId, customGasPrice, customGasLimit) {
const originalTxMeta = this.txStateManager.getTx(originalTxId); const originalTxMeta = this.txStateManager.getTransaction(originalTxId);
const { txParams } = originalTxMeta; const { txParams } = originalTxMeta;
const { gasPrice: lastGasPrice, from, nonce } = txParams; const { gasPrice: lastGasPrice, from, nonce } = txParams;
@ -408,7 +414,7 @@ export default class TransactionController extends EventEmitter {
type: TRANSACTION_TYPES.CANCEL, type: TRANSACTION_TYPES.CANCEL,
}); });
this.addTx(newTxMeta); this.addTransaction(newTxMeta);
await this.approveTransaction(newTxMeta.id); await this.approveTransaction(newTxMeta.id);
return newTxMeta; return newTxMeta;
} }
@ -424,7 +430,7 @@ export default class TransactionController extends EventEmitter {
* @returns {txMeta} * @returns {txMeta}
*/ */
async createSpeedUpTransaction(originalTxId, customGasPrice, customGasLimit) { async createSpeedUpTransaction(originalTxId, customGasPrice, customGasLimit) {
const originalTxMeta = this.txStateManager.getTx(originalTxId); const originalTxMeta = this.txStateManager.getTransaction(originalTxId);
const { txParams } = originalTxMeta; const { txParams } = originalTxMeta;
const { gasPrice: lastGasPrice } = txParams; const { gasPrice: lastGasPrice } = txParams;
@ -447,7 +453,7 @@ export default class TransactionController extends EventEmitter {
newTxMeta.txParams.gas = customGasLimit; newTxMeta.txParams.gas = customGasLimit;
} }
this.addTx(newTxMeta); this.addTransaction(newTxMeta);
await this.approveTransaction(newTxMeta.id); await this.approveTransaction(newTxMeta.id);
return newTxMeta; return newTxMeta;
} }
@ -457,7 +463,10 @@ export default class TransactionController extends EventEmitter {
@param {Object} txMeta - the updated txMeta @param {Object} txMeta - the updated txMeta
*/ */
async updateTransaction(txMeta) { async updateTransaction(txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user updated transaction'); this.txStateManager.updateTransaction(
txMeta,
'confTx: user updated transaction',
);
} }
/** /**
@ -465,7 +474,10 @@ export default class TransactionController extends EventEmitter {
@param {Object} txMeta @param {Object} txMeta
*/ */
async updateAndApproveTransaction(txMeta) { async updateAndApproveTransaction(txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction'); this.txStateManager.updateTransaction(
txMeta,
'confTx: user approved transaction',
);
await this.approveTransaction(txMeta.id); await this.approveTransaction(txMeta.id);
} }
@ -492,7 +504,7 @@ export default class TransactionController extends EventEmitter {
// approve // approve
this.txStateManager.setTxStatusApproved(txId); this.txStateManager.setTxStatusApproved(txId);
// get next nonce // get next nonce
const txMeta = this.txStateManager.getTx(txId); const txMeta = this.txStateManager.getTransaction(txId);
const fromAddress = txMeta.txParams.from; const fromAddress = txMeta.txParams.from;
// wait for a nonce // wait for a nonce
let { customNonceValue } = txMeta; let { customNonceValue } = txMeta;
@ -513,7 +525,10 @@ export default class TransactionController extends EventEmitter {
if (customNonceValue) { if (customNonceValue) {
txMeta.nonceDetails.customNonceValue = customNonceValue; txMeta.nonceDetails.customNonceValue = customNonceValue;
} }
this.txStateManager.updateTx(txMeta, 'transactions#approveTransaction'); this.txStateManager.updateTransaction(
txMeta,
'transactions#approveTransaction',
);
// sign transaction // sign transaction
const rawTx = await this.signTransaction(txId); const rawTx = await this.signTransaction(txId);
await this.publishTransaction(txId, rawTx); await this.publishTransaction(txId, rawTx);
@ -543,7 +558,7 @@ export default class TransactionController extends EventEmitter {
@returns {string} rawTx @returns {string} rawTx
*/ */
async signTransaction(txId) { async signTransaction(txId) {
const txMeta = this.txStateManager.getTx(txId); const txMeta = this.txStateManager.getTransaction(txId);
// add network/chain id // add network/chain id
const chainId = this.getChainId(); const chainId = this.getChainId();
const txParams = { ...txMeta.txParams, chainId }; const txParams = { ...txMeta.txParams, chainId };
@ -558,7 +573,7 @@ export default class TransactionController extends EventEmitter {
txMeta.s = ethUtil.bufferToHex(ethTx.s); txMeta.s = ethUtil.bufferToHex(ethTx.s);
txMeta.v = ethUtil.bufferToHex(ethTx.v); txMeta.v = ethUtil.bufferToHex(ethTx.v);
this.txStateManager.updateTx( this.txStateManager.updateTransaction(
txMeta, txMeta,
'transactions#signTransaction: add r, s, v values', 'transactions#signTransaction: add r, s, v values',
); );
@ -576,13 +591,16 @@ export default class TransactionController extends EventEmitter {
@returns {Promise<void>} @returns {Promise<void>}
*/ */
async publishTransaction(txId, rawTx) { async publishTransaction(txId, rawTx) {
const txMeta = this.txStateManager.getTx(txId); const txMeta = this.txStateManager.getTransaction(txId);
txMeta.rawTx = rawTx; txMeta.rawTx = rawTx;
if (txMeta.type === TRANSACTION_TYPES.SWAP) { if (txMeta.type === TRANSACTION_TYPES.SWAP) {
const preTxBalance = await this.query.getBalance(txMeta.txParams.from); const preTxBalance = await this.query.getBalance(txMeta.txParams.from);
txMeta.preTxBalance = preTxBalance.toString(16); txMeta.preTxBalance = preTxBalance.toString(16);
} }
this.txStateManager.updateTx(txMeta, 'transactions#publishTransaction'); this.txStateManager.updateTransaction(
txMeta,
'transactions#publishTransaction',
);
let txHash; let txHash;
try { try {
txHash = await this.query.sendRawTransaction(rawTx); txHash = await this.query.sendRawTransaction(rawTx);
@ -608,7 +626,7 @@ export default class TransactionController extends EventEmitter {
async confirmTransaction(txId, txReceipt) { async confirmTransaction(txId, txReceipt) {
// get the txReceipt before marking the transaction confirmed // get the txReceipt before marking the transaction confirmed
// to ensure the receipt is gotten before the ui revives the tx // to ensure the receipt is gotten before the ui revives the tx
const txMeta = this.txStateManager.getTx(txId); const txMeta = this.txStateManager.getTransaction(txId);
if (!txMeta) { if (!txMeta) {
return; return;
@ -629,22 +647,22 @@ export default class TransactionController extends EventEmitter {
this.txStateManager.setTxStatusConfirmed(txId); this.txStateManager.setTxStatusConfirmed(txId);
this._markNonceDuplicatesDropped(txId); this._markNonceDuplicatesDropped(txId);
this.txStateManager.updateTx( this.txStateManager.updateTransaction(
txMeta, txMeta,
'transactions#confirmTransaction - add txReceipt', 'transactions#confirmTransaction - add txReceipt',
); );
if (txMeta.type === TRANSACTION_TYPES.SWAP) { if (txMeta.type === TRANSACTION_TYPES.SWAP) {
const postTxBalance = await this.query.getBalance(txMeta.txParams.from); const postTxBalance = await this.query.getBalance(txMeta.txParams.from);
const latestTxMeta = this.txStateManager.getTx(txId); const latestTxMeta = this.txStateManager.getTransaction(txId);
const approvalTxMeta = latestTxMeta.approvalTxId const approvalTxMeta = latestTxMeta.approvalTxId
? this.txStateManager.getTx(latestTxMeta.approvalTxId) ? this.txStateManager.getTransaction(latestTxMeta.approvalTxId)
: null; : null;
latestTxMeta.postTxBalance = postTxBalance.toString(16); latestTxMeta.postTxBalance = postTxBalance.toString(16);
this.txStateManager.updateTx( this.txStateManager.updateTransaction(
latestTxMeta, latestTxMeta,
'transactions#confirmTransaction - add postTxBalance', 'transactions#confirmTransaction - add postTxBalance',
); );
@ -672,9 +690,9 @@ export default class TransactionController extends EventEmitter {
*/ */
setTxHash(txId, txHash) { setTxHash(txId, txHash) {
// Add the tx hash to the persisted meta-tx object // Add the tx hash to the persisted meta-tx object
const txMeta = this.txStateManager.getTx(txId); const txMeta = this.txStateManager.getTransaction(txId);
txMeta.hash = txHash; txMeta.hash = txHash;
this.txStateManager.updateTx(txMeta, 'transactions#setTxHash'); this.txStateManager.updateTransaction(txMeta, 'transactions#setTxHash');
} }
// //
@ -704,8 +722,7 @@ export default class TransactionController extends EventEmitter {
this.txStateManager.getPendingTransactions(account).length; this.txStateManager.getPendingTransactions(account).length;
/** see txStateManager */ /** see txStateManager */
this.getFilteredTxList = (opts) => this.getTransactions = (opts) => this.txStateManager.getTransactions(opts);
this.txStateManager.getFilteredTxList(opts);
} }
// called once on startup // called once on startup
@ -724,23 +741,25 @@ export default class TransactionController extends EventEmitter {
_onBootCleanUp() { _onBootCleanUp() {
this.txStateManager this.txStateManager
.getFilteredTxList({ .getTransactions({
status: TRANSACTION_STATUSES.UNAPPROVED, searchCriteria: {
loadingDefaults: true, status: TRANSACTION_STATUSES.UNAPPROVED,
loadingDefaults: true,
},
}) })
.forEach((tx) => { .forEach((tx) => {
this.addTxGasDefaults(tx) this.addTxGasDefaults(tx)
.then((txMeta) => { .then((txMeta) => {
txMeta.loadingDefaults = false; txMeta.loadingDefaults = false;
this.txStateManager.updateTx( this.txStateManager.updateTransaction(
txMeta, txMeta,
'transactions: gas estimation for tx on boot', 'transactions: gas estimation for tx on boot',
); );
}) })
.catch((error) => { .catch((error) => {
const txMeta = this.txStateManager.getTx(tx.id); const txMeta = this.txStateManager.getTransaction(tx.id);
txMeta.loadingDefaults = false; txMeta.loadingDefaults = false;
this.txStateManager.updateTx( this.txStateManager.updateTransaction(
txMeta, txMeta,
'failed to estimate gas during boot cleanup.', 'failed to estimate gas during boot cleanup.',
); );
@ -749,8 +768,10 @@ export default class TransactionController extends EventEmitter {
}); });
this.txStateManager this.txStateManager
.getFilteredTxList({ .getTransactions({
status: TRANSACTION_STATUSES.APPROVED, searchCriteria: {
status: TRANSACTION_STATUSES.APPROVED,
},
}) })
.forEach((txMeta) => { .forEach((txMeta) => {
const txSignError = new Error( const txSignError = new Error(
@ -771,7 +792,7 @@ export default class TransactionController extends EventEmitter {
); );
this._setupBlockTrackerListener(); this._setupBlockTrackerListener();
this.pendingTxTracker.on('tx:warning', (txMeta) => { this.pendingTxTracker.on('tx:warning', (txMeta) => {
this.txStateManager.updateTx( this.txStateManager.updateTransaction(
txMeta, txMeta,
'transactions/pending-tx-tracker#event: tx:warning', 'transactions/pending-tx-tracker#event: tx:warning',
); );
@ -790,7 +811,7 @@ export default class TransactionController extends EventEmitter {
this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => { this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => {
if (!txMeta.firstRetryBlockNumber) { if (!txMeta.firstRetryBlockNumber) {
txMeta.firstRetryBlockNumber = latestBlockNumber; txMeta.firstRetryBlockNumber = latestBlockNumber;
this.txStateManager.updateTx( this.txStateManager.updateTransaction(
txMeta, txMeta,
'transactions/pending-tx-tracker#event: tx:block-update', 'transactions/pending-tx-tracker#event: tx:block-update',
); );
@ -801,7 +822,7 @@ export default class TransactionController extends EventEmitter {
txMeta.retryCount = 0; txMeta.retryCount = 0;
} }
txMeta.retryCount += 1; txMeta.retryCount += 1;
this.txStateManager.updateTx( this.txStateManager.updateTransaction(
txMeta, txMeta,
'transactions/pending-tx-tracker#event: tx:retry', 'transactions/pending-tx-tracker#event: tx:retry',
); );
@ -878,9 +899,11 @@ export default class TransactionController extends EventEmitter {
*/ */
_markNonceDuplicatesDropped(txId) { _markNonceDuplicatesDropped(txId) {
// get the confirmed transactions nonce and from address // get the confirmed transactions nonce and from address
const txMeta = this.txStateManager.getTx(txId); const txMeta = this.txStateManager.getTransaction(txId);
const { nonce, from } = txMeta.txParams; const { nonce, from } = txMeta.txParams;
const sameNonceTxs = this.txStateManager.getFilteredTxList({ nonce, from }); const sameNonceTxs = this.txStateManager.getTransactions({
searchCriteria: { nonce, from },
});
if (!sameNonceTxs.length) { if (!sameNonceTxs.length) {
return; return;
} }
@ -890,7 +913,7 @@ export default class TransactionController extends EventEmitter {
return; return;
} }
otherTxMeta.replacedBy = txMeta.hash; otherTxMeta.replacedBy = txMeta.hash;
this.txStateManager.updateTx( this.txStateManager.updateTransaction(
txMeta, txMeta,
'transactions/pending-tx-tracker#event: tx:confirmed reference to confirmed txHash with same nonce', 'transactions/pending-tx-tracker#event: tx:confirmed reference to confirmed txHash with same nonce',
); );
@ -936,9 +959,9 @@ export default class TransactionController extends EventEmitter {
*/ */
_updateMemstore() { _updateMemstore() {
const unapprovedTxs = this.txStateManager.getUnapprovedTxList(); const unapprovedTxs = this.txStateManager.getUnapprovedTxList();
const currentNetworkTxList = this.txStateManager.getTxList( const currentNetworkTxList = this.txStateManager.getTransactions({
MAX_MEMSTORE_TX_LIST_SIZE, limit: MAX_MEMSTORE_TX_LIST_SIZE,
); });
this.memStore.updateState({ unapprovedTxs, currentNetworkTxList }); this.memStore.updateState({ unapprovedTxs, currentNetworkTxList });
} }

View File

@ -20,6 +20,9 @@ const noop = () => true;
const currentNetworkId = '42'; const currentNetworkId = '42';
const currentChainId = '0x2a'; const currentChainId = '0x2a';
const VALID_ADDRESS = '0x0000000000000000000000000000000000000000';
const VALID_ADDRESS_TWO = '0x0000000000000000000000000000000000000001';
describe('Transaction Controller', function () { describe('Transaction Controller', function () {
let txController, provider, providerResultStub, fromAccount; let txController, provider, providerResultStub, fromAccount;
@ -81,26 +84,35 @@ describe('Transaction Controller', function () {
describe('#getUnapprovedTxCount', function () { describe('#getUnapprovedTxCount', function () {
it('should return the number of unapproved txs', function () { it('should return the number of unapproved txs', function () {
txController.txStateManager._saveTxList([ txController.txStateManager._addTransactionsToState([
{ {
id: 1, id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED, status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}], history: [{}],
}, },
{ {
id: 2, id: 2,
status: TRANSACTION_STATUSES.UNAPPROVED, status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}], history: [{}],
}, },
{ {
id: 3, id: 3,
status: TRANSACTION_STATUSES.UNAPPROVED, status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}], history: [{}],
}, },
]); ]);
@ -111,26 +123,35 @@ describe('Transaction Controller', function () {
describe('#getPendingTxCount', function () { describe('#getPendingTxCount', function () {
it('should return the number of pending txs', function () { it('should return the number of pending txs', function () {
txController.txStateManager._saveTxList([ txController.txStateManager._addTransactionsToState([
{ {
id: 1, id: 1,
status: TRANSACTION_STATUSES.SUBMITTED, status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}], history: [{}],
}, },
{ {
id: 2, id: 2,
status: TRANSACTION_STATUSES.SUBMITTED, status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}], history: [{}],
}, },
{ {
id: 3, id: 3,
status: TRANSACTION_STATUSES.SUBMITTED, status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}], history: [{}],
}, },
]); ]);
@ -146,7 +167,7 @@ describe('Transaction Controller', function () {
from: address, from: address,
to: '0xc684832530fcbddae4b4230a47e991ddcec2831d', to: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
}; };
txController.txStateManager._saveTxList([ txController.txStateManager._addTransactionsToState([
{ {
id: 0, id: 0,
status: TRANSACTION_STATUSES.CONFIRMED, status: TRANSACTION_STATUSES.CONFIRMED,
@ -232,17 +253,19 @@ describe('Transaction Controller', function () {
txParams, txParams,
history: [{}], history: [{}],
}; };
txController.txStateManager._saveTxList([txMeta]); txController.txStateManager._addTransactionsToState([txMeta]);
stub = sinon stub = sinon
.stub(txController, 'addUnapprovedTransaction') .stub(txController, 'addUnapprovedTransaction')
.callsFake(() => { .callsFake(() => {
txController.emit('newUnapprovedTx', txMeta); txController.emit('newUnapprovedTx', txMeta);
return Promise.resolve(txController.txStateManager.addTx(txMeta)); return Promise.resolve(
txController.txStateManager.addTransaction(txMeta),
);
}); });
}); });
afterEach(function () { afterEach(function () {
txController.txStateManager._saveTxList([]); txController.txStateManager._addTransactionsToState([]);
stub.restore(); stub.restore();
}); });
@ -312,7 +335,7 @@ describe('Transaction Controller', function () {
'should have added 0x0 as the value', 'should have added 0x0 as the value',
); );
const memTxMeta = txController.txStateManager.getTx(txMeta.id); const memTxMeta = txController.txStateManager.getTransaction(txMeta.id);
assert.deepEqual(txMeta, memTxMeta); assert.deepEqual(txMeta, memTxMeta);
}); });
@ -353,12 +376,15 @@ describe('Transaction Controller', function () {
describe('#addTxGasDefaults', function () { describe('#addTxGasDefaults', function () {
it('should add the tx defaults if their are none', async function () { it('should add the tx defaults if their are none', async function () {
txController.txStateManager._saveTxList([ txController.txStateManager._addTransactionsToState([
{ {
id: 1, id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED, status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}], history: [{}],
}, },
]); ]);
@ -386,13 +412,16 @@ describe('Transaction Controller', function () {
}); });
}); });
describe('#addTx', function () { describe('#addTransaction', function () {
it('should emit updates', function (done) { it('should emit updates', function (done) {
const txMeta = { const txMeta = {
id: '1', id: '1',
status: TRANSACTION_STATUSES.UNAPPROVED, status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
}; };
const eventNames = [ const eventNames = [
@ -419,7 +448,7 @@ describe('Transaction Controller', function () {
done(); done();
}) })
.catch(done); .catch(done);
txController.addTx(txMeta); txController.addTransaction(txMeta);
}); });
}); });
@ -431,6 +460,8 @@ describe('Transaction Controller', function () {
status: TRANSACTION_STATUSES.UNAPPROVED, status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: { txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: originalValue, nonce: originalValue,
gas: originalValue, gas: originalValue,
gasPrice: originalValue, gasPrice: originalValue,
@ -440,7 +471,7 @@ describe('Transaction Controller', function () {
this.timeout(15000); this.timeout(15000);
const wrongValue = '0x05'; const wrongValue = '0x05';
txController.addTx(txMeta); txController.addTransaction(txMeta);
providerResultStub.eth_gasPrice = wrongValue; providerResultStub.eth_gasPrice = wrongValue;
providerResultStub.eth_estimateGas = '0x5209'; providerResultStub.eth_estimateGas = '0x5209';
@ -456,7 +487,7 @@ describe('Transaction Controller', function () {
}); });
await txController.approveTransaction(txMeta.id); await txController.approveTransaction(txMeta.id);
const result = txController.txStateManager.getTx(txMeta.id); const result = txController.txStateManager.getTransaction(txMeta.id);
const params = result.txParams; const params = result.txParams;
assert.equal(params.gas, originalValue, 'gas unmodified'); assert.equal(params.gas, originalValue, 'gas unmodified');
@ -474,12 +505,15 @@ describe('Transaction Controller', function () {
describe('#sign replay-protected tx', function () { describe('#sign replay-protected tx', function () {
it('prepares a tx with the chainId set', async function () { it('prepares a tx with the chainId set', async function () {
txController.addTx( txController.addTransaction(
{ {
id: '1', id: '1',
status: TRANSACTION_STATUSES.UNAPPROVED, status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
}, },
noop, noop,
); );
@ -503,9 +537,9 @@ describe('Transaction Controller', function () {
}, },
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
}; };
txController.txStateManager.addTx(txMeta); txController.txStateManager.addTransaction(txMeta);
const approvalPromise = txController.updateAndApproveTransaction(txMeta); const approvalPromise = txController.updateAndApproveTransaction(txMeta);
const tx = txController.txStateManager.getTx(1); const tx = txController.txStateManager.getTransaction(1);
assert.equal(tx.status, TRANSACTION_STATUSES.APPROVED); assert.equal(tx.status, TRANSACTION_STATUSES.APPROVED);
await approvalPromise; await approvalPromise;
}); });
@ -520,53 +554,74 @@ describe('Transaction Controller', function () {
describe('#cancelTransaction', function () { describe('#cancelTransaction', function () {
it('should emit a status change to rejected', function (done) { it('should emit a status change to rejected', function (done) {
txController.txStateManager._saveTxList([ txController.txStateManager._addTransactionsToState([
{ {
id: 0, id: 0,
status: TRANSACTION_STATUSES.UNAPPROVED, status: TRANSACTION_STATUSES.UNAPPROVED,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
history: [{}], history: [{}],
}, },
{ {
id: 1, id: 1,
status: TRANSACTION_STATUSES.REJECTED, status: TRANSACTION_STATUSES.REJECTED,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
history: [{}], history: [{}],
}, },
{ {
id: 2, id: 2,
status: TRANSACTION_STATUSES.APPROVED, status: TRANSACTION_STATUSES.APPROVED,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
history: [{}], history: [{}],
}, },
{ {
id: 3, id: 3,
status: TRANSACTION_STATUSES.SIGNED, status: TRANSACTION_STATUSES.SIGNED,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
history: [{}], history: [{}],
}, },
{ {
id: 4, id: 4,
status: TRANSACTION_STATUSES.SUBMITTED, status: TRANSACTION_STATUSES.SUBMITTED,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
history: [{}], history: [{}],
}, },
{ {
id: 5, id: 5,
status: TRANSACTION_STATUSES.CONFIRMED, status: TRANSACTION_STATUSES.CONFIRMED,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
history: [{}], history: [{}],
}, },
{ {
id: 6, id: 6,
status: TRANSACTION_STATUSES.FAILED, status: TRANSACTION_STATUSES.FAILED,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
history: [{}], history: [{}],
}, },
@ -591,13 +646,13 @@ describe('Transaction Controller', function () {
}); });
describe('#createSpeedUpTransaction', function () { describe('#createSpeedUpTransaction', function () {
let addTxSpy; let addTransactionSpy;
let approveTransactionSpy; let approveTransactionSpy;
let txParams; let txParams;
let expectedTxParams; let expectedTxParams;
beforeEach(function () { beforeEach(function () {
addTxSpy = sinon.spy(txController, 'addTx'); addTransactionSpy = sinon.spy(txController, 'addTransaction');
approveTransactionSpy = sinon.spy(txController, 'approveTransaction'); approveTransactionSpy = sinon.spy(txController, 'approveTransaction');
txParams = { txParams = {
@ -607,7 +662,7 @@ describe('Transaction Controller', function () {
gas: '0x5209', gas: '0x5209',
gasPrice: '0xa', gasPrice: '0xa',
}; };
txController.txStateManager._saveTxList([ txController.txStateManager._addTransactionsToState([
{ {
id: 1, id: 1,
status: TRANSACTION_STATUSES.SUBMITTED, status: TRANSACTION_STATUSES.SUBMITTED,
@ -621,18 +676,18 @@ describe('Transaction Controller', function () {
}); });
afterEach(function () { afterEach(function () {
addTxSpy.restore(); addTransactionSpy.restore();
approveTransactionSpy.restore(); approveTransactionSpy.restore();
}); });
it('should call this.addTx and this.approveTransaction with the expected args', async function () { it('should call this.addTransaction and this.approveTransaction with the expected args', async function () {
await txController.createSpeedUpTransaction(1); await txController.createSpeedUpTransaction(1);
assert.equal(addTxSpy.callCount, 1); assert.equal(addTransactionSpy.callCount, 1);
const addTxArgs = addTxSpy.getCall(0).args[0]; const addTransactionArgs = addTransactionSpy.getCall(0).args[0];
assert.deepEqual(addTxArgs.txParams, expectedTxParams); assert.deepEqual(addTransactionArgs.txParams, expectedTxParams);
const { lastGasPrice, type } = addTxArgs; const { lastGasPrice, type } = addTransactionArgs;
assert.deepEqual( assert.deepEqual(
{ lastGasPrice, type }, { lastGasPrice, type },
{ {
@ -674,7 +729,10 @@ describe('Transaction Controller', function () {
txMeta = { txMeta = {
id: 1, id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED, status: TRANSACTION_STATUSES.UNAPPROVED,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
}; };
providerResultStub.eth_sendRawTransaction = hash; providerResultStub.eth_sendRawTransaction = hash;
@ -683,9 +741,9 @@ describe('Transaction Controller', function () {
it('should publish a tx, updates the rawTx when provided a one', async function () { it('should publish a tx, updates the rawTx when provided a one', async function () {
const rawTx = const rawTx =
'0x477b2e6553c917af0db0388ae3da62965ff1a184558f61b749d1266b2e6d024c'; '0x477b2e6553c917af0db0388ae3da62965ff1a184558f61b749d1266b2e6d024c';
txController.txStateManager.addTx(txMeta); txController.txStateManager.addTransaction(txMeta);
await txController.publishTransaction(txMeta.id, rawTx); await txController.publishTransaction(txMeta.id, rawTx);
const publishedTx = txController.txStateManager.getTx(1); const publishedTx = txController.txStateManager.getTransaction(1);
assert.equal(publishedTx.hash, hash); assert.equal(publishedTx.hash, hash);
assert.equal(publishedTx.status, TRANSACTION_STATUSES.SUBMITTED); assert.equal(publishedTx.status, TRANSACTION_STATUSES.SUBMITTED);
}); });
@ -696,9 +754,9 @@ describe('Transaction Controller', function () {
}; };
const rawTx = const rawTx =
'0xf86204831e848082520894f231d46dd78806e1dd93442cf33c7671f853874880802ca05f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57a00259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a'; '0xf86204831e848082520894f231d46dd78806e1dd93442cf33c7671f853874880802ca05f973e540f2d3c2f06d3725a626b75247593cb36477187ae07ecfe0a4db3cf57a00259b52ee8c58baaa385fb05c3f96116e58de89bcc165cb3bfdfc708672fed8a';
txController.txStateManager.addTx(txMeta); txController.txStateManager.addTransaction(txMeta);
await txController.publishTransaction(txMeta.id, rawTx); await txController.publishTransaction(txMeta.id, rawTx);
const publishedTx = txController.txStateManager.getTx(1); const publishedTx = txController.txStateManager.getTransaction(1);
assert.equal( assert.equal(
publishedTx.hash, publishedTx.hash,
'0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09', '0x2cc5a25744486f7383edebbf32003e5a66e18135799593d6b5cdd2bb43674f09',
@ -709,62 +767,92 @@ describe('Transaction Controller', function () {
describe('#_markNonceDuplicatesDropped', function () { describe('#_markNonceDuplicatesDropped', function () {
it('should mark all nonce duplicates as dropped without marking the confirmed transaction as dropped', function () { it('should mark all nonce duplicates as dropped without marking the confirmed transaction as dropped', function () {
txController.txStateManager._saveTxList([ txController.txStateManager._addTransactionsToState([
{ {
id: 1, id: 1,
status: TRANSACTION_STATUSES.CONFIRMED, status: TRANSACTION_STATUSES.CONFIRMED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
history: [{}], history: [{}],
txParams: { nonce: '0x01' }, txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
}, },
{ {
id: 2, id: 2,
status: TRANSACTION_STATUSES.SUBMITTED, status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
history: [{}], history: [{}],
txParams: { nonce: '0x01' }, txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
}, },
{ {
id: 3, id: 3,
status: TRANSACTION_STATUSES.SUBMITTED, status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
history: [{}], history: [{}],
txParams: { nonce: '0x01' }, txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
}, },
{ {
id: 4, id: 4,
status: TRANSACTION_STATUSES.SUBMITTED, status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
history: [{}], history: [{}],
txParams: { nonce: '0x01' }, txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
}, },
{ {
id: 5, id: 5,
status: TRANSACTION_STATUSES.SUBMITTED, status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
history: [{}], history: [{}],
txParams: { nonce: '0x01' }, txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
}, },
{ {
id: 6, id: 6,
status: TRANSACTION_STATUSES.SUBMITTED, status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
history: [{}], history: [{}],
txParams: { nonce: '0x01' }, txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
}, },
{ {
id: 7, id: 7,
status: TRANSACTION_STATUSES.SUBMITTED, status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
history: [{}], history: [{}],
txParams: { nonce: '0x01' }, txParams: {
to: VALID_ADDRESS_TWO,
from: VALID_ADDRESS,
nonce: '0x01',
},
}, },
]); ]);
txController._markNonceDuplicatesDropped(1); txController._markNonceDuplicatesDropped(1);
const confirmedTx = txController.txStateManager.getTx(1); const confirmedTx = txController.txStateManager.getTransaction(1);
const droppedTxs = txController.txStateManager.getFilteredTxList({ const droppedTxs = txController.txStateManager.getTransactions({
nonce: '0x01', searchCriteria: {
status: TRANSACTION_STATUSES.DROPPED, nonce: '0x01',
status: TRANSACTION_STATUSES.DROPPED,
},
}); });
assert.equal( assert.equal(
confirmedTx.status, confirmedTx.status,
@ -927,53 +1015,74 @@ describe('Transaction Controller', function () {
describe('#getPendingTransactions', function () { describe('#getPendingTransactions', function () {
it('should show only submitted and approved transactions as pending transaction', function () { it('should show only submitted and approved transactions as pending transaction', function () {
txController.txStateManager._saveTxList([ txController.txStateManager._addTransactionsToState([
{ {
id: 1, id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED, status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
}, },
{ {
id: 2, id: 2,
status: TRANSACTION_STATUSES.REJECTED, status: TRANSACTION_STATUSES.REJECTED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}], history: [{}],
}, },
{ {
id: 3, id: 3,
status: TRANSACTION_STATUSES.APPROVED, status: TRANSACTION_STATUSES.APPROVED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}], history: [{}],
}, },
{ {
id: 4, id: 4,
status: TRANSACTION_STATUSES.SIGNED, status: TRANSACTION_STATUSES.SIGNED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}], history: [{}],
}, },
{ {
id: 5, id: 5,
status: TRANSACTION_STATUSES.SUBMITTED, status: TRANSACTION_STATUSES.SUBMITTED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}], history: [{}],
}, },
{ {
id: 6, id: 6,
status: TRANSACTION_STATUSES.CONFIRMED, status: TRANSACTION_STATUSES.CONFIRMED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}], history: [{}],
}, },
{ {
id: 7, id: 7,
status: TRANSACTION_STATUSES.FAILED, status: TRANSACTION_STATUSES.FAILED,
metamaskNetworkId: currentNetworkId, metamaskNetworkId: currentNetworkId,
txParams: {}, txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}], history: [{}],
}, },
]); ]);

View File

@ -14,6 +14,12 @@ const normalizers = {
gasPrice: (gasPrice) => addHexPrefix(gasPrice), gasPrice: (gasPrice) => addHexPrefix(gasPrice),
}; };
export function normalizeAndValidateTxParams(txParams, lowerCase = true) {
const normalizedTxParams = normalizeTxParams(txParams, lowerCase);
validateTxParams(normalizedTxParams);
return normalizedTxParams;
}
/** /**
* Normalizes the given txParams * Normalizes the given txParams
* @param {Object} txParams - The transaction params * @param {Object} txParams - The transaction params
@ -49,22 +55,48 @@ export function validateTxParams(txParams) {
); );
} }
validateFrom(txParams); Object.entries(txParams).forEach(([key, value]) => {
validateRecipient(txParams); // validate types
if ('value' in txParams) { switch (key) {
const value = txParams.value.toString(); case 'from':
if (value.includes('-')) { validateFrom(txParams);
throw ethErrors.rpc.invalidParams( break;
`Invalid transaction value "${txParams.value}": not a positive number.`, case 'to':
); validateRecipient(txParams);
} break;
case 'value':
if (typeof value !== 'string') {
throw ethErrors.rpc.invalidParams(
`Invalid transaction params: ${key} is not a string. got: (${value})`,
);
}
if (value.toString().includes('-')) {
throw ethErrors.rpc.invalidParams(
`Invalid transaction value "${value}": not a positive number.`,
);
}
if (value.includes('.')) { if (value.toString().includes('.')) {
throw ethErrors.rpc.invalidParams( throw ethErrors.rpc.invalidParams(
`Invalid transaction value of "${txParams.value}": number must be in wei.`, `Invalid transaction value of "${value}": number must be in wei.`,
); );
}
break;
case 'chainId':
if (typeof value !== 'number' && typeof value !== 'string') {
throw ethErrors.rpc.invalidParams(
`Invalid transaction params: ${key} is not a Number or hex string. got: (${value})`,
);
}
break;
default:
if (typeof value !== 'string') {
throw ethErrors.rpc.invalidParams(
`Invalid transaction params: ${key} is not a string. got: (${value})`,
);
}
} }
} });
} }
/** /**

View File

@ -1,6 +1,7 @@
import EventEmitter from 'safe-event-emitter'; import EventEmitter from 'safe-event-emitter';
import { ObservableStore } from '@metamask/obs-store'; import { ObservableStore } from '@metamask/obs-store';
import log from 'loglevel'; import log from 'loglevel';
import { keyBy, mapValues, omitBy, pickBy, sortBy } from 'lodash';
import createId from '../../../../shared/modules/random-id'; import createId from '../../../../shared/modules/random-id';
import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction'; import { TRANSACTION_STATUSES } from '../../../../shared/constants/transaction';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller'; import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
@ -10,11 +11,29 @@ import {
replayHistory, replayHistory,
snapshotFromTxMeta, snapshotFromTxMeta,
} from './lib/tx-state-history-helpers'; } from './lib/tx-state-history-helpers';
import { getFinalStates, normalizeTxParams } from './lib/util'; import { getFinalStates, normalizeAndValidateTxParams } from './lib/util';
/** /**
* TransactionStatuses reimported from the shared transaction constants file * TransactionStatuses reimported from the shared transaction constants file
* @typedef {import('../../../../shared/constants/transaction').TransactionStatuses} TransactionStatuses * @typedef {import(
* '../../../../shared/constants/transaction'
* ).TransactionStatusString} TransactionStatusString
*/
/**
* @typedef {import('../../../../shared/constants/transaction').TxParams} TxParams
*/
/**
* @typedef {import(
* '../../../../shared/constants/transaction'
* ).TransactionMeta} TransactionMeta
*/
/**
* @typedef {Object} TransactionState
* @property {Record<string, TransactionMeta>} transactions - TransactionMeta
* keyed by the transaction's id.
*/ */
/** /**
@ -22,7 +41,8 @@ import { getFinalStates, normalizeTxParams } from './lib/util';
* storing the transaction. It also has some convenience methods for finding * storing the transaction. It also has some convenience methods for finding
* subsets of transactions. * subsets of transactions.
* @param {Object} opts * @param {Object} opts
* @param {Object} [opts.initState={ transactions: [] }] - initial transactions list with the key transaction {Array} * @param {TransactionState} [opts.initState={ transactions: {} }] - initial
* transactions list keyed by id
* @param {number} [opts.txHistoryLimit] - limit for how many finished * @param {number} [opts.txHistoryLimit] - limit for how many finished
* transactions can hang around in state * transactions can hang around in state
* @param {Function} opts.getNetwork - return network number * @param {Function} opts.getNetwork - return network number
@ -32,15 +52,25 @@ export default class TransactionStateManager extends EventEmitter {
constructor({ initState, txHistoryLimit, getNetwork, getCurrentChainId }) { constructor({ initState, txHistoryLimit, getNetwork, getCurrentChainId }) {
super(); super();
this.store = new ObservableStore({ transactions: [], ...initState }); this.store = new ObservableStore({
transactions: {},
...initState,
});
this.txHistoryLimit = txHistoryLimit; this.txHistoryLimit = txHistoryLimit;
this.getNetwork = getNetwork; this.getNetwork = getNetwork;
this.getCurrentChainId = getCurrentChainId; this.getCurrentChainId = getCurrentChainId;
} }
/** /**
* @param {Object} opts - the object to use when overwriting defaults * Generates a TransactionMeta object consisting of the fields required for
* @returns {txMeta} the default txMeta object * use throughout the extension. The argument here will override everything
* in the resulting transaction meta.
*
* TODO: Don't overwrite everything?
*
* @param {Partial<TransactionMeta>} opts - the object to use when
* overwriting default keys of the TransactionMeta
* @returns {TransactionMeta} the default txMeta object
*/ */
generateTxMeta(opts) { generateTxMeta(opts) {
const netId = this.getNetwork(); const netId = this.getNetwork();
@ -60,100 +90,70 @@ export default class TransactionStateManager extends EventEmitter {
} }
/** /**
* Returns the full tx list for the current network * Get an object containing all unapproved transactions for the current
* network. This is the only transaction fetching method that returns an
* object, so it doesn't use getTransactions like everything else.
* *
* The list is iterated backwards as new transactions are pushed onto it. * @returns {Record<string, TransactionMeta>} Unapproved transactions keyed
* * by id
* @param {number} [limit] - a limit for the number of transactions to return
* @returns {Object[]} The {@code txMeta}s, filtered to the current network
*/
getTxList(limit) {
const network = this.getNetwork();
const chainId = this.getCurrentChainId();
const fullTxList = this.getFullTxList();
const nonces = new Set();
const txs = [];
for (let i = fullTxList.length - 1; i > -1; i--) {
const txMeta = fullTxList[i];
if (transactionMatchesNetwork(txMeta, chainId, network) === false) {
continue;
}
if (limit !== undefined) {
const { nonce } = txMeta.txParams;
if (!nonces.has(nonce)) {
if (nonces.size < limit) {
nonces.add(nonce);
} else {
continue;
}
}
}
txs.unshift(txMeta);
}
return txs;
}
/**
* @returns {Array} of all the txMetas in store
*/
getFullTxList() {
return this.store.getState().transactions;
}
/**
* @returns {Array} the tx list with unapproved status
*/ */
getUnapprovedTxList() { getUnapprovedTxList() {
const txList = this.getTxsByMetaData( const chainId = this.getCurrentChainId();
'status', const network = this.getNetwork();
TRANSACTION_STATUSES.UNAPPROVED, return pickBy(
this.store.getState().transactions,
(transaction) =>
transaction.status === TRANSACTION_STATUSES.UNAPPROVED &&
transactionMatchesNetwork(transaction, chainId, network),
); );
return txList.reduce((result, tx) => {
result[tx.id] = tx;
return result;
}, {});
} }
/** /**
* @param {string} [address] - hex prefixed address to sort the txMetas for [optional] * Get all approved transactions for the current network. If an address is
* @returns {Array} the tx list with approved status if no address is provide * provided, the list will be further refined to only those transactions
* returns all txMetas with approved statuses for the current network * originating from the supplied address.
*
* @param {string} [address] - hex prefixed address to find transactions for.
* @returns {TransactionMeta[]} the filtered list of transactions
*/ */
getApprovedTransactions(address) { getApprovedTransactions(address) {
const opts = { status: TRANSACTION_STATUSES.APPROVED }; const searchCriteria = { status: TRANSACTION_STATUSES.APPROVED };
if (address) { if (address) {
opts.from = address; searchCriteria.from = address;
} }
return this.getFilteredTxList(opts); return this.getTransactions({ searchCriteria });
} }
/** /**
* @param {string} [address] - hex prefixed address to sort the txMetas for [optional] * Get all pending transactions for the current network. If an address is
* @returns {Array} the tx list submitted status if no address is provide * provided, the list will be further refined to only those transactions
* returns all txMetas with submitted statuses for the current network * originating from the supplied address.
*
* @param {string} [address] - hex prefixed address to find transactions for.
* @returns {TransactionMeta[]} the filtered list of transactions
*/ */
getPendingTransactions(address) { getPendingTransactions(address) {
const opts = { status: TRANSACTION_STATUSES.SUBMITTED }; const searchCriteria = { status: TRANSACTION_STATUSES.SUBMITTED };
if (address) { if (address) {
opts.from = address; searchCriteria.from = address;
} }
return this.getFilteredTxList(opts); return this.getTransactions({ searchCriteria });
} }
/** /**
@param {string} [address] - hex prefixed address to sort the txMetas for [optional] * Get all confirmed transactions for the current network. If an address is
@returns {Array} the tx list whose status is confirmed if no address is provide * provided, the list will be further refined to only those transactions
returns all txMetas who's status is confirmed for the current network * originating from the supplied address.
*/ *
* @param {string} [address] - hex prefixed address to find transactions for.
* @returns {TransactionMeta[]} the filtered list of transactions
*/
getConfirmedTransactions(address) { getConfirmedTransactions(address) {
const opts = { status: TRANSACTION_STATUSES.CONFIRMED }; const searchCriteria = { status: TRANSACTION_STATUSES.CONFIRMED };
if (address) { if (address) {
opts.from = address; searchCriteria.from = address;
} }
return this.getFilteredTxList(opts); return this.getTransactions({ searchCriteria });
} }
/** /**
@ -162,13 +162,14 @@ export default class TransactionStateManager extends EventEmitter {
* is in its final state. * is in its final state.
* it will also add the key `history` to the txMeta with the snap shot of * it will also add the key `history` to the txMeta with the snap shot of
* the original object * the original object
* @param {Object} txMeta * @param {TransactionMeta} txMeta - The TransactionMeta object to add.
* @returns {Object} the txMeta * @returns {TransactionMeta} The same TransactionMeta, but with validated
* txParams and transaction history.
*/ */
addTx(txMeta) { addTransaction(txMeta) {
// normalize and validate txParams if present // normalize and validate txParams if present
if (txMeta.txParams) { if (txMeta.txParams) {
txMeta.txParams = this.normalizeAndValidateTxParams(txMeta.txParams); txMeta.txParams = normalizeAndValidateTxParams(txMeta.txParams, false);
} }
this.once(`${txMeta.id}:signed`, () => { this.once(`${txMeta.id}:signed`, () => {
@ -183,42 +184,43 @@ export default class TransactionStateManager extends EventEmitter {
const snapshot = snapshotFromTxMeta(txMeta); const snapshot = snapshotFromTxMeta(txMeta);
txMeta.history.push(snapshot); txMeta.history.push(snapshot);
const transactions = this.getFullTxList(); const transactions = this.getTransactions({
filterToCurrentNetwork: false,
});
const txCount = transactions.length; const txCount = transactions.length;
const { txHistoryLimit } = this; const { txHistoryLimit } = this;
// checks if the length of the tx history is // checks if the length of the tx history is longer then desired persistence
// longer then desired persistence limit // limit and then if it is removes the oldest confirmed or rejected tx.
// and then if it is removes only confirmed // Pending or unapproved transactions will not be removed by this
// or rejected tx's. // operation.
// not tx's that are pending or unapproved //
// TODO: we are already limiting what we send to the UI, and in the future
// we will send UI only collected groups of transactions *per page* so at
// some point in the future, this persistence limit can be adjusted. When
// we do that I think we should figure out a better storage solution for
// transaction history entries.
if (txCount > txHistoryLimit - 1) { if (txCount > txHistoryLimit - 1) {
const index = transactions.findIndex((metaTx) => { const index = transactions.findIndex((metaTx) => {
return getFinalStates().includes(metaTx.status); return getFinalStates().includes(metaTx.status);
}); });
if (index !== -1) { if (index !== -1) {
transactions.splice(index, 1); this._deleteTransaction(transactions[index].id);
} }
} }
const newTxIndex = transactions.findIndex(
(currentTxMeta) => currentTxMeta.time > txMeta.time,
);
newTxIndex === -1 this._addTransactionsToState([txMeta]);
? transactions.push(txMeta)
: transactions.splice(newTxIndex, 0, txMeta);
this._saveTxList(transactions);
return txMeta; return txMeta;
} }
/** /**
* @param {number} txId * @param {number} txId
* @returns {Object} the txMeta who matches the given id if none found * @returns {TransactionMeta} the txMeta who matches the given id if none found
* for the network returns undefined * for the network returns undefined
*/ */
getTx(txId) { getTransaction(txId) {
const txMeta = this.getTxsByMetaData('id', txId)[0]; const { transactions } = this.store.getState();
return txMeta; return transactions[txId];
} }
/** /**
@ -226,10 +228,10 @@ export default class TransactionStateManager extends EventEmitter {
* @param {Object} txMeta - the txMeta to update * @param {Object} txMeta - the txMeta to update
* @param {string} [note] - a note about the update for history * @param {string} [note] - a note about the update for history
*/ */
updateTx(txMeta, note) { updateTransaction(txMeta, note) {
// normalize and validate txParams if present // normalize and validate txParams if present
if (txMeta.txParams) { if (txMeta.txParams) {
txMeta.txParams = this.normalizeAndValidateTxParams(txMeta.txParams); txMeta.txParams = normalizeAndValidateTxParams(txMeta.txParams, false);
} }
// create txMeta snapshot for history // create txMeta snapshot for history
@ -244,232 +246,277 @@ export default class TransactionStateManager extends EventEmitter {
// commit txMeta to state // commit txMeta to state
const txId = txMeta.id; const txId = txMeta.id;
const txList = this.getFullTxList(); this.store.updateState({
const index = txList.findIndex((txData) => txData.id === txId); transactions: {
txList[index] = txMeta; ...this.store.getState().transactions,
this._saveTxList(txList); [txId]: txMeta,
},
});
} }
/** /**
* merges txParams obj onto txMeta.txParams use extend to ensure * SearchCriteria can search in any key in TxParams or the base
* that all fields are filled * TransactionMeta. This type represents any key on either of those two
* @param {number} txId - the id of the txMeta * types.
* @param {Object} txParams - the updated txParams * @typedef {TxParams[keyof TxParams] | TransactionMeta[keyof TransactionMeta]} SearchableKeys
*/ */
updateTxParams(txId, txParams) {
const txMeta = this.getTx(txId);
txMeta.txParams = { ...txMeta.txParams, ...txParams };
this.updateTx(txMeta, `txStateManager#updateTxParams`);
}
/** /**
* normalize and validate txParams members * Predicates can either be strict values, which is shorthand for using
* @param {Object} txParams - txParams * strict equality, or a method that receives he value of the specified key
* and returns a boolean.
* @typedef {(v: unknown) => boolean | unknown} FilterPredicate
*/ */
normalizeAndValidateTxParams(txParams) {
if (typeof txParams.data === 'undefined') { /**
delete txParams.data; * Retrieve a list of transactions from state. By default this will return
* the full list of Transactions for the currently selected chain/network.
* Additional options can be provided to change what is included in the final
* list.
*
* @param opts - options to change filter behavior
* @param {Record<SearchableKeys, FilterPredicate>} [opts.searchCriteria] -
* an object with keys that match keys in TransactionMeta or TxParams, and
* values that are predicates. Predicates can either be strict values,
* which is shorthand for using strict equality, or a method that receives
* the value of the specified key and returns a boolean. The transaction
* list will be filtered to only those items that the predicate returns
* truthy for. **HINT**: `err: undefined` is like looking for a tx with no
* err. so you can also search txs that don't have something as well by
* setting the value as undefined.
* @param {TransactionMeta[]} [opts.initialList] - If provided the filtering
* will occur on the provided list. By default this will be the full list
* from state sorted by time ASC.
* @param {boolean} [opts.filterToCurrentNetwork=true] - Filter transaction
* list to only those that occurred on the current chain or network.
* Defaults to true.
* @param {number} [opts.limit] - limit the number of transactions returned
* to N unique nonces.
* @returns {TransactionMeta[]} The TransactionMeta objects that all provided
* predicates return truthy for.
*/
getTransactions({
searchCriteria = {},
initialList,
filterToCurrentNetwork = true,
limit,
} = {}) {
const chainId = this.getCurrentChainId();
const network = this.getNetwork();
// searchCriteria is an object that might have values that aren't predicate
// methods. When providing any other value type (string, number, etc), we
// consider this shorthand for "check the value at key for strict equality
// with the provided value". To conform this object to be only methods, we
// mapValues (lodash) such that every value on the object is a method that
// returns a boolean.
const predicateMethods = mapValues(searchCriteria, (predicate) => {
return typeof predicate === 'function'
? predicate
: (v) => v === predicate;
});
// If an initial list is provided we need to change it back into an object
// first, so that it matches the shape of our state. This is done by the
// lodash keyBy method. This is the edge case for this method, typically
// initialList will be undefined.
const transactionsToFilter = initialList
? keyBy(initialList, 'id')
: this.store.getState().transactions;
// Combine sortBy and pickBy to transform our state object into an array of
// matching transactions that are sorted by time.
const filteredTransactions = sortBy(
pickBy(transactionsToFilter, (transaction) => {
// default matchesCriteria to the value of transactionMatchesNetwork
// when filterToCurrentNetwork is true.
if (
filterToCurrentNetwork &&
transactionMatchesNetwork(transaction, chainId, network) === false
) {
return false;
}
// iterate over the predicateMethods keys to check if the transaction
// matches the searchCriteria
for (const [key, predicate] of Object.entries(predicateMethods)) {
// We return false early as soon as we know that one of the specified
// search criteria do not match the transaction. This prevents
// needlessly checking all criteria when we already know the criteria
// are not fully satisfied. We check both txParams and the base
// object as predicate keys can be either.
if (key in transaction.txParams) {
if (predicate(transaction.txParams[key]) === false) {
return false;
}
} else if (predicate(transaction[key]) === false) {
return false;
}
}
return true;
}),
'time',
);
if (limit !== undefined) {
// We need to have all transactions of a given nonce in order to display
// necessary details in the UI. We use the size of this set to determine
// whether we have reached the limit provided, thus ensuring that all
// transactions of nonces we include will be sent to the UI.
const nonces = new Set();
const txs = [];
// By default, the transaction list we filter from is sorted by time ASC.
// To ensure that filtered results prefers the newest transactions we
// iterate from right to left, inserting transactions into front of a new
// array. The original order is preserved, but we ensure that newest txs
// are preferred.
for (let i = filteredTransactions.length - 1; i > -1; i--) {
const txMeta = filteredTransactions[i];
const { nonce } = txMeta.txParams;
if (!nonces.has(nonce)) {
if (nonces.size < limit) {
nonces.add(nonce);
} else {
continue;
}
}
// Push transaction into the beginning of our array to ensure the
// original order is preserved.
txs.unshift(txMeta);
}
return txs;
} }
// eslint-disable-next-line no-param-reassign return filteredTransactions;
txParams = normalizeTxParams(txParams, false);
this.validateTxParams(txParams);
return txParams;
} }
/** /**
* validates txParams members by type * Update status of the TransactionMeta with provided id to 'rejected'.
* @param {Object} txParams - txParams to validate * After setting the status, the TransactionMeta is deleted from state.
*/ *
validateTxParams(txParams) { * TODO: Should we show historically rejected transactions somewhere in the
Object.keys(txParams).forEach((key) => { * UI? Seems like it could be valuable for information purposes. Of course
const value = txParams[key]; * only after limit issues are reduced.
// validate types *
switch (key) { * @param {number} txId - the target TransactionMeta's Id
case 'chainId':
if (typeof value !== 'number' && typeof value !== 'string') {
throw new Error(
`${key} in txParams is not a Number or hex string. got: (${value})`,
);
}
break;
default:
if (typeof value !== 'string') {
throw new Error(
`${key} in txParams is not a string. got: (${value})`,
);
}
break;
}
});
}
/**
@param {Object} opts - an object of fields to search for eg:<br>
let <code>thingsToLookFor = {<br>
to: '0x0..',<br>
from: '0x0..',<br>
status: 'signed', \\ (status) => status !== 'rejected' give me all txs who's status is not rejected<br>
err: undefined,<br>
}<br></code>
optionally the values of the keys can be functions for situations like where
you want all but one status.
@param {Array} [initialList=this.getTxList()]
@returns {Array} array of txMeta with all
options matching
*/
/*
****************HINT****************
| `err: undefined` is like looking |
| for a tx with no err |
| so you can also search txs that |
| dont have something as well by |
| setting the value as undefined |
************************************
this is for things like filtering a the tx list
for only tx's from 1 account
or for filtering for all txs from one account
and that have been 'confirmed'
*/
getFilteredTxList(opts, initialList) {
let filteredTxList = initialList;
Object.keys(opts).forEach((key) => {
filteredTxList = this.getTxsByMetaData(key, opts[key], filteredTxList);
});
return filteredTxList;
}
/**
* @param {string} key - the key to check
* @param {any} value - the value your looking for can also be a function that returns a bool
* @param {Array} [txList=this.getTxList()] - the list to search. default is the txList
* from txStateManager#getTxList
* @returns {Array} a list of txMetas who matches the search params
*/
getTxsByMetaData(key, value, txList = this.getTxList()) {
const filter = typeof value === 'function' ? value : (v) => v === value;
return txList.filter((txMeta) => {
if (key in txMeta.txParams) {
return filter(txMeta.txParams[key]);
}
return filter(txMeta[key]);
});
}
// get::set status
/**
* @param {number} txId - the txMeta Id
* @returns {string} the status of the tx.
*/
getTxStatus(txId) {
const txMeta = this.getTx(txId);
return txMeta.status;
}
/**
* Update the status of the tx to 'rejected'.
* @param {number} txId - the txMeta Id
*/ */
setTxStatusRejected(txId) { setTxStatusRejected(txId) {
this._setTxStatus(txId, 'rejected'); this._setTransactionStatus(txId, 'rejected');
this._removeTx(txId); this._deleteTransaction(txId);
} }
/** /**
* Update the status of the tx to 'unapproved'. * Update status of the TransactionMeta with provided id to 'unapproved'
* @param {number} txId - the txMeta Id *
* @param {number} txId - the target TransactionMeta's Id
*/ */
setTxStatusUnapproved(txId) { setTxStatusUnapproved(txId) {
this._setTxStatus(txId, TRANSACTION_STATUSES.UNAPPROVED); this._setTransactionStatus(txId, TRANSACTION_STATUSES.UNAPPROVED);
} }
/** /**
* Update the status of the tx to 'approved'. * Update status of the TransactionMeta with provided id to 'approved'
* @param {number} txId - the txMeta Id *
* @param {number} txId - the target TransactionMeta's Id
*/ */
setTxStatusApproved(txId) { setTxStatusApproved(txId) {
this._setTxStatus(txId, TRANSACTION_STATUSES.APPROVED); this._setTransactionStatus(txId, TRANSACTION_STATUSES.APPROVED);
} }
/** /**
* Update the status of the tx to 'signed'. * Update status of the TransactionMeta with provided id to 'signed'
* @param {number} txId - the txMeta Id *
* @param {number} txId - the target TransactionMeta's Id
*/ */
setTxStatusSigned(txId) { setTxStatusSigned(txId) {
this._setTxStatus(txId, TRANSACTION_STATUSES.SIGNED); this._setTransactionStatus(txId, TRANSACTION_STATUSES.SIGNED);
} }
/** /**
* Update the status of the tx to 'submitted' and add a time stamp * Update status of the TransactionMeta with provided id to 'submitted'
* for when it was called * and sets the 'submittedTime' property with the current Unix epoch time.
* @param {number} txId - the txMeta Id *
* @param {number} txId - the target TransactionMeta's Id
*/ */
setTxStatusSubmitted(txId) { setTxStatusSubmitted(txId) {
const txMeta = this.getTx(txId); const txMeta = this.getTransaction(txId);
txMeta.submittedTime = new Date().getTime(); txMeta.submittedTime = new Date().getTime();
this.updateTx(txMeta, 'txStateManager - add submitted time stamp'); this.updateTransaction(txMeta, 'txStateManager - add submitted time stamp');
this._setTxStatus(txId, TRANSACTION_STATUSES.SUBMITTED); this._setTransactionStatus(txId, TRANSACTION_STATUSES.SUBMITTED);
} }
/** /**
* Update the status of the tx to 'confirmed'. * Update status of the TransactionMeta with provided id to 'confirmed'
* @param {number} txId - the txMeta Id *
* @param {number} txId - the target TransactionMeta's Id
*/ */
setTxStatusConfirmed(txId) { setTxStatusConfirmed(txId) {
this._setTxStatus(txId, TRANSACTION_STATUSES.CONFIRMED); this._setTransactionStatus(txId, TRANSACTION_STATUSES.CONFIRMED);
} }
/** /**
* Update the status of the tx to 'dropped'. * Update status of the TransactionMeta with provided id to 'dropped'
* @param {number} txId - the txMeta Id *
* @param {number} txId - the target TransactionMeta's Id
*/ */
setTxStatusDropped(txId) { setTxStatusDropped(txId) {
this._setTxStatus(txId, TRANSACTION_STATUSES.DROPPED); this._setTransactionStatus(txId, TRANSACTION_STATUSES.DROPPED);
} }
/** /**
* Updates the status of the tx to 'failed' and put the error on the txMeta * Update status of the TransactionMeta with provided id to 'failed' and put
* @param {number} txId - the txMeta Id * the error on the TransactionMeta object.
* @param {erroObject} err - error object *
* @param {number} txId - the target TransactionMeta's Id
* @param {Error} err - error object
*/ */
setTxStatusFailed(txId, err) { setTxStatusFailed(txId, err) {
const error = err || new Error('Internal metamask failure'); const error = err || new Error('Internal metamask failure');
const txMeta = this.getTx(txId); const txMeta = this.getTransaction(txId);
txMeta.err = { txMeta.err = {
message: error.toString(), message: error.toString(),
rpc: error.value, rpc: error.value,
stack: error.stack, stack: error.stack,
}; };
this.updateTx(txMeta, 'transactions:tx-state-manager#fail - add error'); this.updateTransaction(
this._setTxStatus(txId, TRANSACTION_STATUSES.FAILED); txMeta,
'transactions:tx-state-manager#fail - add error',
);
this._setTransactionStatus(txId, TRANSACTION_STATUSES.FAILED);
} }
/** /**
* Removes transaction from the given address for the current network * Removes all transactions for the given address on the current network,
* from the txList * preferring chainId for comparison over networkId.
*
* @param {string} address - hex string of the from address on the txParams * @param {string} address - hex string of the from address on the txParams
* to remove * to remove
*/ */
wipeTransactions(address) { wipeTransactions(address) {
// network only tx // network only tx
const txs = this.getFullTxList(); const { transactions } = this.store.getState();
const network = this.getNetwork(); const network = this.getNetwork();
const chainId = this.getCurrentChainId(); const chainId = this.getCurrentChainId();
// Filter out the ones from the current account and network
const otherAccountTxs = txs.filter(
(txMeta) =>
!(
txMeta.txParams.from === address &&
transactionMatchesNetwork(txMeta, chainId, network)
),
);
// Update state // Update state
this._saveTxList(otherAccountTxs); this.store.updateState({
transactions: omitBy(
transactions,
(transaction) =>
transaction.txParams.from === address &&
transactionMatchesNetwork(transaction, chainId, network),
),
});
}
/**
* Filters out the unapproved transactions from state
*/
clearUnapprovedTxs() {
this.store.updateState({
transactions: omitBy(
this.store.getState().transactions,
(transaction) => transaction.status === TRANSACTION_STATUSES.UNAPPROVED,
),
});
} }
// //
@ -477,14 +524,37 @@ export default class TransactionStateManager extends EventEmitter {
// //
/** /**
* @param {number} txId - the txMeta Id * Updates a transaction's status in state, and then emits events that are
* @param {TransactionStatuses[keyof TransactionStatuses]} status - the status to set on the txMeta * subscribed to elsewhere. See below for best guesses on where and how these
* @emits tx:status-update - passes txId and status * events are received.
* @emits ${txMeta.id}:finished - if it is a finished state. Passes the txMeta * @param {number} txId - the TransactionMeta Id
* @emits 'updateBadge' * @param {TransactionStatusString} status - the status to set on the
* TransactionMeta
* @emits txMeta.id:txMeta.status - every time a transaction's status changes
* we emit the change passing along the id. This does not appear to be used
* outside of this file, which only listens to this to unsubscribe listeners
* of :rejected and :signed statuses when the inverse status changes. Likely
* safe to drop.
* @emits tx:status-update - every time a transaction's status changes we
* emit this event and pass txId and status. This event is subscribed to in
* the TransactionController and re-broadcast by the TransactionController.
* It is used internally within the TransactionController to try and update
* pending transactions on each new block (from blockTracker). It's also
* subscribed to in metamask-controller to display a browser notification on
* confirmed or failed transactions.
* @emits txMeta.id:finished - When a transaction moves to a finished state
* this event is emitted, which is used in the TransactionController to pass
* along details of the transaction to the dapp that suggested them. This
* pattern is replicated across all of the message managers and can likely
* be supplemented or replaced by the ApprovalController.
* @emits updateBadge - When the number of transactions changes in state,
* the badge in the browser extension bar should be updated to reflect the
* number of pending transactions. This particular emit doesn't appear to
* bubble up anywhere that is actually used. TransactionController emits
* this *anytime the state changes*, so this is probably superfluous.
*/ */
_setTxStatus(txId, status) { _setTransactionStatus(txId, status) {
const txMeta = this.getTx(txId); const txMeta = this.getTransaction(txId);
if (!txMeta) { if (!txMeta) {
return; return;
@ -492,7 +562,10 @@ export default class TransactionStateManager extends EventEmitter {
txMeta.status = status; txMeta.status = status;
try { try {
this.updateTx(txMeta, `txStateManager: setting status to ${status}`); this.updateTransaction(
txMeta,
`txStateManager: setting status to ${status}`,
);
this.emit(`${txMeta.id}:${status}`, txId); this.emit(`${txMeta.id}:${status}`, txId);
this.emit(`tx:status-update`, txId, status); this.emit(`tx:status-update`, txId, status);
if ( if (
@ -511,26 +584,32 @@ export default class TransactionStateManager extends EventEmitter {
} }
/** /**
* Saves the new/updated txList. Intended only for internal use * Adds one or more transactions into state. This is not intended for
* @param {Array} transactions - the list of transactions to save * external use.
*
* @private
* @param {TransactionMeta[]} transactions - the list of transactions to save
*/ */
_saveTxList(transactions) { _addTransactionsToState(transactions) {
this.store.updateState({ transactions }); this.store.updateState({
} transactions: transactions.reduce((result, newTx) => {
result[newTx.id] = newTx;
_removeTx(txId) { return result;
const transactionList = this.getFullTxList(); }, this.store.getState().transactions),
this._saveTxList(transactionList.filter((txMeta) => txMeta.id !== txId)); });
} }
/** /**
* Filters out the unapproved transactions * removes one transaction from state. This is not intended for external use.
*
* @private
* @param {number} targetTransactionId - the transaction to delete
*/ */
clearUnapprovedTxs() { _deleteTransaction(targetTransactionId) {
const transactions = this.getFullTxList(); const { transactions } = this.store.getState();
const nonUnapprovedTxs = transactions.filter( delete transactions[targetTransactionId];
(tx) => tx.status !== TRANSACTION_STATUSES.UNAPPROVED, this.store.updateState({
); transactions,
this._saveTxList(nonUnapprovedTxs); });
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -322,7 +322,7 @@ export default class MetamaskController extends EventEmitter {
status === TRANSACTION_STATUSES.CONFIRMED || status === TRANSACTION_STATUSES.CONFIRMED ||
status === TRANSACTION_STATUSES.FAILED status === TRANSACTION_STATUSES.FAILED
) { ) {
const txMeta = this.txController.txStateManager.getTx(txId); const txMeta = this.txController.txStateManager.getTransaction(txId);
const frequentRpcListDetail = this.preferencesController.getFrequentRpcListDetail(); const frequentRpcListDetail = this.preferencesController.getFrequentRpcListDetail();
let rpcPrefs = {}; let rpcPrefs = {};
if (txMeta.chainId) { if (txMeta.chainId) {
@ -504,9 +504,11 @@ export default class MetamaskController extends EventEmitter {
processEncryptionPublicKey: this.newRequestEncryptionPublicKey.bind(this), processEncryptionPublicKey: this.newRequestEncryptionPublicKey.bind(this),
getPendingNonce: this.getPendingNonce.bind(this), getPendingNonce: this.getPendingNonce.bind(this),
getPendingTransactionByHash: (hash) => getPendingTransactionByHash: (hash) =>
this.txController.getFilteredTxList({ this.txController.getTransactions({
hash, searchCriteria: {
status: TRANSACTION_STATUSES.SUBMITTED, hash,
status: TRANSACTION_STATUSES.SUBMITTED,
},
})[0], })[0],
}; };
const providerProxy = this.networkController.initializeProvider( const providerProxy = this.networkController.initializeProvider(
@ -763,7 +765,6 @@ export default class MetamaskController extends EventEmitter {
), ),
createCancelTransaction: nodeify(this.createCancelTransaction, this), createCancelTransaction: nodeify(this.createCancelTransaction, this),
createSpeedUpTransaction: nodeify(this.createSpeedUpTransaction, this), createSpeedUpTransaction: nodeify(this.createSpeedUpTransaction, this),
getFilteredTxList: nodeify(txController.getFilteredTxList, txController),
isNonceTaken: nodeify(txController.isNonceTaken, txController), isNonceTaken: nodeify(txController.isNonceTaken, txController),
estimateGas: nodeify(this.estimateGas, this), estimateGas: nodeify(this.estimateGas, this),
getPendingNonce: nodeify(this.getPendingNonce, this), getPendingNonce: nodeify(this.getPendingNonce, this),

View File

@ -743,7 +743,7 @@ describe('MetaMaskController', function () {
selectedAddressStub.returns('0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'); selectedAddressStub.returns('0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc');
getNetworkstub.returns(42); getNetworkstub.returns(42);
metamaskController.txController.txStateManager._saveTxList([ metamaskController.txController.txStateManager._addTransactionsToState([
createTxMeta({ createTxMeta({
id: 1, id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED, status: TRANSACTION_STATUSES.UNAPPROVED,
@ -771,7 +771,7 @@ describe('MetaMaskController', function () {
await metamaskController.resetAccount(); await metamaskController.resetAccount();
assert.equal( assert.equal(
metamaskController.txController.txStateManager.getTx(1), metamaskController.txController.txStateManager.getTransaction(1),
undefined, undefined,
); );
}); });

View File

@ -0,0 +1,44 @@
import { cloneDeep, keyBy } from 'lodash';
import createId from '../../../shared/modules/random-id';
const version = 57;
/**
* replace 'incomingTxLastFetchedBlocksByNetwork' with 'incomingTxLastFetchedBlockByChainId'
*/
export default {
version,
async migrate(originalVersionedData) {
const versionedData = cloneDeep(originalVersionedData);
versionedData.meta.version = version;
const state = versionedData.data;
versionedData.data = transformState(state);
return versionedData;
},
};
function transformState(state) {
if (
state?.TransactionController?.transactions &&
Array.isArray(state.TransactionController.transactions) &&
!state.TransactionController.transactions.some(
(item) =>
typeof item !== 'object' || typeof item.txParams === 'undefined',
)
) {
state.TransactionController.transactions = keyBy(
state.TransactionController.transactions,
// In case for some reason any of a user's transactions do not have an id
// generate a new one for the transaction.
(tx) => {
if (typeof tx.id === 'undefined' || tx.id === null) {
// This mutates the item in the array, so will result in a change to
// the state.
tx.id = createId();
}
return tx.id;
},
);
}
return state;
}

View File

@ -0,0 +1,193 @@
import { strict as assert } from 'assert';
import migration57 from './057';
describe('migration #57', function () {
it('should update the version metadata', async function () {
const oldStorage = {
meta: {
version: 56,
},
data: {},
};
const newStorage = await migration57.migrate(oldStorage);
assert.deepEqual(newStorage.meta, {
version: 57,
});
});
it('should transactions array into an object keyed by id', async function () {
const oldStorage = {
meta: {},
data: {
TransactionController: {
transactions: [
{
id: 0,
txParams: { foo: 'bar' },
},
{
id: 1,
txParams: { foo: 'bar' },
},
{
id: 2,
txParams: { foo: 'bar' },
},
{
id: 3,
txParams: { foo: 'bar' },
},
],
},
foo: 'bar',
},
};
const newStorage = await migration57.migrate(oldStorage);
assert.deepEqual(newStorage.data, {
TransactionController: {
transactions: {
0: {
id: 0,
txParams: { foo: 'bar' },
},
1: {
id: 1,
txParams: { foo: 'bar' },
},
2: {
id: 2,
txParams: { foo: 'bar' },
},
3: { id: 3, txParams: { foo: 'bar' } },
},
},
foo: 'bar',
});
});
it('should handle transactions without an id, just in case', async function () {
const oldStorage = {
meta: {},
data: {
TransactionController: {
transactions: [
{
id: 0,
txParams: { foo: 'bar' },
},
{
txParams: { foo: 'bar' },
},
{
txParams: { foo: 'bar' },
},
{
txParams: { foo: 'bar' },
},
],
},
foo: 'bar',
},
};
const newStorage = await migration57.migrate(oldStorage);
const expectedTransactions = {};
for (const transaction of Object.values(
newStorage.data.TransactionController.transactions,
)) {
// Make sure each transaction now has an id.
assert.ok(
typeof transaction.id !== 'undefined',
'transaction id is undefined',
);
// Build expected transaction object
expectedTransactions[transaction.id] = transaction;
}
// Ensure that we got the correct number of transactions
assert.equal(
Object.keys(expectedTransactions).length,
oldStorage.data.TransactionController.transactions.length,
);
// Ensure that the one transaction with id is preserved, even though it is
// a falsy id.
assert.equal(newStorage.data.TransactionController.transactions[0].id, 0);
});
it('should not blow up if transactions are not an array', async function () {
const storageWithTransactionsAsString = {
meta: {},
data: {
TransactionController: {
transactions: 'someone might have weird state in the future',
},
},
};
const storageWithTransactionsAsArrayOfString = {
meta: {},
data: {
TransactionController: {
transactions: 'someone might have weird state in the future'.split(
'',
),
},
},
};
const result1 = await migration57.migrate(storageWithTransactionsAsString);
const result2 = await migration57.migrate(
storageWithTransactionsAsArrayOfString,
);
assert.deepEqual(storageWithTransactionsAsString.data, result1.data);
assert.deepEqual(storageWithTransactionsAsArrayOfString.data, result2.data);
});
it('should do nothing if transactions state does not exist', async function () {
const oldStorage = {
meta: {},
data: {
TransactionController: {
bar: 'baz',
},
foo: 'bar',
},
};
const newStorage = await migration57.migrate(oldStorage);
assert.deepEqual(oldStorage.data, newStorage.data);
});
it('should convert empty array into empty object', async function () {
const oldStorage = {
meta: {},
data: {
TransactionController: {
transactions: [],
bar: 'baz',
},
foo: 'bar',
},
};
const newStorage = await migration57.migrate(oldStorage);
assert.deepEqual(newStorage.data, {
TransactionController: {
transactions: {},
bar: 'baz',
},
foo: 'bar',
});
});
it('should do nothing if state is empty', async function () {
const oldStorage = {
meta: {},
data: {},
};
const newStorage = await migration57.migrate(oldStorage);
assert.deepEqual(oldStorage.data, newStorage.data);
});
});

View File

@ -61,6 +61,7 @@ const migrations = [
require('./054').default, require('./054').default,
require('./055').default, require('./055').default,
require('./056').default, require('./056').default,
require('./057').default,
]; ];
export default migrations; export default migrations;

View File

@ -155,6 +155,14 @@ export const TRANSACTION_GROUP_CATEGORIES = {
* @property {string} gas - The max amount of gwei, in hexadecimal, the user is willing to pay * @property {string} gas - The max amount of gwei, in hexadecimal, the user is willing to pay
* @property {string} [data] - Hexadecimal encoded string representing calls to the EVM's ABI * @property {string} [data] - Hexadecimal encoded string representing calls to the EVM's ABI
*/ */
/**
* @typedef {Object} TxError
* @property {string} message - The message from the encountered error.
* @property {any} rpc - The "value" of the error.
* @property {string} [stack] - the stack trace from the error, if available.
*/
/** /**
* An object representing a transaction, in whatever state it is in. * An object representing a transaction, in whatever state it is in.
* @typedef {Object} TransactionMeta * @typedef {Object} TransactionMeta
@ -183,6 +191,7 @@ export const TRANSACTION_GROUP_CATEGORIES = {
* ready to submit to the network. * ready to submit to the network.
* @property {string} hash - A hex string of the transaction hash, used to * @property {string} hash - A hex string of the transaction hash, used to
* identify the transaction on the network. * identify the transaction on the network.
* @property {number} submittedTime - The time the transaction was submitted to * @property {number} [submittedTime] - The time the transaction was submitted to
* the network, in Unix epoch time (ms). * the network, in Unix epoch time (ms).
* @property {TxError} [err] - The error encountered during the transaction
*/ */

View File

@ -54,7 +54,6 @@ export default class SendFooter extends Component {
sign, sign,
to, to,
unapprovedTxs, unapprovedTxs,
// updateTx,
update, update,
toAccounts, toAccounts,
history, history,