From 3b0dbd6e500cd2a76e9d339ffe80ce12847f1878 Mon Sep 17 00:00:00 2001 From: Stanley Yuen <102275989+stanleyyconsensys@users.noreply.github.com> Date: Thu, 22 Aug 2024 08:51:31 +0800 Subject: [PATCH] refactor: add `TransactionStateManager` (#328) * refactor: add transaction state manager * chore: lint * chore: update review comment --- .../state/transaction-state-manager.test.ts | 367 ++++++++++++++++++ .../src/state/transaction-state-manager.ts | 310 +++++++++++++++ packages/starknet-snap/src/types/snapState.ts | 2 + packages/starknet-snap/test/utils.ts | 138 ++++++- 4 files changed, 816 insertions(+), 1 deletion(-) create mode 100644 packages/starknet-snap/src/state/transaction-state-manager.test.ts create mode 100644 packages/starknet-snap/src/state/transaction-state-manager.ts diff --git a/packages/starknet-snap/src/state/transaction-state-manager.test.ts b/packages/starknet-snap/src/state/transaction-state-manager.test.ts new file mode 100644 index 00000000..03d61572 --- /dev/null +++ b/packages/starknet-snap/src/state/transaction-state-manager.test.ts @@ -0,0 +1,367 @@ +import { + TransactionType, + constants, + TransactionFinalityStatus, + TransactionExecutionStatus, +} from 'starknet'; + +import { generateTransactions } from '../../test/utils'; +import { PRELOADED_TOKENS } from '../utils/constants'; +import { mockAcccounts, mockState } from './__tests__/helper'; +import { StateManagerError } from './state-manager'; +import { TransactionStateManager } from './transaction-state-manager'; + +describe('TransactionStateManager', () => { + const prepareMockData = async (chainId) => { + const accounts = await mockAcccounts(chainId, 1); + const txns = generateTransactions({ + chainId, + address: accounts[0].address, + txnTypes: [ + TransactionType.DECLARE, + TransactionType.DEPLOY_ACCOUNT, + TransactionType.INVOKE, + ], + cnt: 10, + }); + const { state, setDataSpy, getDataSpy } = await mockState({ + transactions: txns, + }); + return { + state, + setDataSpy, + getDataSpy, + account: accounts[0], + txns, + }; + }; + + describe('getTransaction', () => { + it('returns the transaction', async () => { + const chainId = constants.StarknetChainId.SN_SEPOLIA; + const { txns } = await prepareMockData(chainId); + + const stateManager = new TransactionStateManager(); + const result = await stateManager.getTransaction({ + txnHash: txns[0].txnHash, + }); + + expect(result).toStrictEqual(txns[0]); + }); + + it('finds the transaction by chainId and txnHash', async () => { + const chainId = constants.StarknetChainId.SN_SEPOLIA; + const { txns } = await prepareMockData(chainId); + + const stateManager = new TransactionStateManager(); + const result = await stateManager.getTransaction({ + txnHash: txns[1].txnHash, + chainId: txns[1].chainId, + }); + + expect(result).toStrictEqual(txns[1]); + }); + + it('returns null if the transaction can not be found', async () => { + const chainId = constants.StarknetChainId.SN_SEPOLIA; + const { txns } = await prepareMockData(chainId); + + const stateManager = new TransactionStateManager(); + + const result = await stateManager.getTransaction({ + txnHash: txns[0].txnHash, + chainId: constants.StarknetChainId.SN_MAIN, + }); + expect(result).toBeNull(); + }); + }); + + describe('findTransactions', () => { + const prepareFindTransctions = async () => { + const chainId = constants.StarknetChainId.SN_SEPOLIA; + const { txns } = await prepareMockData(chainId); + const stateManager = new TransactionStateManager(); + return { + stateManager, + txns, + }; + }; + + it('returns the list of transaction by chain id', async () => { + const chainId = constants.StarknetChainId.SN_SEPOLIA; + const { txns, stateManager } = await prepareFindTransctions(); + + const result = await stateManager.findTransactions({ + chainId: [chainId], + }); + + expect(result).toStrictEqual( + txns.filter((txn) => txn.chainId === chainId), + ); + }); + + it('returns the list of transaction by txn hash', async () => { + const { txns, stateManager } = await prepareFindTransctions(); + + const result = await stateManager.findTransactions({ + txnHash: [txns[0].txnHash, txns[1].txnHash], + }); + + expect(result).toStrictEqual([txns[0], txns[1]]); + }); + + it('returns the list of transaction by txn type', async () => { + const { txns, stateManager } = await prepareFindTransctions(); + + const result = await stateManager.findTransactions({ + txnType: [TransactionType.DEPLOY_ACCOUNT], + }); + + expect(result).toStrictEqual([ + txns.find((txn) => txn.txnType === TransactionType.DEPLOY_ACCOUNT), + ]); + }); + + it('returns the list of transaction by sender address', async () => { + const { txns, stateManager } = await prepareFindTransctions(); + + const result = await stateManager.findTransactions({ + senderAddress: [txns[0].senderAddress], + }); + + expect(result).toStrictEqual(txns); + }); + + it('returns the list of transaction by contract address', async () => { + const { txns, stateManager } = await prepareFindTransctions(); + const tokenAddress1 = PRELOADED_TOKENS.map((token) => token.address)[0]; + const tokenAddress2 = PRELOADED_TOKENS.map((token) => token.address)[2]; + + const result = await stateManager.findTransactions({ + contractAddress: [tokenAddress1, tokenAddress2], + }); + + expect(result).toStrictEqual( + txns.filter( + (txn) => + txn.contractAddress === tokenAddress1 || + txn.contractAddress === tokenAddress2, + ), + ); + }); + + it('returns the list of transaction by timestamp if the transaction timestamp is >= the search timestamp', async () => { + const { txns, stateManager } = await prepareFindTransctions(); + + const result = await stateManager.findTransactions({ + // The timestamp from data source is in seconds, but we are comparing it in milliseconds + timestamp: txns[5].timestamp * 1000, + }); + + expect(result).toStrictEqual( + txns.filter((txn) => txn.timestamp >= txns[5].timestamp), + ); + }); + + it('returns the list of transaction by finalityStatus', async () => { + const { txns, stateManager } = await prepareFindTransctions(); + const finalityStatusCond = [ + TransactionFinalityStatus.ACCEPTED_ON_L1, + TransactionFinalityStatus.ACCEPTED_ON_L2, + ]; + + const result = await stateManager.findTransactions({ + finalityStatus: finalityStatusCond, + }); + + expect(result).toStrictEqual( + txns.filter((txn) => { + return finalityStatusCond.includes( + txn.finalityStatus as unknown as TransactionFinalityStatus, + ); + }), + ); + }); + + it('returns the list of transaction by executionStatus', async () => { + const { txns, stateManager } = await prepareFindTransctions(); + const executionStatusCond = [TransactionExecutionStatus.REJECTED]; + + const result = await stateManager.findTransactions({ + executionStatus: executionStatusCond, + }); + + expect(result).toStrictEqual( + txns.filter((txn) => { + return executionStatusCond.includes( + txn.executionStatus as unknown as TransactionExecutionStatus, + ); + }), + ); + }); + + it('returns the list of transaction by mutilple conditions', async () => { + const { txns, stateManager } = await prepareFindTransctions(); + const finalityStatusCond = [ + TransactionFinalityStatus.ACCEPTED_ON_L1, + TransactionFinalityStatus.ACCEPTED_ON_L2, + ]; + const executionStatusCond = [ + TransactionExecutionStatus.REVERTED, + TransactionExecutionStatus.SUCCEEDED, + TransactionExecutionStatus.REJECTED, + ]; + const contractAddressCond = [ + PRELOADED_TOKENS.map((token) => token.address)[0], + ]; + const timestampCond = txns[5].timestamp * 1000; + const chainIdCond = [ + txns[0].chainId as unknown as constants.StarknetChainId, + ]; + + const result = await stateManager.findTransactions({ + chainId: chainIdCond, + finalityStatus: finalityStatusCond, + executionStatus: executionStatusCond, + timestamp: timestampCond, + contractAddress: contractAddressCond, + senderAddress: [txns[0].senderAddress], + }); + + expect(result).toStrictEqual( + txns.filter((txn) => { + return ( + (finalityStatusCond.includes( + txn.finalityStatus as unknown as TransactionFinalityStatus, + ) || + executionStatusCond.includes( + txn.executionStatus as unknown as TransactionExecutionStatus, + )) && + txn.timestamp >= txns[5].timestamp && + contractAddressCond.includes(txn.contractAddress) && + chainIdCond.includes( + txn.chainId as unknown as constants.StarknetChainId, + ) && + txn.senderAddress === txns[0].senderAddress + ); + }), + ); + }); + + it('returns empty array if none of the transaction found', async () => { + const { stateManager } = await prepareFindTransctions(); + + const result = await stateManager.findTransactions({ + chainId: ['0x1' as unknown as constants.StarknetChainId], + }); + + expect(result).toStrictEqual([]); + }); + }); + + describe('updateTransaction', () => { + it('updates the transaction', async () => { + const chainId = constants.StarknetChainId.SN_SEPOLIA; + const { txns, state } = await prepareMockData(chainId); + + const stateManager = new TransactionStateManager(); + const txn = txns[2]; + const updatedEntity = { + ...txn, + executionStatus: TransactionExecutionStatus.REJECTED, + finalityStatus: TransactionFinalityStatus.ACCEPTED_ON_L1, + timestamp: Math.floor(Date.now() / 1000), + }; + await stateManager.updateTransaction(updatedEntity); + + expect( + state.transactions.find( + (transaction) => transaction.txnHash === txn.txnHash, + ), + ).toStrictEqual(updatedEntity); + }); + + it('throws `Transaction does not exist` error if the update entity can not be found', async () => { + const chainId = constants.StarknetChainId.SN_SEPOLIA; + const { txns } = await prepareMockData(chainId); + + const stateManager = new TransactionStateManager(); + const txn = txns[2]; + const updatedEntity = { + ...txn, + timestamp: Math.floor(Date.now() / 1000), + txnHash: '0x123', + }; + + await expect( + stateManager.updateTransaction(updatedEntity), + ).rejects.toThrow('Transaction does not exist'); + }); + }); + + describe('removeTransactions', () => { + it('removes the transaction', async () => { + const chainId = constants.StarknetChainId.SN_SEPOLIA; + const { txns, state } = await prepareMockData(chainId); + const txnHashCond = [txns[2].txnHash, txns[1].txnHash]; + + const stateManager = new TransactionStateManager(); + + await stateManager.removeTransactions({ + txnHash: txnHashCond, + }); + + expect( + state.transactions.filter((txn) => txnHashCond.includes(txn.txnHash)), + ).toStrictEqual([]); + }); + + it('throws a `StateManagerError` error if an error was thrown', async () => { + const chainId = constants.StarknetChainId.SN_SEPOLIA; + const { txns, setDataSpy } = await prepareMockData(chainId); + setDataSpy.mockRejectedValue(new Error('Error')); + const txnHashCond = [txns[2].txnHash, txns[1].txnHash]; + + const stateManager = new TransactionStateManager(); + + await expect( + stateManager.removeTransactions({ + txnHash: txnHashCond, + }), + ).rejects.toThrow(StateManagerError); + }); + }); + + describe('addTransaction', () => { + it('adds a transaction', async () => { + const chainId = constants.StarknetChainId.SN_SEPOLIA; + const { + txns: [txnToBeAdded, ...txns], + getDataSpy, + state, + } = await prepareMockData(chainId); + getDataSpy.mockResolvedValue({ + transactions: txns, + }); + + const stateManager = new TransactionStateManager(); + + await stateManager.addTransaction(txnToBeAdded); + + expect( + state.transactions.find((txn) => txn.txnHash === txnToBeAdded.txnHash), + ).toStrictEqual(txnToBeAdded); + }); + + it('throws a `Transaction already exist` error if the transaction is exist', async () => { + const chainId = constants.StarknetChainId.SN_SEPOLIA; + const { txns } = await prepareMockData(chainId); + + const stateManager = new TransactionStateManager(); + + await expect(stateManager.addTransaction(txns[0])).rejects.toThrow( + 'Transaction already exist', + ); + }); + }); +}); diff --git a/packages/starknet-snap/src/state/transaction-state-manager.ts b/packages/starknet-snap/src/state/transaction-state-manager.ts new file mode 100644 index 00000000..a4805bb9 --- /dev/null +++ b/packages/starknet-snap/src/state/transaction-state-manager.ts @@ -0,0 +1,310 @@ +import type { constants, TransactionType } from 'starknet'; +import { + TransactionFinalityStatus, + TransactionExecutionStatus, +} from 'starknet'; +import { assert, enums, number } from 'superstruct'; + +import type { Transaction, SnapState } from '../types/snapState'; +import { TransactionStatusType } from '../types/snapState'; +import type { IFilter } from './filter'; +import { + BigIntFilter, + ChainIdFilter as BaseChainIdFilter, + StringFllter, + Filter, +} from './filter'; +import { StateManager, StateManagerError } from './state-manager'; + +export type ITxFilter = IFilter; + +export class ChainIdFilter + extends BaseChainIdFilter + implements ITxFilter {} + +export class ContractAddressFilter + extends BigIntFilter + implements ITxFilter +{ + dataKey = 'contractAddress'; +} +export class SenderAddressFilter + extends BigIntFilter + implements ITxFilter +{ + dataKey = 'senderAddress'; +} + +export class TxHashFilter + extends BigIntFilter + implements ITxFilter +{ + dataKey = 'txnHash'; +} + +export class TxTimestampFilter + extends Filter + implements ITxFilter +{ + _apply(data: Transaction): boolean { + // The timestamp from the data source is in seconds, but we are comparing it in milliseconds + // e.g if the search is 1630000000, it means we return the txns where the timestamp is greater than or equal to 1630000000 * 1000 + // example use case: search for txns for the last 7 days, the search will be Date.now() - 7 * 24 * 60 * 60 * 1000 + return this.search !== undefined && data.timestamp * 1000 >= this.search; + } +} + +export class TxnTypeFilter + extends StringFllter + implements ITxFilter +{ + dataKey = 'txnType'; +} + +// Filter for transaction status +// Search for transactions based on the finality status and execution status +// It compare the finality status and execution status in OR condition, due to our use case is to find the transactions that fit to the given finality status or the given execution status +export class TxStatusFilter implements ITxFilter { + finalityStatus: Set; + + executionStatus: Set; + + constructor(finalityStatus: string[], executionStatus: string[]) { + this.finalityStatus = new Set( + finalityStatus.map((val) => val.toLowerCase()), + ); + this.executionStatus = new Set( + executionStatus.map((val) => val.toLowerCase()), + ); + } + + apply(txn: Transaction): boolean { + let finalityStatusCond = false; + let executionStatusCond = false; + + if (this.finalityStatus.size > 0) { + finalityStatusCond = + Object.prototype.hasOwnProperty.call( + txn, + TransactionStatusType.FINALITY, + ) && + txn[TransactionStatusType.FINALITY] && + this.finalityStatus.has( + txn[TransactionStatusType.FINALITY].toLowerCase(), + ); + } + + if (this.executionStatus.size > 0) { + executionStatusCond = + Object.prototype.hasOwnProperty.call( + txn, + TransactionStatusType.EXECUTION, + ) && + txn[TransactionStatusType.EXECUTION] && + this.executionStatus.has( + txn[TransactionStatusType.EXECUTION].toLowerCase(), + ); + } + return finalityStatusCond || executionStatusCond; + } +} + +export type SearchFilter = { + txnHash?: string[]; + txnType?: TransactionType[]; + chainId?: constants.StarknetChainId[]; + senderAddress?: string[]; + contractAddress?: string[]; + executionStatus?: TransactionExecutionStatus[]; + finalityStatus?: TransactionFinalityStatus[]; + timestamp?: number; +}; + +export class TransactionStateManager extends StateManager { + protected getCollection(state: SnapState): Transaction[] { + return state.transactions; + } + + protected updateEntity(dataInState: Transaction, data: Transaction): void { + assert( + data.executionStatus, + enums(Object.values(TransactionExecutionStatus)), + ); + assert( + data.finalityStatus, + enums(Object.values(TransactionFinalityStatus)), + ); + assert(data.timestamp, number()); + + dataInState.executionStatus = data.executionStatus; + dataInState.finalityStatus = data.finalityStatus; + dataInState.timestamp = data.timestamp; + dataInState.failureReason = data.failureReason; + } + + #getCompositeKey(data: Transaction): string { + const key1 = BigInt(data.chainId); + const key2 = BigInt(data.txnHash); + return `${key1}&${key2}`; + } + + async findTransactions( + { + txnHash, + txnType, + chainId, + senderAddress, + contractAddress, + executionStatus, + finalityStatus, + timestamp, + }: SearchFilter, + state?: SnapState, + ): Promise { + const filters: ITxFilter[] = []; + if (txnHash !== undefined && txnHash.length > 0) { + filters.push(new TxHashFilter(txnHash)); + } + + if (chainId !== undefined && chainId.length > 0) { + filters.push(new ChainIdFilter(chainId)); + } + + if (timestamp !== undefined) { + filters.push(new TxTimestampFilter(timestamp)); + } + + if (senderAddress !== undefined && senderAddress.length > 0) { + filters.push(new SenderAddressFilter(senderAddress)); + } + + if (contractAddress !== undefined && contractAddress.length > 0) { + filters.push(new ContractAddressFilter(contractAddress)); + } + + if (txnType !== undefined && txnType.length > 0) { + filters.push(new TxnTypeFilter(txnType)); + } + + if (finalityStatus !== undefined || executionStatus !== undefined) { + filters.push( + new TxStatusFilter(finalityStatus ?? [], executionStatus ?? []), + ); + } + + return this.list( + filters, + // sort by timestamp in descending order + (entityA: Transaction, entityB: Transaction) => + entityB.timestamp - entityA.timestamp, + state, + ); + } + + /** + * Finds a transaction object based on the given chain id and txn hash. + * + * @param param - The param object. + * @param param.txnHash - The txn hash of the transaction object to search for. + * @param [param.chainId] - The optional chain id of the transaction object to search for. + * @param [state] - The optional SnapState object. + * @returns A Promise that resolves with the transaction object if found, or null if not found. + */ + async getTransaction( + { + txnHash, + chainId, + }: { + txnHash: string; + chainId?: string; + }, + state?: SnapState, + ): Promise { + const filters: ITxFilter[] = [new TxHashFilter([txnHash])]; + if (chainId !== undefined) { + filters.push(new ChainIdFilter([chainId])); + } + return this.find(filters, state); + } + + /** + * Updates a transaction object in the state with the given data. + * + * @param data - The updated transaction object. + * @returns A Promise that resolves when the update is complete. + * @throws {StateManagerError} If there is an error updating the transaction object, such as: + * If the transaction object to be updated does not exist in the state. + */ + async updateTransaction(data: Transaction): Promise { + try { + await this.update(async (state: SnapState) => { + const dataInState = await this.getTransaction( + { + txnHash: data.txnHash, + chainId: data.chainId, + }, + state, + ); + if (!dataInState) { + throw new Error(`Transaction does not exist`); + } + this.updateEntity(dataInState, data); + }); + } catch (error) { + throw new StateManagerError(error.message); + } + } + + /** + * Adds a new transaction object to the state with the given data. + * + * @param data - The transaction object to add. + * @returns A Promise that resolves when the add is complete. + * @throws {StateManagerError} If there is an error adding the transaction object, such as: + * If the transaction object to be added already exists in the state. + */ + async addTransaction(data: Transaction): Promise { + try { + await this.update(async (state: SnapState) => { + const dataInState = await this.getTransaction( + { + txnHash: data.txnHash, + chainId: data.chainId, + }, + state, + ); + + if (dataInState) { + throw new Error(`Transaction already exist`); + } + state.transactions.push(data); + }); + } catch (error) { + throw new StateManagerError(error.message); + } + } + + /** + * Removes the transaction objects in the state with the given search conditions. + * + * @param searchFilter - The searchFilter object. + * @returns A Promise that resolves when the remove is complete. + */ + async removeTransactions(searchFilter: SearchFilter): Promise { + try { + await this.update(async (state: SnapState) => { + const dataInState = await this.findTransactions(searchFilter, state); + + const dataSet = new Set( + dataInState.map((txn) => this.#getCompositeKey(txn)), + ); + + state.transactions = state.transactions.filter((txn) => { + return !dataSet.has(this.#getCompositeKey(txn)); + }); + }); + } catch (error) { + throw new StateManagerError(error.message); + } + } +} diff --git a/packages/starknet-snap/src/types/snapState.ts b/packages/starknet-snap/src/types/snapState.ts index 735b22d0..a1ae56e7 100644 --- a/packages/starknet-snap/src/types/snapState.ts +++ b/packages/starknet-snap/src/types/snapState.ts @@ -81,8 +81,10 @@ export enum TransactionStatusType { // for retrieving txn from StarkNet feeder g export type Transaction = { txnHash: string; // in hex + // TODO: Change the type of txnType to `TransactionType` in the SnapState, when this state manager apply to getTransactions, there is no migration neeeded, as the state is override for every fetch for getTransactions txnType: VoyagerTransactionType | string; chainId: string; // in hex + // TODO: rename it to address to sync with the same naming convention in the AccContract senderAddress: string; // in hex contractAddress: string; // in hex contractFuncName: string; diff --git a/packages/starknet-snap/test/utils.ts b/packages/starknet-snap/test/utils.ts index d3efed5b..d78716f5 100644 --- a/packages/starknet-snap/test/utils.ts +++ b/packages/starknet-snap/test/utils.ts @@ -6,15 +6,19 @@ import { hash, type Calldata, num as numUtils, + TransactionFinalityStatus, + TransactionExecutionStatus, + TransactionType, } from 'starknet'; import { BIP44CoinTypeNode, getBIP44AddressKeyDeriver, } from '@metamask/key-tree'; -import { AccContract } from '../src/types/snapState'; +import { AccContract, Transaction } from '../src/types/snapState'; import { ACCOUNT_CLASS_HASH, ACCOUNT_CLASS_HASH_LEGACY, + PRELOADED_TOKENS, PROXY_CONTRACT_HASH, } from '../src/utils/constants'; import { grindKey } from '../src/utils/keyPair'; @@ -121,3 +125,135 @@ export async function generateAccounts( } return accounts; } + +/** + * Method to generate transactions. + * + * @param params + * @param params.chainId - Starknet Chain Id. + * @param params.address - Address of the account. + * @param params.contractAddresses - Contract addresses to generate transactions. + * @param params.txnTypes - Array of transaction types. + * @param params.finalityStatuses - Array of transaction finality status. + * @param params.executionStatuses - Array of transaction execution status. + * @param params.cnt - Number of transaction to generate. + * @returns An array of transaction object. + */ +export function generateTransactions({ + chainId, + address, + contractAddresses = PRELOADED_TOKENS.map((token) => token.address), + txnTypes = Object.values(TransactionType), + finalityStatuses = Object.values(TransactionFinalityStatus), + executionStatuses = Object.values(TransactionExecutionStatus), + // The timestamp from data source is in seconds + timestamp = Math.floor(Date.now() / 1000), + cnt = 1, +}: { + chainId: constants.StarknetChainId; + address: string; + contractAddresses?: string[]; + txnTypes?: TransactionType[]; + finalityStatuses?: TransactionFinalityStatus[]; + executionStatuses?: TransactionExecutionStatus[]; + timestamp?: number; + cnt?: number; +}): Transaction[] { + const transaction = { + chainId: chainId, + contractAddress: '', + contractCallData: [], + contractFuncName: '', + senderAddress: address, + timestamp: timestamp, + txnHash: '', + txnType: '', + failureReason: '', + status: '', + executionStatus: '', + finalityStatus: '', + eventIds: [], + }; + let accumulatedTimestamp = timestamp; + let accumulatedTxnHash = BigInt( + '0x2a8c2d5d4908a6561de87ecb18a76305c64800e3f81b393b9988de1abd37284', + ); + + let createCnt = cnt; + let filteredTxnTypes = txnTypes; + const transactions: Transaction[] = []; + + // only 1 deploy account transaction to generate + if ( + txnTypes.includes(TransactionType.DEPLOY_ACCOUNT) || + txnTypes.includes(TransactionType.DEPLOY) + ) { + transactions.push({ + ...transaction, + contractAddress: address, + txnType: TransactionType.DEPLOY_ACCOUNT, + finalityStatus: TransactionFinalityStatus.ACCEPTED_ON_L1, + executionStatus: TransactionExecutionStatus.SUCCEEDED, + timestamp: accumulatedTimestamp, + txnHash: '0x' + accumulatedTxnHash.toString(16), + }); + createCnt -= 1; + // exclude deploy txnType + filteredTxnTypes = filteredTxnTypes.filter( + (type) => + type !== TransactionType.DEPLOY_ACCOUNT && + type !== TransactionType.DEPLOY, + ); + } + + if (filteredTxnTypes.length === 0) { + filteredTxnTypes = [TransactionType.INVOKE]; + } + + for (let i = 1; i <= createCnt; i++) { + const randomContractAddress = + contractAddresses[Math.floor(Math.random() * contractAddresses.length)]; + const randomTxnType = + filteredTxnTypes[Math.floor(Math.random() * filteredTxnTypes.length)]; + let randomFinalityStatus = + finalityStatuses[Math.floor(Math.random() * finalityStatuses.length)]; + let randomExecutionStatus = + executionStatuses[Math.floor(Math.random() * executionStatuses.length)]; + let randomContractFuncName = ['transfer', 'upgrade'][ + Math.floor(Math.random() * 2) + ]; + accumulatedTimestamp += i * 100; + accumulatedTxnHash += BigInt(i * 100); + + if (randomExecutionStatus === TransactionExecutionStatus.REJECTED) { + if ( + [ + TransactionFinalityStatus.NOT_RECEIVED, + TransactionFinalityStatus.RECEIVED, + TransactionFinalityStatus.ACCEPTED_ON_L1, + ].includes(randomFinalityStatus) + ) { + randomFinalityStatus = TransactionFinalityStatus.ACCEPTED_ON_L2; + } + } + + if (randomFinalityStatus === TransactionFinalityStatus.NOT_RECEIVED) { + randomFinalityStatus = TransactionFinalityStatus.ACCEPTED_ON_L2; + randomExecutionStatus = TransactionExecutionStatus.SUCCEEDED; + } + + transactions.push({ + ...transaction, + contractAddress: randomContractAddress, + txnType: randomTxnType, + finalityStatus: randomFinalityStatus, + executionStatus: randomExecutionStatus, + timestamp: accumulatedTimestamp, + contractFuncName: + randomTxnType === TransactionType.INVOKE ? randomContractFuncName : '', + txnHash: '0x' + accumulatedTxnHash.toString(16), + }); + } + + return transactions.sort((a, b) => b.timestamp - a.timestamp); +}