1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-01 21:57:06 +01:00
metamask-extension/app/scripts/controllers/transactions/IncomingTransactionHelper.ts

283 lines
7.4 KiB
TypeScript
Raw Normal View History

import EventEmitter from 'events';
import type { BlockTracker, NetworkState } from '@metamask/network-controller';
import type { Hex } from '@metamask/utils';
import log from 'loglevel';
import { TransactionMeta } from '../../../../shared/constants/transaction';
import { RemoteTransactionSource } from './types';
const UPDATE_CHECKS: ((txMeta: TransactionMeta) => any)[] = [
(txMeta) => txMeta.status,
];
export class IncomingTransactionHelper {
hub: EventEmitter;
#blockTracker: BlockTracker;
#getCurrentAccount: () => string;
#getLocalTransactions: () => TransactionMeta[];
#getNetworkState: () => NetworkState;
#isEnabled: () => boolean;
#isRunning: boolean;
#isUpdating: boolean;
#lastFetchedBlockNumbers: Record<string, number>;
#onLatestBlock: (blockNumberHex: Hex) => Promise<void>;
#remoteTransactionSource: RemoteTransactionSource;
#transactionLimit?: number;
#updateTransactions: boolean;
constructor({
blockTracker,
getCurrentAccount,
getLocalTransactions,
getNetworkState,
isEnabled,
lastFetchedBlockNumbers,
remoteTransactionSource,
transactionLimit,
updateTransactions,
}: {
blockTracker: BlockTracker;
getCurrentAccount: () => string;
getNetworkState: () => NetworkState;
getLocalTransactions?: () => TransactionMeta[];
isEnabled?: () => boolean;
lastFetchedBlockNumbers?: Record<string, number>;
remoteTransactionSource: RemoteTransactionSource;
transactionLimit?: number;
updateTransactions?: boolean;
}) {
this.hub = new EventEmitter();
this.#blockTracker = blockTracker;
this.#getCurrentAccount = getCurrentAccount;
this.#getLocalTransactions = getLocalTransactions || (() => []);
this.#getNetworkState = getNetworkState;
this.#isEnabled = isEnabled ?? (() => true);
this.#isRunning = false;
this.#isUpdating = false;
this.#lastFetchedBlockNumbers = lastFetchedBlockNumbers ?? {};
this.#remoteTransactionSource = remoteTransactionSource;
this.#transactionLimit = transactionLimit;
this.#updateTransactions = updateTransactions ?? false;
// Using a property instead of a method to provide a listener reference
// with the correct scope that we can remove later if stopped.
this.#onLatestBlock = async (blockNumberHex: Hex) => {
await this.update(blockNumberHex);
};
}
start() {
if (this.#isRunning) {
return;
}
if (!this.#canStart()) {
return;
}
this.#blockTracker.addListener('latest', this.#onLatestBlock);
this.#isRunning = true;
}
stop() {
this.#blockTracker.removeListener('latest', this.#onLatestBlock);
this.#isRunning = false;
}
async update(latestBlockNumberHex?: Hex): Promise<void> {
if (this.#isUpdating) {
return;
}
this.#isUpdating = true;
try {
if (!this.#canStart()) {
return;
}
const latestBlockNumber = parseInt(
latestBlockNumberHex || (await this.#blockTracker.getLatestBlock()),
16,
);
const fromBlock = this.#getFromBlock(latestBlockNumber);
const address = this.#getCurrentAccount();
const currentChainId = this.#getCurrentChainId();
const currentNetworkId = this.#getCurrentNetworkId();
let remoteTransactions = [];
try {
remoteTransactions =
await this.#remoteTransactionSource.fetchTransactions({
address,
currentChainId,
currentNetworkId,
fromBlock,
limit: this.#transactionLimit,
});
} catch (error: any) {
return;
}
if (!this.#updateTransactions) {
remoteTransactions = remoteTransactions.filter(
(tx) => tx.txParams.to?.toLowerCase() === address.toLowerCase(),
);
}
const localTransactions = this.#updateTransactions
? this.#getLocalTransactions()
: [];
const newTransactions = this.#getNewTransactions(
remoteTransactions,
localTransactions,
);
const updatedTransactions = this.#getUpdatedTransactions(
remoteTransactions,
localTransactions,
);
if (newTransactions.length > 0 || updatedTransactions.length > 0) {
this.#sortTransactionsByTime(newTransactions);
this.#sortTransactionsByTime(updatedTransactions);
this.hub.emit('transactions', {
added: newTransactions,
updated: updatedTransactions,
});
}
this.#updateLastFetchedBlockNumber(remoteTransactions);
} catch (error) {
log.error('Error while checking incoming transactions', error);
} finally {
this.#isUpdating = false;
}
}
#sortTransactionsByTime(transactions: TransactionMeta[]) {
transactions.sort((a, b) => (a.time < b.time ? -1 : 1));
}
#getNewTransactions(
remoteTxs: TransactionMeta[],
localTxs: TransactionMeta[],
): TransactionMeta[] {
return remoteTxs.filter(
(tx) => !localTxs.some(({ hash }) => hash === tx.hash),
);
}
#getUpdatedTransactions(
remoteTxs: TransactionMeta[],
localTxs: TransactionMeta[],
): TransactionMeta[] {
return remoteTxs.filter((remoteTx) =>
localTxs.some(
(localTx) =>
remoteTx.hash === localTx.hash &&
this.#isTransactionOutdated(remoteTx, localTx),
),
);
}
#isTransactionOutdated(
remoteTx: TransactionMeta,
localTx: TransactionMeta,
): boolean {
return UPDATE_CHECKS.some(
(getValue) => getValue(remoteTx) !== getValue(localTx),
);
}
#getFromBlock(latestBlockNumber: number): number {
const lastFetchedKey = this.#getBlockNumberKey();
const lastFetchedBlockNumber =
this.#lastFetchedBlockNumbers[lastFetchedKey];
if (lastFetchedBlockNumber) {
return lastFetchedBlockNumber + 1;
}
// Avoid using latest block as remote transaction source
// may not have indexed it yet
return Math.max(latestBlockNumber - 10, 0);
}
#updateLastFetchedBlockNumber(remoteTxs: TransactionMeta[]) {
let lastFetchedBlockNumber = -1;
for (const tx of remoteTxs) {
const currentBlockNumberValue = tx.blockNumber
? parseInt(tx.blockNumber, 10)
: -1;
lastFetchedBlockNumber = Math.max(
lastFetchedBlockNumber,
currentBlockNumberValue,
);
}
if (lastFetchedBlockNumber === -1) {
return;
}
const lastFetchedKey = this.#getBlockNumberKey();
const previousValue = this.#lastFetchedBlockNumbers[lastFetchedKey];
if (previousValue === lastFetchedBlockNumber) {
return;
}
this.#lastFetchedBlockNumbers[lastFetchedKey] = lastFetchedBlockNumber;
this.hub.emit('updatedLastFetchedBlockNumbers', {
lastFetchedBlockNumbers: this.#lastFetchedBlockNumbers,
blockNumber: lastFetchedBlockNumber,
});
}
#getBlockNumberKey(): string {
return `${this.#getCurrentChainId()}#${this.#getCurrentAccount().toLowerCase()}`;
}
#canStart(): boolean {
const isEnabled = this.#isEnabled();
const currentChainId = this.#getCurrentChainId();
const currentNetworkId = this.#getCurrentNetworkId();
const isSupportedNetwork = this.#remoteTransactionSource.isSupportedNetwork(
currentChainId,
currentNetworkId,
);
return isEnabled && isSupportedNetwork;
}
#getCurrentChainId(): Hex {
return this.#getNetworkState().providerConfig.chainId;
}
#getCurrentNetworkId(): string {
return this.#getNetworkState().networkId as string;
}
}