diff --git a/app/scripts/controllers/transactions/EtherscanRemoteTransactionSource.test.ts b/app/scripts/controllers/transactions/EtherscanRemoteTransactionSource.test.ts index 2e7effd2c7bb..891f81218e83 100644 --- a/app/scripts/controllers/transactions/EtherscanRemoteTransactionSource.test.ts +++ b/app/scripts/controllers/transactions/EtherscanRemoteTransactionSource.test.ts @@ -105,6 +105,7 @@ const EXPECTED_NORMALISED_TRANSACTION_BASE = { value: '0xb1a2bc2ec50000', }, type: TransactionType.incoming, + verifiedOnBlockchain: false, }; const EXPECTED_NORMALISED_TRANSACTION_SUCCESS = { diff --git a/app/scripts/controllers/transactions/EtherscanRemoteTransactionSource.ts b/app/scripts/controllers/transactions/EtherscanRemoteTransactionSource.ts index ee37d0d4b2ec..0dd90f6aca43 100644 --- a/app/scripts/controllers/transactions/EtherscanRemoteTransactionSource.ts +++ b/app/scripts/controllers/transactions/EtherscanRemoteTransactionSource.ts @@ -151,6 +151,7 @@ export class EtherscanRemoteTransactionSource value: BNToHex(new BN(txMeta.value)), }, type: TransactionType.incoming, + verifiedOnBlockchain: false, } as TransactionMeta; } } diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index 76c7d529e43e..c6238f0b8a15 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -963,6 +963,7 @@ export default class TransactionController extends EventEmitter { if (blockTimestamp) { txMeta.blockTimestamp = blockTimestamp; } + txMeta.verifiedOnBlockchain = true; this.txStateManager.setTxStatusConfirmed(txId); this._markNonceDuplicatesDropped(txId); diff --git a/app/scripts/controllers/transactions/pending-tx-tracker.js b/app/scripts/controllers/transactions/pending-tx-tracker.js index 403c94acec56..093910a482c8 100644 --- a/app/scripts/controllers/transactions/pending-tx-tracker.js +++ b/app/scripts/controllers/transactions/pending-tx-tracker.js @@ -181,7 +181,10 @@ export default class PendingTransactionTracker extends EventEmitter { const txId = txMeta.id; // Only check submitted txs - if (txMeta.status !== TransactionStatus.submitted) { + if ( + txMeta.status !== TransactionStatus.submitted || + txMeta.verifiedOnBlockchain + ) { return; } diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index 20e142c6c052..d7cb159f2105 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -140,6 +140,7 @@ export default class TransactionStateManager extends EventEmitter { loadingDefaults: true, dappSuggestedGasFees, sendFlowHistory: [], + verifiedOnBlockchain: false, ...opts, }; } diff --git a/app/scripts/migrations/098.test.ts b/app/scripts/migrations/098.test.ts new file mode 100644 index 000000000000..9675870f2c40 --- /dev/null +++ b/app/scripts/migrations/098.test.ts @@ -0,0 +1,117 @@ +import { migrate, version } from './098'; + +const oldVersion = 97; +describe('migration #98', () => { + it('updates the version metadata', async () => { + const oldStorage = { + meta: { version: oldVersion }, + data: {}, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.meta).toStrictEqual({ version }); + }); + + it('handles missing TransactionController', async () => { + const oldState = { + OtherController: {}, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toEqual(oldState); + }); + + it('handles empty transactions', async () => { + const oldState = { + TransactionController: { + transactions: {}, + }, + }; + + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: oldState, + }); + + expect(transformedState.data).toEqual(oldState); + }); + + it('handles missing state', async () => { + const transformedState = await migrate({ + meta: { version: oldVersion }, + data: {}, + }); + + expect(transformedState.data).toEqual({}); + }); + + it('adds verifiedOnBlockchain in transaction based on the presence of txReceipt', async () => { + const oldState = { + TransactionController: { + transactions: { + tx1: { + to: '0x9ef57335bc7d5b6cbc06dca6064a604b75e09ace', + txReceipt: { + blockHash: + '0xafa4e1fd95e429d9c6e6c7c1d282b2bd0bbeb50d0a68743e9392b9c95a06e2eb', + }, + otherProp: 'value', + }, + tx2: { + to: '0x9ef57335bc7d5b6cbc06dca6064a604b75e09ace', + otherProp: 'value', + }, + tx3: { + to: '0x9ef57335bc7d5b6cbc06dca6064a604b75e09ace', + txReceipt: { + blockHash: + '0xafa4e1fd95e429d9c6e6c7c1d282b2bd0bbeb50d0a68743e9392b9c95a06e2eb', + }, + otherProp: 'value', + }, + }, + }, + }; + const oldStorage = { + meta: { version: oldVersion }, + data: oldState, + }; + + const newStorage = await migrate(oldStorage); + + expect(newStorage.data).toEqual({ + TransactionController: { + transactions: { + tx1: { + to: '0x9ef57335bc7d5b6cbc06dca6064a604b75e09ace', + txReceipt: { + blockHash: + '0xafa4e1fd95e429d9c6e6c7c1d282b2bd0bbeb50d0a68743e9392b9c95a06e2eb', + }, + otherProp: 'value', + verifiedOnBlockchain: true, + }, + tx2: { + to: '0x9ef57335bc7d5b6cbc06dca6064a604b75e09ace', + otherProp: 'value', + verifiedOnBlockchain: false, + }, + tx3: { + to: '0x9ef57335bc7d5b6cbc06dca6064a604b75e09ace', + txReceipt: { + blockHash: + '0xafa4e1fd95e429d9c6e6c7c1d282b2bd0bbeb50d0a68743e9392b9c95a06e2eb', + }, + otherProp: 'value', + verifiedOnBlockchain: true, + }, + }, + }, + }); + }); +}); diff --git a/app/scripts/migrations/098.ts b/app/scripts/migrations/098.ts new file mode 100644 index 000000000000..3085827b4c6a --- /dev/null +++ b/app/scripts/migrations/098.ts @@ -0,0 +1,52 @@ +import { cloneDeep, isEmpty } from 'lodash'; + +type VersionedData = { + meta: { version: number }; + data: Record; +}; + +export const version = 98; // Increment the version number + +/** + * Add `verifiedOnBlockchain` property to transactions based on the presence of `txReceipt` + * + * @param originalVersionedData - Versioned MetaMask extension state, exactly what we persist to dist. + * @param originalVersionedData.meta - State metadata. + * @param originalVersionedData.meta.version - The current state version. + * @param originalVersionedData.data - The persisted MetaMask state, keyed by controller. + * @returns Updated versioned MetaMask extension state. + */ +export async function migrate( + originalVersionedData: VersionedData, +): Promise { + const versionedData = cloneDeep(originalVersionedData); + versionedData.meta.version = version; + transformState(versionedData.data); + return versionedData; +} + +function transformState(state: Record) { + const transactionControllerState = state?.TransactionController || {}; + const transactions = transactionControllerState?.transactions || {}; + + if (isEmpty(transactions)) { + return; + } + + const newTxs = Object.keys(transactions).reduce((txs, txId) => { + const transaction = transactions[txId]; + + // Add the `verifiedOnBlockchain` property based on the presence of `txReceipt` + transaction.verifiedOnBlockchain = Boolean(transaction.txReceipt); + + return { + ...txs, + [txId]: transaction, + }; + }, {}); + + state.TransactionController = { + ...transactionControllerState, + transactions: newTxs, + }; +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index c8b27ac352b0..621ddd37a662 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -104,6 +104,7 @@ import * as m094 from './094'; import * as m095 from './095'; import * as m096 from './096'; import * as m097 from './097'; +import * as m098 from './098'; const migrations = [ m002, @@ -205,5 +206,6 @@ const migrations = [ m095, m096, m097, + m098, ]; export default migrations; diff --git a/shared/constants/transaction.ts b/shared/constants/transaction.ts index 8a5ac8f98eb6..480e9459c34c 100644 --- a/shared/constants/transaction.ts +++ b/shared/constants/transaction.ts @@ -400,6 +400,10 @@ export interface TransactionMeta { submittedTime?: number; /** The error encountered during the transaction */ txErr?: TxError; + /** + * Whether the transaction is verified on the blockchain. + */ + verifiedOnBlockchain?: boolean; } /**