From 504533503fa30d04bd41fe5fa5242880bdb07bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20T=C3=B3rz?= Date: Tue, 8 Aug 2023 10:16:59 +0200 Subject: [PATCH 1/2] add timestamps to l2 txs --- .../controllers/L2TransactionController.ts | 12 +- .../collectors/FeederGatewayCollector.test.ts | 613 ++++++++++-------- .../core/collectors/FeederGatewayCollector.ts | 178 +++-- .../sync/LiveL2TransactionDownloader.test.ts | 5 +- .../core/sync/LiveL2TransactionDownloader.ts | 1 + .../database/L2TransactionRepository.test.ts | 31 +- .../database/L2TransactionRepository.ts | 95 ++- .../039_l2_transactions_timestamp.ts | 25 + .../src/peripherals/database/shared/types.ts | 1 + .../PerpetualLiveL2TransactionResponse.ts | 2 +- .../starkware/toPerpetualTransactions.ts | 2 + .../src/preview/data/l2Transactions.ts | 7 +- packages/frontend/src/preview/routes.ts | 14 +- .../PerpetualL2TransactionsTable.tsx | 7 + .../PerpetualL2TransactionDetailsPage.tsx | 1 - .../src/view/pages/l2-transaction/common.ts | 11 +- .../PerpetualConditionalTransferDetails.tsx | 3 + .../details/PerpetualDeleverageDetails.tsx | 4 + .../details/PerpetualDepositDetails.tsx | 4 + .../details/PerpetualForcedTradeDetails.tsx | 4 + .../PerpetualForcedWithdrawalDetails.tsx | 4 + .../details/PerpetualFundingTickDetails.tsx | 3 + .../details/PerpetualLiquidateDetails.tsx | 4 + .../PerpetualMultiTransactionDetails.tsx | 2 + .../PerpetualOraclePricesTickDetails.tsx | 3 + .../details/PerpetualTradeDetails.tsx | 4 + .../details/PerpetualTransferDetails.tsx | 3 + .../PerpetualWithdrawalToAddressDetails.tsx | 3 + .../components/details/index.tsx | 15 +- 29 files changed, 680 insertions(+), 381 deletions(-) create mode 100644 packages/backend/src/peripherals/database/migrations/039_l2_transactions_timestamp.ts diff --git a/packages/backend/src/api/controllers/L2TransactionController.ts b/packages/backend/src/api/controllers/L2TransactionController.ts index 7116cbd44..a04de5467 100644 --- a/packages/backend/src/api/controllers/L2TransactionController.ts +++ b/packages/backend/src/api/controllers/L2TransactionController.ts @@ -123,7 +123,7 @@ export class L2TransactionController { aggregatedL2Transaction: AggregatedL2TransactionRecord, multiIndex: number | undefined, altIndex: number | undefined - ) { + ): AggregatedL2TransactionRecord | undefined { if (multiIndex !== undefined && altIndex !== undefined) { const altTransaction = aggregatedL2Transaction.alternativeTransactions[altIndex] @@ -141,7 +141,10 @@ export class L2TransactionController { return { ...aggregatedL2Transaction, - originalTransaction: multiTransaction, + originalTransaction: { + timestamp: altTransaction.timestamp, + ...multiTransaction, + }, alternativeTransactions: [], } } @@ -161,7 +164,10 @@ export class L2TransactionController { return { ...aggregatedL2Transaction, - originalTransaction: multiTransaction, + originalTransaction: { + timestamp: aggregatedL2Transaction.originalTransaction.timestamp, + ...multiTransaction, + }, alternativeTransactions: [], } } diff --git a/packages/backend/src/core/collectors/FeederGatewayCollector.test.ts b/packages/backend/src/core/collectors/FeederGatewayCollector.test.ts index 9ee205950..4c3ddde22 100644 --- a/packages/backend/src/core/collectors/FeederGatewayCollector.test.ts +++ b/packages/backend/src/core/collectors/FeederGatewayCollector.test.ts @@ -16,35 +16,27 @@ import { FeederGatewayCollector } from './FeederGatewayCollector' describe(FeederGatewayCollector.name, () => { describe(FeederGatewayCollector.prototype.collect.name, () => { it('should collect transactions from scratch if no transactions were synced before', async () => { - const mockFeederGatewayClient = mockObject({ - getPerpetualBatchInfo: mockFn(async (batchId: number) => { - return { - transactionsInfo: fakeTransactionsInfo(batchId), - } as PerpetualBatchInfo - }), - }) - const knexTransaction = mockObject({}) + const knexTransaction = mockObject() const mockL2TransactionRepository = mockObject({ findLatestStateUpdateId: mockFn().resolvesTo(undefined), - addFeederGatewayTransaction: mockFn().resolvesTo(1), runInTransactionWithLockedTable: mockFn( async (fun: (trx: Knex.Transaction) => Promise) => { await fun(knexTransaction) } ), - deleteByTransactionIds: mockFn().resolvesTo(undefined), }) const mockStateUpdateRepository = mockObject({ - findById: mockFn(async (id: number) => fakeStateUpdateRecord(id)), findLast: mockFn().resolvesTo(fakeStateUpdateRecord(5)), }) const feederGatewayCollector = new FeederGatewayCollector( - mockFeederGatewayClient, + mockObject(), mockL2TransactionRepository, mockStateUpdateRepository, Logger.SILENT, true ) + const mockCollectForStateUpdate = mockFn().resolvesTo(undefined) + feederGatewayCollector.collectForStateUpdate = mockCollectForStateUpdate await feederGatewayCollector.collect() @@ -55,112 +47,37 @@ describe(FeederGatewayCollector.name, () => { expect( mockL2TransactionRepository.runInTransactionWithLockedTable ).toHaveBeenCalledTimes(1) + for (const i of [1, 2, 3, 4, 5]) { - expect(mockStateUpdateRepository.findById).toHaveBeenNthCalledWith(i, i) - const stateUpdate = fakeStateUpdateRecord(i) - expect( - mockL2TransactionRepository.deleteByTransactionIds - ).toHaveBeenNthCalledWith( + expect(mockCollectForStateUpdate).toHaveBeenCalledWith( i, - [(i - 1) * 2 + 1, (i - 1) * 2 + 2], - knexTransaction - ) - expect( - mockFeederGatewayClient.getPerpetualBatchInfo - ).toHaveBeenNthCalledWith(i, stateUpdate.batchId) - const transactionsInfo = fakeTransactionsInfo(stateUpdate.batchId) - expect( - mockL2TransactionRepository.addFeederGatewayTransaction - ).toHaveBeenNthCalledWith( - 4 * (i - 1) + 1, - { - stateUpdateId: stateUpdate.id, - blockNumber: stateUpdate.blockNumber, - transactionId: transactionsInfo[0]!.originalTransactionId, - data: transactionsInfo[0]!.originalTransaction, - state: undefined, - }, - knexTransaction - ) - expect( - mockL2TransactionRepository.addFeederGatewayTransaction - ).toHaveBeenNthCalledWith( - 4 * (i - 1) + 2, - { - stateUpdateId: stateUpdate.id, - blockNumber: stateUpdate.blockNumber, - transactionId: transactionsInfo[1]!.originalTransactionId, - data: transactionsInfo[1]!.originalTransaction, - state: 'replaced', - }, - knexTransaction - ) - expect( - mockL2TransactionRepository.addFeederGatewayTransaction - ).toHaveBeenNthCalledWith( - 4 * (i - 1) + 3, - { - stateUpdateId: stateUpdate.id, - blockNumber: stateUpdate.blockNumber, - transactionId: transactionsInfo[1]!.originalTransactionId, - data: transactionsInfo[1]!.alternativeTransactions![0]!, - state: 'alternative', - }, - knexTransaction - ) - expect( - mockL2TransactionRepository.addFeederGatewayTransaction - ).toHaveBeenNthCalledWith( - 4 * (i - 1) + 4, - { - stateUpdateId: stateUpdate.id, - blockNumber: stateUpdate.blockNumber, - transactionId: transactionsInfo[1]!.originalTransactionId, - data: transactionsInfo[1]!.alternativeTransactions![1]!, - state: 'alternative', - }, knexTransaction ) } - expect( - mockL2TransactionRepository.findLatestStateUpdateId - ).toHaveBeenExhausted() - expect(mockStateUpdateRepository.findById).toHaveBeenExhausted() - expect( - mockL2TransactionRepository.addFeederGatewayTransaction - ).toHaveBeenExhausted() }) it('should collect transactions from last state update if some transactions were synced before', async () => { - const mockFeederGatewayClient = mockObject({ - getPerpetualBatchInfo: mockFn(async (batchId: number) => { - return { - transactionsInfo: fakeTransactionsInfo(batchId), - } as PerpetualBatchInfo - }), - }) const knexTransaction = mockObject({}) const mockL2TransactionRepository = mockObject({ findLatestStateUpdateId: mockFn().resolvesTo(6), - addFeederGatewayTransaction: mockFn().resolvesTo(1), runInTransactionWithLockedTable: mockFn( async (fun: (trx: Knex.Transaction) => Promise) => { await fun(knexTransaction) } ), - deleteByTransactionIds: mockFn().resolvesTo(0), }) const mockStateUpdateRepository = mockObject({ - findById: mockFn(async (id: number) => fakeStateUpdateRecord(id)), findLast: mockFn().resolvesTo(fakeStateUpdateRecord(10)), }) const feederGatewayCollector = new FeederGatewayCollector( - mockFeederGatewayClient, + mockObject(), mockL2TransactionRepository, mockStateUpdateRepository, Logger.SILENT, true ) + const mockCollectForStateUpdate = mockFn().resolvesTo(undefined) + feederGatewayCollector.collectForStateUpdate = mockCollectForStateUpdate await feederGatewayCollector.collect() @@ -174,124 +91,17 @@ describe(FeederGatewayCollector.name, () => { ).toHaveBeenCalledTimes(1) for (const i of [7, 8, 9, 10]) { - expect(mockStateUpdateRepository.findById).toHaveBeenNthCalledWith( - i - 6, - i - ) - const stateUpdate = fakeStateUpdateRecord(i) - expect( - mockL2TransactionRepository.deleteByTransactionIds - ).toHaveBeenNthCalledWith( - i - 6, - [(i - 1) * 2 + 1, (i - 1) * 2 + 2], - knexTransaction - ) - expect( - mockFeederGatewayClient.getPerpetualBatchInfo - ).toHaveBeenNthCalledWith(i - 6, stateUpdate.batchId) - const transactionsInfo = fakeTransactionsInfo(stateUpdate.batchId) - expect( - mockL2TransactionRepository.addFeederGatewayTransaction - ).toHaveBeenNthCalledWith( - 4 * (i - 7) + 1, - { - stateUpdateId: stateUpdate.id, - blockNumber: stateUpdate.blockNumber, - transactionId: transactionsInfo[0]!.originalTransactionId, - data: transactionsInfo[0]!.originalTransaction, - state: undefined, - }, - knexTransaction - ) - expect( - mockL2TransactionRepository.addFeederGatewayTransaction - ).toHaveBeenNthCalledWith( - 4 * (i - 7) + 2, - { - stateUpdateId: stateUpdate.id, - blockNumber: stateUpdate.blockNumber, - transactionId: transactionsInfo[1]!.originalTransactionId, - data: transactionsInfo[1]!.originalTransaction, - state: 'replaced', - }, - knexTransaction - ) - expect( - mockL2TransactionRepository.addFeederGatewayTransaction - ).toHaveBeenNthCalledWith( - 4 * (i - 7) + 3, - { - stateUpdateId: stateUpdate.id, - blockNumber: stateUpdate.blockNumber, - transactionId: transactionsInfo[1]!.originalTransactionId, - data: transactionsInfo[1]!.alternativeTransactions![0]!, - state: 'alternative', - }, - knexTransaction - ) - expect( - mockL2TransactionRepository.addFeederGatewayTransaction - ).toHaveBeenNthCalledWith( - 4 * (i - 7) + 4, - { - stateUpdateId: stateUpdate.id, - blockNumber: stateUpdate.blockNumber, - transactionId: transactionsInfo[1]!.originalTransactionId, - data: transactionsInfo[1]!.alternativeTransactions![1]!, - state: 'alternative', - }, + expect(mockCollectForStateUpdate).toHaveBeenCalledWith( + i, knexTransaction ) } - expect( - mockL2TransactionRepository.findLatestStateUpdateId - ).toHaveBeenExhausted() - expect(mockStateUpdateRepository.findById).toHaveBeenExhausted() - expect( - mockL2TransactionRepository.addFeederGatewayTransaction - ).toHaveBeenExhausted() - }) - - it('should stop collecting transactions if there is no batch data', async () => { - const knexTransaction = mockObject() - const mockedL2TransactionRepository = mockObject( - { - findLatestStateUpdateId: mockFn().resolvesTo(undefined), - addFeederGatewayTransaction: mockFn(), - runInTransactionWithLockedTable: mockFn( - async (fun: (trx: Knex.Transaction) => Promise) => { - await fun(knexTransaction) - } - ), - } - ) - const feederGatewayCollector = new FeederGatewayCollector( - mockObject({ - getPerpetualBatchInfo: mockFn().resolvesTo(undefined), - }), - mockedL2TransactionRepository, - mockObject({ - findById: mockFn().resolvesTo({} as StateUpdateRecord), - findLast: mockFn().resolvesTo(fakeStateUpdateRecord(10)), - }), - Logger.SILENT, - true - ) - - await feederGatewayCollector.collect() - expect( - mockedL2TransactionRepository.runInTransactionWithLockedTable - ).toHaveBeenCalledTimes(1) - expect( - mockedL2TransactionRepository.addFeederGatewayTransaction - ).not.toHaveBeenCalled() }) it('should stop collecting transactions if there is no state update in db', async () => { const mockedL2TransactionRepository = mockObject( { findLatestStateUpdateId: mockFn(), - addFeederGatewayTransaction: mockFn(), } ) const mockedStateUpdateRepository = mockObject({ @@ -310,9 +120,6 @@ describe(FeederGatewayCollector.name, () => { expect( mockedL2TransactionRepository.findLatestStateUpdateId ).not.toHaveBeenCalled() - expect( - mockedL2TransactionRepository.addFeederGatewayTransaction - ).not.toHaveBeenCalled() }) it('should not do anything if l2 transactions are disabled', async () => { @@ -337,39 +144,300 @@ describe(FeederGatewayCollector.name, () => { }) }) - it('should throw error if there is no state update in db for given stateUpdateId', async () => { - const knexTransaction = mockObject() - const mockedL2TransactionRepository = mockObject({ - findLatestStateUpdateId: mockFn().resolvesTo(5), - addFeederGatewayTransaction: mockFn(), - runInTransactionWithLockedTable: mockFn( - async (fun: (trx: Knex.Transaction) => Promise) => { - await fun(knexTransaction) - } - ), + describe(FeederGatewayCollector.prototype.collectForStateUpdate.name, () => { + it('should collect l2 transactions for given state update id', async () => { + const stateUpdateId = 6 + const timestampsGroupedByTransactionId = { + 11: [Timestamp(100), Timestamp(200)], + } + const mockFeederGatewayClient = mockObject({ + getPerpetualBatchInfo: mockFn(async (batchId: number) => + fakePerpetualBatchInfo(batchId) + ), + }) + const knexTransaction = mockObject({}) + const mockL2TransactionRepository = mockObject({ + findLatestStateUpdateId: mockFn().resolvesTo(undefined), + addFeederGatewayTransaction: mockFn().resolvesTo(1), + runInTransactionWithLockedTable: mockFn( + async (fun: (trx: Knex.Transaction) => Promise) => { + await fun(knexTransaction) + } + ), + getTimestampsGroupedByTransactionId: mockFn().resolvesTo( + timestampsGroupedByTransactionId + ), + deleteByTransactionIds: mockFn().resolvesTo(undefined), + }) + const mockStateUpdateRepository = mockObject({ + findById: mockFn(async (id: number) => fakeStateUpdateRecord(id)), + findLast: mockFn().resolvesTo(fakeStateUpdateRecord(5)), + }) + const feederGatewayCollector = new FeederGatewayCollector( + mockFeederGatewayClient, + mockL2TransactionRepository, + mockStateUpdateRepository, + Logger.SILENT, + true + ) + const getL2TransactionTimestampMockFn = mockFn() + .returnsOnce(Timestamp(100)) + .returnsOnce(Timestamp(200)) + .returnsOnce(Timestamp(300)) + .returnsOnce(Timestamp(400)) + + feederGatewayCollector.getL2TransactionTimestamp = + getL2TransactionTimestampMockFn + + await feederGatewayCollector.collectForStateUpdate( + stateUpdateId, + knexTransaction + ) + + expect(mockStateUpdateRepository.findById).toHaveBeenOnlyCalledWith( + stateUpdateId, + knexTransaction + ) + const stateUpdate = fakeStateUpdateRecord(stateUpdateId) + expect( + mockFeederGatewayClient.getPerpetualBatchInfo + ).toHaveBeenOnlyCalledWith(stateUpdate.batchId) + + expect( + mockL2TransactionRepository.getTimestampsGroupedByTransactionId + ).toHaveBeenOnlyCalledWith([11, 12], knexTransaction) + expect( + mockL2TransactionRepository.deleteByTransactionIds + ).toHaveBeenOnlyCalledWith([11, 12], knexTransaction) + + const perpetualBatchInfo = fakePerpetualBatchInfo(stateUpdate.batchId) + expect(getL2TransactionTimestampMockFn).toHaveBeenNthCalledWith( + 1, + timestampsGroupedByTransactionId, + perpetualBatchInfo.transactionsInfo[0]!.originalTransactionId, + perpetualBatchInfo.timeCreated + ) + expect( + mockL2TransactionRepository.addFeederGatewayTransaction + ).toHaveBeenNthCalledWith( + 1, + { + stateUpdateId: stateUpdate.id, + blockNumber: stateUpdate.blockNumber, + transactionId: + perpetualBatchInfo.transactionsInfo[0]!.originalTransactionId, + timestamp: Timestamp(100), + data: perpetualBatchInfo.transactionsInfo[0]!.originalTransaction, + state: undefined, + }, + knexTransaction + ) + expect(getL2TransactionTimestampMockFn).toHaveBeenNthCalledWith( + 2, + timestampsGroupedByTransactionId, + perpetualBatchInfo.transactionsInfo[1]!.originalTransactionId, + perpetualBatchInfo.timeCreated + ) + expect( + mockL2TransactionRepository.addFeederGatewayTransaction + ).toHaveBeenNthCalledWith( + 2, + { + stateUpdateId: stateUpdate.id, + blockNumber: stateUpdate.blockNumber, + transactionId: + perpetualBatchInfo.transactionsInfo[1]!.originalTransactionId, + timestamp: Timestamp(200), + data: perpetualBatchInfo.transactionsInfo[1]!.originalTransaction, + state: 'replaced', + }, + knexTransaction + ) + expect(getL2TransactionTimestampMockFn).toHaveBeenNthCalledWith( + 3, + timestampsGroupedByTransactionId, + perpetualBatchInfo.transactionsInfo[1]!.originalTransactionId, + perpetualBatchInfo.timeCreated, + 0 + ) + expect( + mockL2TransactionRepository.addFeederGatewayTransaction + ).toHaveBeenNthCalledWith( + 3, + { + stateUpdateId: stateUpdate.id, + blockNumber: stateUpdate.blockNumber, + transactionId: + perpetualBatchInfo.transactionsInfo[1]!.originalTransactionId, + data: perpetualBatchInfo.transactionsInfo[1]! + .alternativeTransactions![0]!, + timestamp: Timestamp(300), + state: 'alternative', + }, + knexTransaction + ) + expect(getL2TransactionTimestampMockFn).toHaveBeenNthCalledWith( + 4, + timestampsGroupedByTransactionId, + perpetualBatchInfo.transactionsInfo[1]!.originalTransactionId, + perpetualBatchInfo.timeCreated, + 1 + ) + expect( + mockL2TransactionRepository.addFeederGatewayTransaction + ).toHaveBeenNthCalledWith( + 4, + { + stateUpdateId: stateUpdate.id, + blockNumber: stateUpdate.blockNumber, + transactionId: + perpetualBatchInfo.transactionsInfo[1]!.originalTransactionId, + data: perpetualBatchInfo.transactionsInfo[1]! + .alternativeTransactions![1]!, + timestamp: Timestamp(400), + state: 'alternative', + }, + knexTransaction + ) + expect(getL2TransactionTimestampMockFn).toHaveBeenExhausted() }) - const feederGatewayCollector = new FeederGatewayCollector( - mockObject(), - mockedL2TransactionRepository, - mockObject({ + + it('should throw error if there is no state update in db for given stateUpdateId', async () => { + const stateUpdateId = 1 + const knexTransaction = mockObject() + const mockStateUpdateRepository = mockObject({ findById: mockFn().resolvesTo(undefined), - findLast: mockFn().resolvesTo(fakeStateUpdateRecord(10)), - }), - Logger.SILENT, - true - ) + }) + const feederGatewayCollector = new FeederGatewayCollector( + mockObject(), + mockObject(), + mockStateUpdateRepository, + Logger.SILENT, + true + ) - await expect(feederGatewayCollector.collect()).toBeRejectedWith( - `State update 6 not found` - ) - expect( - mockedL2TransactionRepository.runInTransactionWithLockedTable - ).toHaveBeenCalledTimes(1) - expect( - mockedL2TransactionRepository.addFeederGatewayTransaction - ).not.toHaveBeenCalled() + await expect( + feederGatewayCollector.collectForStateUpdate( + stateUpdateId, + knexTransaction + ) + ).toBeRejectedWith(`State update ${stateUpdateId} not found`) + expect(mockStateUpdateRepository.findById).toHaveBeenOnlyCalledWith( + stateUpdateId, + knexTransaction + ) + }) + + it('should stop collecting transactions if there is no batch data', async () => { + const stateUpdateId = 1 + const knexTransaction = mockObject() + const mockL2TransactionRepository = mockObject({ + getTimestampsGroupedByTransactionId: mockFn(), + }) + const feederGatewayCollector = new FeederGatewayCollector( + mockObject({ + getPerpetualBatchInfo: mockFn().resolvesTo(undefined), + }), + mockL2TransactionRepository, + mockObject({ + findById: mockFn().resolvesTo({} as StateUpdateRecord), + }), + Logger.SILENT, + true + ) + + await feederGatewayCollector.collectForStateUpdate( + stateUpdateId, + knexTransaction + ) + + expect( + mockL2TransactionRepository.getTimestampsGroupedByTransactionId + ).not.toHaveBeenCalled() + }) }) + describe( + FeederGatewayCollector.prototype.getL2TransactionTimestamp.name, + () => { + const feederGatewayCollector = new FeederGatewayCollector( + mockObject(), + mockObject(), + mockObject(), + Logger.SILENT, + mockObject() + ) + + it('should return timestamp from timestampsGroupedByTransactionId if it exists', () => { + const timestampsGroupedByTransactionId = { + 1: [Timestamp(100), Timestamp(200)], + } + const transactionId = 1 + const fallbackTimestamp = Timestamp(300) + + const timestamp = feederGatewayCollector.getL2TransactionTimestamp( + timestampsGroupedByTransactionId, + transactionId, + fallbackTimestamp + ) + + expect(timestamp).toEqual(Timestamp(100)) + }) + + it('should return fallbackTimestamp if there is no timestamp in timestampsGroupedByTransactionId', () => { + const timestampsGroupedByTransactionId = { + 1: [Timestamp(100), Timestamp(200)], + } + const transactionId = 2 + const fallbackTimestamp = Timestamp(300) + + const timestamp = feederGatewayCollector.getL2TransactionTimestamp( + timestampsGroupedByTransactionId, + transactionId, + fallbackTimestamp + ) + + expect(timestamp).toEqual(fallbackTimestamp) + }) + + it('should return timestamp from timestampsGroupedByTransactionId at given index if it exists', () => { + const timestampsGroupedByTransactionId = { + 1: [Timestamp(100), Timestamp(200)], + } + const transactionId = 1 + const fallbackTimestamp = Timestamp(300) + const index = 1 + + const timestamp = feederGatewayCollector.getL2TransactionTimestamp( + timestampsGroupedByTransactionId, + transactionId, + fallbackTimestamp, + index + ) + + expect(timestamp).toEqual(Timestamp(200)) + }) + + it('should return last timestamp if there is no timestamp in timestampsGroupedByTransactionId at given index', () => { + const timestampsGroupedByTransactionId = { + 1: [Timestamp(100), Timestamp(200)], + } + const transactionId = 1 + const fallbackTimestamp = Timestamp(300) + const index = 3 + + const timestamp = feederGatewayCollector.getL2TransactionTimestamp( + timestampsGroupedByTransactionId, + transactionId, + fallbackTimestamp, + index + ) + + expect(timestamp).toEqual(Timestamp(200)) + }) + } + ) + describe(FeederGatewayCollector.prototype.discardAfter.name, () => { const mockedL2TransactionRepository = mockObject({ deleteAfterBlock: mockFn().resolvesTo(1), @@ -400,50 +468,51 @@ const fakeStateUpdateRecord = (id: number) => blockNumber: id + 1000, } as StateUpdateRecord) -const fakeTransactionsInfo = ( - batchId: number -): PerpetualBatchInfo['transactionsInfo'] => { - return [ - { - wasReplaced: false, - originalTransactionId: 2 * batchId + 1, - originalTransaction: { - type: 'Deposit', - starkKey: StarkKey.fake(`1${batchId}`), - positionId: 1234n, - amount: 5000n, - }, - }, - { - wasReplaced: false, - originalTransactionId: 2 * batchId + 2, - originalTransaction: { - positionId: 1234n, - starkKey: StarkKey.fake(`2${batchId}`), - ethereumAddress: EthereumAddress.fake(`3${batchId}`), - amount: 12345n, - nonce: 10n, - expirationTimestamp: Timestamp(1234), - signature: { - r: Hash256.fake(`4${batchId}`), - s: Hash256.fake(`5${batchId}`), - }, - type: 'WithdrawalToAddress', - }, - alternativeTransactions: [ - { +const fakePerpetualBatchInfo = (batchId: number): PerpetualBatchInfo => { + return { + timeCreated: Timestamp(1000 * batchId), + transactionsInfo: [ + { + wasReplaced: false, + originalTransactionId: 2 * batchId + 1, + originalTransaction: { type: 'Deposit', - starkKey: StarkKey.fake(`6${batchId}`), + starkKey: StarkKey.fake(`1${batchId}`), positionId: 1234n, amount: 5000n, }, - { - type: 'Deposit', - starkKey: StarkKey.fake(`7${batchId}`), + }, + { + wasReplaced: false, + originalTransactionId: 2 * batchId + 2, + originalTransaction: { positionId: 1234n, - amount: 5000n, + starkKey: StarkKey.fake(`2${batchId}`), + ethereumAddress: EthereumAddress.fake(`3${batchId}`), + amount: 12345n, + nonce: 10n, + expirationTimestamp: Timestamp(1234), + signature: { + r: Hash256.fake(`4${batchId}`), + s: Hash256.fake(`5${batchId}`), + }, + type: 'WithdrawalToAddress', }, - ], - }, - ] as PerpetualBatchInfo['transactionsInfo'] + alternativeTransactions: [ + { + type: 'Deposit', + starkKey: StarkKey.fake(`6${batchId}`), + positionId: 1234n, + amount: 5000n, + }, + { + type: 'Deposit', + starkKey: StarkKey.fake(`7${batchId}`), + positionId: 1234n, + amount: 5000n, + }, + ], + }, + ], + } as PerpetualBatchInfo } diff --git a/packages/backend/src/core/collectors/FeederGatewayCollector.ts b/packages/backend/src/core/collectors/FeederGatewayCollector.ts index 3c83d3ef4..5678aafc3 100644 --- a/packages/backend/src/core/collectors/FeederGatewayCollector.ts +++ b/packages/backend/src/core/collectors/FeederGatewayCollector.ts @@ -1,4 +1,6 @@ +import { Timestamp } from '@explorer/types' import { Logger } from '@l2beat/backend-tools' +import { Knex } from 'knex' import { L2TransactionRepository } from '../../peripherals/database/L2TransactionRepository' import { StateUpdateRepository } from '../../peripherals/database/StateUpdateRepository' @@ -31,75 +33,123 @@ export class FeederGatewayCollector { stateUpdateId <= latestStateUpdate.id; stateUpdateId++ ) { - const stateUpdate = await this.stateUpdateRepository.findById( - stateUpdateId - ) - - // We throw an error if the state update is not found. - // It's a critical error and we should stop the sync process, becasue - // latestStateUpdate was found, but the state update before it was not. - if (!stateUpdate) { - throw new Error(`State update ${stateUpdateId} not found`) - } - - this.logger.info(`Collecting transactions from Feeder Gateway`, { - stateUpdateId: stateUpdate.id, - }) - - const data = await this.feederGatewayClient.getPerpetualBatchInfo( - stateUpdate.batchId - ) - - // We stop collecting transactions if there is no batch data. - // It will not stop the sync process, but it will stop the transaction collection. - // We will try to collect unsynced transactions on the next state update sync. - if (!data) { - return - } - - const transactionIds = data.transactionsInfo.map( - (tx) => tx.originalTransactionId - ) - await this.l2TransactionRepository.deleteByTransactionIds( - transactionIds, - trx - ) - for (const transactionInfo of data.transactionsInfo) { - const isReplaced = - !!transactionInfo.alternativeTransactions && - transactionInfo.alternativeTransactions.length > 0 - - await this.l2TransactionRepository.addFeederGatewayTransaction( - { - stateUpdateId: stateUpdate.id, - blockNumber: stateUpdate.blockNumber, - transactionId: transactionInfo.originalTransactionId, - data: transactionInfo.originalTransaction, - state: isReplaced ? 'replaced' : undefined, - }, - trx - ) - if (!transactionInfo.alternativeTransactions) { - continue - } - for (const alternativeTransaction of transactionInfo.alternativeTransactions) { - await this.l2TransactionRepository.addFeederGatewayTransaction( - { - stateUpdateId: stateUpdate.id, - blockNumber: stateUpdate.blockNumber, - transactionId: transactionInfo.originalTransactionId, - data: alternativeTransaction, - state: 'alternative', - }, - trx - ) - } - } + await this.collectForStateUpdate(stateUpdateId, trx) } } ) } + async collectForStateUpdate(stateUpdateId: number, trx: Knex.Transaction) { + const stateUpdate = await this.stateUpdateRepository.findById( + stateUpdateId, + trx + ) + + // We throw an error if the state update is not found. + // It's a critical error and we should stop the sync process, becasue + // latestStateUpdate was found, but the state update before it was not. + if (!stateUpdate) { + throw new Error(`State update ${stateUpdateId} not found`) + } + + this.logger.info(`Collecting transactions from Feeder Gateway`, { + stateUpdateId: stateUpdate.id, + }) + + const data = await this.feederGatewayClient.getPerpetualBatchInfo( + stateUpdate.batchId + ) + + // We stop collecting transactions if there is no batch data. + // It will not stop the sync process, but it will stop the transaction collection. + // We will try to collect unsynced transactions on the next state update sync. + if (!data) { + return + } + + const transactionIds = data.transactionsInfo.map( + (tx) => tx.originalTransactionId + ) + const timestampsGroupedByTransactionId = + await this.l2TransactionRepository.getTimestampsGroupedByTransactionId( + transactionIds, + trx + ) + + await this.l2TransactionRepository.deleteByTransactionIds( + transactionIds, + trx + ) + + for (const transactionInfo of data.transactionsInfo) { + const isReplaced = + !!transactionInfo.alternativeTransactions && + transactionInfo.alternativeTransactions.length > 0 + + const timestamp = this.getL2TransactionTimestamp( + timestampsGroupedByTransactionId, + transactionInfo.originalTransactionId, + data.timeCreated + ) + + await this.l2TransactionRepository.addFeederGatewayTransaction( + { + stateUpdateId: stateUpdate.id, + blockNumber: stateUpdate.blockNumber, + transactionId: transactionInfo.originalTransactionId, + data: transactionInfo.originalTransaction, + timestamp, + state: isReplaced ? 'replaced' : undefined, + }, + trx + ) + if (!transactionInfo.alternativeTransactions) { + continue + } + + for (const [ + altIndex, + alternativeTransaction, + ] of transactionInfo.alternativeTransactions.entries()) { + const timestamp = this.getL2TransactionTimestamp( + timestampsGroupedByTransactionId, + transactionInfo.originalTransactionId, + data.timeCreated, + altIndex + ) + + await this.l2TransactionRepository.addFeederGatewayTransaction( + { + stateUpdateId: stateUpdate.id, + blockNumber: stateUpdate.blockNumber, + transactionId: transactionInfo.originalTransactionId, + data: alternativeTransaction, + timestamp, + state: 'alternative', + }, + trx + ) + } + } + } + + getL2TransactionTimestamp( + timestampsGroupedByTransactionId: Record, + transactionId: number, + fallbackTimestamp: Timestamp | undefined, + altIndex?: number + ) { + const timestamps = timestampsGroupedByTransactionId[transactionId] + if (!timestamps) { + return fallbackTimestamp + } + if (altIndex !== undefined) { + return timestamps.at(altIndex + 1) ?? timestamps.at(-1) + } + + return timestamps.at(0) + } + async discardAfter(blockNumber: number) { await this.l2TransactionRepository.deleteAfterBlock(blockNumber) } diff --git a/packages/backend/src/core/sync/LiveL2TransactionDownloader.test.ts b/packages/backend/src/core/sync/LiveL2TransactionDownloader.test.ts index 8c9121821..e57f314c4 100644 --- a/packages/backend/src/core/sync/LiveL2TransactionDownloader.test.ts +++ b/packages/backend/src/core/sync/LiveL2TransactionDownloader.test.ts @@ -1,4 +1,4 @@ -import { StarkKey } from '@explorer/types' +import { StarkKey, Timestamp } from '@explorer/types' import { Logger } from '@l2beat/backend-tools' import { expect, mockFn, mockObject } from 'earl' import { Knex } from 'knex' @@ -18,6 +18,7 @@ const fakeL2Transaction = ( ): PerpetualL2Transaction => ({ thirdPartyId: 1024, transactionId: 2048, + timestamp: Timestamp(4096), transaction: { type: 'Deposit', positionId: 4096n, @@ -91,6 +92,7 @@ describe(LiveL2TransactionDownloader.name, () => { i + 1, { transactionId: tx.transactionId, + timestamp: tx.timestamp, data: tx.transaction, }, mockKnexTransaction @@ -118,6 +120,7 @@ describe(LiveL2TransactionDownloader.name, () => { firstTxs.length + i + 1, { data: tx.transaction, + timestamp: tx.timestamp, transactionId: tx.transactionId, }, mockKnexTransaction diff --git a/packages/backend/src/core/sync/LiveL2TransactionDownloader.ts b/packages/backend/src/core/sync/LiveL2TransactionDownloader.ts index dbd7ce095..c37541652 100644 --- a/packages/backend/src/core/sync/LiveL2TransactionDownloader.ts +++ b/packages/backend/src/core/sync/LiveL2TransactionDownloader.ts @@ -144,6 +144,7 @@ export class LiveL2TransactionDownloader { const record = { transactionId: transaction.transactionId, data: transaction.transaction, + timestamp: transaction.timestamp, } await this.l2TransactionRepository.addLiveTransaction(record, trx) diff --git a/packages/backend/src/peripherals/database/L2TransactionRepository.test.ts b/packages/backend/src/peripherals/database/L2TransactionRepository.test.ts index bfadb1c87..e83da8786 100644 --- a/packages/backend/src/peripherals/database/L2TransactionRepository.test.ts +++ b/packages/backend/src/peripherals/database/L2TransactionRepository.test.ts @@ -14,6 +14,7 @@ const genericLiveMultiTransaction = ( transactions: PerpetualL2TransactionData[] ) => ({ transactionId: 1234, + timestamp: Timestamp(4321), data: { type: 'MultiTransaction', transactions, @@ -31,6 +32,7 @@ const genericMultiTransaction = ( const genericLiveDepositTransaction = { transactionId: 1234, + timestamp: Timestamp(4321), data: { type: 'Deposit', starkKey: StarkKey.fake(), @@ -48,6 +50,7 @@ const genericDepositTransaction = { const genericLiveWithdrawalToAddressTransaction = { transactionId: 1234, + timestamp: Timestamp(1234), data: { positionId: 1234n, starkKey: StarkKey.fake('2'), @@ -95,6 +98,7 @@ describe(L2TransactionRepository.name, () => { starkKeyA: genericDepositTransaction.data.starkKey, starkKeyB: undefined, data: genericDepositTransaction.data, + timestamp: genericDepositTransaction.timestamp, state: undefined, parentId: undefined, }) @@ -116,6 +120,7 @@ describe(L2TransactionRepository.name, () => { starkKeyA: genericDepositTransaction.data.starkKey, starkKeyB: undefined, data: genericDepositTransaction.data, + timestamp: genericDepositTransaction.timestamp, state: 'replaced', parentId: undefined, }) @@ -142,6 +147,7 @@ describe(L2TransactionRepository.name, () => { starkKeyA: genericDepositTransaction.data.starkKey, starkKeyB: undefined, data: genericDepositTransaction.data, + timestamp: genericDepositTransaction.timestamp, state: 'alternative', parentId: undefined, }) @@ -167,6 +173,7 @@ describe(L2TransactionRepository.name, () => { transactionId: record.transactionId, blockNumber: record.blockNumber, state: undefined, + timestamp: record.timestamp, parentId: undefined, starkKeyA: undefined, starkKeyB: undefined, @@ -178,6 +185,7 @@ describe(L2TransactionRepository.name, () => { transactionId: record.transactionId, blockNumber: record.blockNumber, state: undefined, + timestamp: record.timestamp, parentId: id, starkKeyA: StarkKey.fake('1'), starkKeyB: undefined, @@ -189,6 +197,7 @@ describe(L2TransactionRepository.name, () => { transactionId: record.transactionId, blockNumber: record.blockNumber, state: undefined, + timestamp: record.timestamp, parentId: id, starkKeyA: StarkKey.fake('2'), starkKeyB: undefined, @@ -249,6 +258,7 @@ describe(L2TransactionRepository.name, () => { starkKeyA: record.data.starkKey, starkKeyB: undefined, data: record.data, + timestamp: record.timestamp, state: undefined, parentId: undefined, }) @@ -265,6 +275,7 @@ describe(L2TransactionRepository.name, () => { blockNumber: undefined, starkKeyA: record.data.starkKey, starkKeyB: undefined, + timestamp: record.timestamp, data: record.data, state: 'replaced', parentId: undefined, @@ -278,21 +289,27 @@ describe(L2TransactionRepository.name, () => { starkKeyA: alternativeRecord.data.starkKey, starkKeyB: undefined, data: alternativeRecord.data, + timestamp: alternativeRecord.timestamp, state: 'alternative', parentId: undefined, }) }) - it('does not add transaction if transaction with the same transaction id already exists and is included', async () => { - await repository.addFeederGatewayTransaction(genericDepositTransaction) - - const id = await repository.addLiveTransaction( - genericLiveDepositTransaction + it('updates transaction timestamp if transaction with the same transaction id already exists and is included', async () => { + const liveTxTimestamp = Timestamp(100000) + const id = await repository.addFeederGatewayTransaction( + genericDepositTransaction ) + await repository.addLiveTransaction({ + ...genericLiveDepositTransaction, + timestamp: liveTxTimestamp, + }) + const transaction = await repository.findById(id) - expect(id).toEqual(0) - expect(transaction?.stateUpdateId).toBeNullish() + + expect(id).not.toEqual(0) + expect(transaction?.timestamp).toEqual(liveTxTimestamp) }) }) diff --git a/packages/backend/src/peripherals/database/L2TransactionRepository.ts b/packages/backend/src/peripherals/database/L2TransactionRepository.ts index 41578359d..314693649 100644 --- a/packages/backend/src/peripherals/database/L2TransactionRepository.ts +++ b/packages/backend/src/peripherals/database/L2TransactionRepository.ts @@ -2,7 +2,7 @@ import { PerpetualL2MultiTransactionData, PerpetualL2TransactionData, } from '@explorer/shared' -import { StarkKey } from '@explorer/types' +import { StarkKey, Timestamp } from '@explorer/types' import { Logger } from '@l2beat/backend-tools' import { Knex } from 'knex' import { L2TransactionRow } from 'knex/types/tables' @@ -21,7 +21,7 @@ import { import { BaseRepository } from './shared/BaseRepository' import { Database } from './shared/Database' -interface Record< +export interface L2TransactionRecord< T extends PerpetualL2TransactionData['type'] = PerpetualL2TransactionData['type'] > { id: number @@ -33,20 +33,20 @@ interface Record< starkKeyA: StarkKey | undefined starkKeyB: StarkKey | undefined data: Extract + timestamp: Timestamp | undefined } -interface AggregatedRecord { +export interface AggregatedL2TransactionRecord { id: number transactionId: number stateUpdateId: number | undefined blockNumber: number | undefined - originalTransaction: PerpetualL2TransactionData - alternativeTransactions: PerpetualL2TransactionData[] -} - -export type { - AggregatedRecord as AggregatedL2TransactionRecord, - Record as L2TransactionRecord, + originalTransaction: PerpetualL2TransactionData & { + timestamp: Timestamp | undefined + } + alternativeTransactions: (PerpetualL2TransactionData & { + timestamp: Timestamp | undefined + })[] } export class L2TransactionRepository extends BaseRepository { @@ -74,6 +74,9 @@ export class L2TransactionRepository extends BaseRepository { this.getPaginatedWithoutMultiByStateUpdateId ) this.getUserSpecificPaginated = this.wrapGet(this.getUserSpecificPaginated) + this.getTimestampsGroupedByTransactionId = this.wrapAny( + this.getTimestampsGroupedByTransactionId + ) this.findById = this.wrapFind(this.findById) this.findByTransactionId = this.wrapFind(this.findByTransactionId) this.findAggregatedByTransactionId = this.wrapFind( @@ -99,6 +102,7 @@ export class L2TransactionRepository extends BaseRepository { data: PerpetualL2TransactionData stateUpdateId: number blockNumber: number + timestamp: Timestamp | undefined state: 'replaced' | 'alternative' | undefined }, trx?: Knex.Transaction @@ -119,6 +123,7 @@ export class L2TransactionRepository extends BaseRepository { if (record.state !== 'alternative') { if (existing) { + console.log(record, existing, record.state) throw new Error( 'L2 Transaction already exists when adding from Feeder Gatway' ) @@ -132,6 +137,7 @@ export class L2TransactionRepository extends BaseRepository { record: { transactionId: number data: PerpetualL2TransactionData + timestamp: Timestamp | undefined }, trx?: Knex.Transaction ): Promise { @@ -152,7 +158,9 @@ export class L2TransactionRepository extends BaseRepository { existingRecord.transactionId === record.transactionId && existingRecord.stateUpdateId !== undefined ) { - return 0 + return await knex('l2_transactions') + .update({ timestamp: record.timestamp?.valueOf() }) + .where({ id: existingRecord.id }) } if (existingRecord.state === undefined) { @@ -175,6 +183,7 @@ export class L2TransactionRepository extends BaseRepository { record: { transactionId: number data: PerpetualL2TransactionData + timestamp: Timestamp | undefined parentId?: number stateUpdateId?: number blockNumber?: number @@ -207,6 +216,7 @@ export class L2TransactionRepository extends BaseRepository { record: { transactionId: number data: Exclude + timestamp: Timestamp | undefined parentId?: number stateUpdateId?: number blockNumber?: number @@ -225,6 +235,7 @@ export class L2TransactionRepository extends BaseRepository { state: record.state ?? null, stark_key_a: starkKeyA?.toString(), stark_key_b: starkKeyB?.toString(), + timestamp: record.timestamp?.valueOf(), type: record.data.type, data, }) @@ -237,6 +248,7 @@ export class L2TransactionRepository extends BaseRepository { record: { transactionId: number data: PerpetualL2MultiTransactionData + timestamp: Timestamp | undefined stateUpdateId?: number blockNumber?: number state: 'replaced' | 'alternative' | undefined @@ -250,6 +262,7 @@ export class L2TransactionRepository extends BaseRepository { transaction_id: record.transactionId, state_update_id: record.stateUpdateId, block_number: record.blockNumber, + timestamp: record.timestamp?.valueOf(), type: record.data.type, state: record.state ?? null, data, @@ -265,6 +278,7 @@ export class L2TransactionRepository extends BaseRepository { transactionId: record.transactionId, stateUpdateId: record.stateUpdateId, blockNumber: record.blockNumber, + timestamp: record.timestamp, data: transaction, parentId, state: record.state, @@ -487,10 +501,35 @@ export class L2TransactionRepository extends BaseRepository { return rows.map(toRecord) } + async getTimestampsGroupedByTransactionId( + transactionIds: number[], + trx?: Knex.Transaction + ): Promise> { + const knex = await this.knex(trx) + + const rows = await knex('l2_transactions') + .select('transaction_id', 'timestamp') + .whereIn('transaction_id', transactionIds) + .orderBy('timestamp', 'asc') + + const timestampsGroupedByTransactionId = rows.reduce< + Record + >((res, cur) => { + if (!cur.timestamp) return res + if (!res[cur.transaction_id]) { + res[cur.transaction_id] = [] + } + res[cur.transaction_id]?.push(Timestamp(cur.timestamp)) + return res + }, {}) + + return timestampsGroupedByTransactionId + } + async findById( id: number, trx?: Knex.Transaction - ): Promise { + ): Promise { const knex = await this.knex(trx) const row = await knex('l2_transactions').where({ id }).first() @@ -500,7 +539,7 @@ export class L2TransactionRepository extends BaseRepository { async findByTransactionId( transactionId: number, trx?: Knex.Transaction - ): Promise { + ): Promise { const knex = await this.knex(trx) const row = await knex('l2_transactions') .where({ transaction_id: transactionId }) @@ -511,7 +550,7 @@ export class L2TransactionRepository extends BaseRepository { async findAggregatedByTransactionId( id: number - ): Promise { + ): Promise { const knex = await this.knex() const originalTransaction = await knex('l2_transactions') .where({ transaction_id: id, parent_id: null }) @@ -533,7 +572,9 @@ export class L2TransactionRepository extends BaseRepository { return toAggregatedRecord(originalTransaction, alternativeTransactions) } - async findOldestByTransactionId(id: number): Promise { + async findOldestByTransactionId( + id: number + ): Promise { const knex = await this.knex() const row = await knex('l2_transactions') .where({ transaction_id: id }) @@ -556,7 +597,7 @@ export class L2TransactionRepository extends BaseRepository { return results?.state_update_id ? results.state_update_id : undefined } - async findLatestIncluded(): Promise { + async findLatestIncluded(): Promise { const knex = await this.knex() const row = await knex('l2_transactions') .whereNotNull('state_update_id') @@ -579,6 +620,7 @@ export class L2TransactionRepository extends BaseRepository { trx?: Knex.Transaction ) { const knex = await this.knex(trx) + return knex('l2_transactions') .whereIn('transaction_id', transactionIds) .delete() @@ -617,7 +659,7 @@ export class L2TransactionRepository extends BaseRepository { } } -function toRecord(row: L2TransactionRow): Record { +function toRecord(row: L2TransactionRow): L2TransactionRecord { return { id: row.id, transactionId: row.transaction_id, @@ -627,6 +669,7 @@ function toRecord(row: L2TransactionRow): Record { state: row.state ? row.state : undefined, starkKeyA: row.stark_key_a ? StarkKey(row.stark_key_a) : undefined, starkKeyB: row.stark_key_b ? StarkKey(row.stark_key_b) : undefined, + timestamp: row.timestamp ? Timestamp(row.timestamp) : undefined, data: decodeL2TransactionData(row.data), } } @@ -634,7 +677,7 @@ function toRecord(row: L2TransactionRow): Record { function toAggregatedRecord( transaction: L2TransactionRow, alternatives: L2TransactionRow[] -): AggregatedRecord { +): AggregatedL2TransactionRecord { return { id: transaction.id, transactionId: transaction.transaction_id, @@ -644,10 +687,18 @@ function toAggregatedRecord( blockNumber: transaction.block_number ? transaction.block_number : undefined, - originalTransaction: decodeL2TransactionData(transaction.data), - alternativeTransactions: alternatives.map((alternative) => - decodeL2TransactionData(alternative.data) - ), + originalTransaction: { + timestamp: transaction.timestamp + ? Timestamp(transaction.timestamp) + : undefined, + ...decodeL2TransactionData(transaction.data), + }, + alternativeTransactions: alternatives.map((alternative) => ({ + timestamp: alternative.timestamp + ? Timestamp(alternative.timestamp) + : undefined, + ...decodeL2TransactionData(alternative.data), + })), } } diff --git a/packages/backend/src/peripherals/database/migrations/039_l2_transactions_timestamp.ts b/packages/backend/src/peripherals/database/migrations/039_l2_transactions_timestamp.ts new file mode 100644 index 000000000..dd5b6799a --- /dev/null +++ b/packages/backend/src/peripherals/database/migrations/039_l2_transactions_timestamp.ts @@ -0,0 +1,25 @@ +/* + ====== IMPORTANT NOTICE ====== + +DO NOT EDIT OR RENAME THIS FILE + +This is a migration file. Once created the file should not be renamed or edited, +because migrations are only run once on the production server. + +If you find that something was incorrectly set up in the `up` function you +should create a new migration file that fixes the issue. + +*/ +import { Knex } from 'knex' + +export async function up(knex: Knex) { + await knex.schema.alterTable('l2_transactions', (table) => { + table.bigint('timestamp').nullable() + }) +} + +export async function down(knex: Knex) { + await knex.schema.alterTable('l2_transactions', (table) => { + table.dropColumn('timestamp') + }) +} diff --git a/packages/backend/src/peripherals/database/shared/types.ts b/packages/backend/src/peripherals/database/shared/types.ts index 452636141..808d8f552 100644 --- a/packages/backend/src/peripherals/database/shared/types.ts +++ b/packages/backend/src/peripherals/database/shared/types.ts @@ -300,6 +300,7 @@ declare module 'knex/types/tables' { stark_key_b: string | null data: PerpetualL2TransactionDataJson type: string + timestamp: bigint | null } interface Tables { diff --git a/packages/backend/src/peripherals/starkware/schema/PerpetualLiveL2TransactionResponse.ts b/packages/backend/src/peripherals/starkware/schema/PerpetualLiveL2TransactionResponse.ts index 7d2a05319..09de966f2 100644 --- a/packages/backend/src/peripherals/starkware/schema/PerpetualLiveL2TransactionResponse.ts +++ b/packages/backend/src/peripherals/starkware/schema/PerpetualLiveL2TransactionResponse.ts @@ -13,7 +13,7 @@ const PerpetualLiveL2TransactionResponseTransaction = z.object({ .transform((s) => PerpetualLiveL2TransactionResponseTransactionInfo.parse(JSON.parse(s)) ), - time_created: z.number().optional(), // Temporarily optional, only on goerli API + time_created: z.number(), }) export type PerpetualLiveL2TransactionResponse = z.infer< diff --git a/packages/backend/src/peripherals/starkware/toPerpetualTransactions.ts b/packages/backend/src/peripherals/starkware/toPerpetualTransactions.ts index 90164ef21..ac3711c02 100644 --- a/packages/backend/src/peripherals/starkware/toPerpetualTransactions.ts +++ b/packages/backend/src/peripherals/starkware/toPerpetualTransactions.ts @@ -24,6 +24,7 @@ export interface PerpetualL2Transaction { thirdPartyId: number transactionId: number transaction: PerpetualL2TransactionData + timestamp: Timestamp } export function toPerpetualL2Transactions( @@ -34,6 +35,7 @@ export function toPerpetualL2Transactions( thirdPartyId: tx.apex_id, transactionId: tx.tx_info.tx_id, transaction: toPerpetualL2TransactionData(tx.tx_info.tx), + timestamp: Timestamp.fromSeconds(tx.time_created), } }) } diff --git a/packages/frontend/src/preview/data/l2Transactions.ts b/packages/frontend/src/preview/data/l2Transactions.ts index e73768584..5d292ed08 100644 --- a/packages/frontend/src/preview/data/l2Transactions.ts +++ b/packages/frontend/src/preview/data/l2Transactions.ts @@ -129,6 +129,7 @@ export function randomPerpetualL2TransactionEntry( return { transactionId: randomInt(0, 100000), stateUpdateId: randomInt(0, 10) > 7 ? undefined : randomInt(0, 100000), + timestamp: randomTimestamp(), data: data ? data : perpetualL2TransactionsBucket.pick(), state: randomInt(0, 10) > 8 @@ -144,6 +145,7 @@ export function randomPerpetualUserL2TransactionEntry( return { transactionId: randomInt(0, 100000), stateUpdateId: randomInt(0, 10) > 7 ? undefined : randomInt(0, 100000), + timestamp: randomTimestamp(), data: data ? data : perpetualUserL2TransactionsBucket.pick(), state: randomInt(0, 10) > 8 @@ -159,7 +161,10 @@ export function randomAggregatedPerpetualL2TransactionEntry( return { transactionId: randomInt(0, 100000), stateUpdateId: randomInt(0, 10) > 7 ? undefined : randomInt(0, 100000), - originalTransaction: data ? data : perpetualL2TransactionsBucket.pick(), + originalTransaction: { + ...(data ? data : perpetualL2TransactionsBucket.pick()), + timestamp: randomTimestamp(), + }, alternativeTransactions: [], } } diff --git a/packages/frontend/src/preview/routes.ts b/packages/frontend/src/preview/routes.ts index 3bee655b5..7217677b9 100644 --- a/packages/frontend/src/preview/routes.ts +++ b/packages/frontend/src/preview/routes.ts @@ -692,9 +692,10 @@ const routes: Route[] = [ context, transaction: { ...randomAggregatedPerpetualL2TransactionEntry(), - alternativeTransactions: repeat(randomInt(1, 10), () => - perpetualL2TransactionsBucket.pick() - ), + alternativeTransactions: repeat(randomInt(1, 10), () => ({ + timestamp: randomTimestamp(), + ...perpetualL2TransactionsBucket.pick(), + })), }, }) }, @@ -923,9 +924,10 @@ const routes: Route[] = [ ...randomAggregatedPerpetualL2TransactionEntry( randomPerpetualL2MultiTransaction() ), - alternativeTransactions: repeat(randomInt(1, 10), () => - perpetualL2TransactionsBucket.pick() - ), + alternativeTransactions: repeat(randomInt(1, 10), () => ({ + ...perpetualL2TransactionsBucket.pick(), + timestamp: randomTimestamp(), + })), }, }) }, diff --git a/packages/frontend/src/view/components/tables/l2-transactions/perpetual/PerpetualL2TransactionsTable.tsx b/packages/frontend/src/view/components/tables/l2-transactions/perpetual/PerpetualL2TransactionsTable.tsx index 1ff111450..fe0f04fc1 100644 --- a/packages/frontend/src/view/components/tables/l2-transactions/perpetual/PerpetualL2TransactionsTable.tsx +++ b/packages/frontend/src/view/components/tables/l2-transactions/perpetual/PerpetualL2TransactionsTable.tsx @@ -13,6 +13,7 @@ import { Link } from '../../../Link' import { StatusBadge } from '../../../StatusBadge' import { Table } from '../../../table/Table' import { Column } from '../../../table/types' +import { TimeCell } from '../../../TimeCell' import { TooltipWrapper } from '../../../Tooltip' import { PerpetualL2TransactionFreeForm } from './PerpetualL2TransactionFreeForm' @@ -25,6 +26,7 @@ export function PerpetualL2TransactionsTable( props: PerpetualL2TransactionsTableProps ) { const columns: Column[] = [ + { header: 'Time' }, { header: 'Type' }, { header: `Transaction id` }, { header: 'Status' }, @@ -38,6 +40,11 @@ export function PerpetualL2TransactionsTable( transaction.stateUpdateId ) const cells: ReactNode[] = [ + transaction.timestamp ? ( + + ) : ( + '-' + ), 0 - return ( + timestamp: Timestamp | undefined collateralAsset: CollateralAsset } @@ -21,6 +23,7 @@ export interface PerpetualL2TransactionEntry< transactionId: number data: Extract stateUpdateId: number | undefined + timestamp: Timestamp | undefined isPartOfMulti: boolean state?: 'alternative' | 'replaced' } @@ -28,8 +31,12 @@ export interface PerpetualL2TransactionEntry< export interface AggregatedPerpetualL2TransactionEntry { transactionId: number stateUpdateId: number | undefined - originalTransaction: PerpetualL2TransactionData - alternativeTransactions: PerpetualL2TransactionData[] + originalTransaction: PerpetualL2TransactionData & { + timestamp: Timestamp | undefined + } + alternativeTransactions: (PerpetualL2TransactionData & { + timestamp: Timestamp | undefined + })[] } export function l2TransactionTypeToText( diff --git a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualConditionalTransferDetails.tsx b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualConditionalTransferDetails.tsx index b075f4a13..46bb6cbc6 100644 --- a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualConditionalTransferDetails.tsx +++ b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualConditionalTransferDetails.tsx @@ -68,6 +68,9 @@ export function PerpetualConditionalTransferDetails( {props.data.nonce.toString()} + + {props.timestamp ? formatTimestamp(props.timestamp) : '-'} + ) } diff --git a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualDeleverageDetails.tsx b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualDeleverageDetails.tsx index 02bcfd7aa..2dd0d774c 100644 --- a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualDeleverageDetails.tsx +++ b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualDeleverageDetails.tsx @@ -1,5 +1,6 @@ import React from 'react' +import { formatTimestamp } from '../../../../../utils/formatting/formatTimestamp' import { TransactionField, TransactionYesOrNoField, @@ -39,6 +40,9 @@ export function PerpetualDeleverageDetails( amount: props.data.collateralAmount, }} /> + + {props.timestamp ? formatTimestamp(props.timestamp) : '-'} + ) } diff --git a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualDepositDetails.tsx b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualDepositDetails.tsx index c6e4a859a..208cfc043 100644 --- a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualDepositDetails.tsx +++ b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualDepositDetails.tsx @@ -1,5 +1,6 @@ import React from 'react' +import { formatTimestamp } from '../../../../../utils/formatting/formatTimestamp' import { AssetAmountCard } from '../../../../components/AssetAmountCard' import { Link } from '../../../../components/Link' import { TransactionField } from '../../../transaction/components/TransactionField' @@ -29,6 +30,9 @@ export function PerpetualDepositDetails( asset={{ hashOrId: props.collateralAsset.assetId }} amount={props.data.amount} /> + + {props.timestamp ? formatTimestamp(props.timestamp) : '-'} + ) } diff --git a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualForcedTradeDetails.tsx b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualForcedTradeDetails.tsx index 54f4f0041..ed14809b7 100644 --- a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualForcedTradeDetails.tsx +++ b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualForcedTradeDetails.tsx @@ -1,6 +1,7 @@ import { getCollateralAssetIdFromHash } from '@explorer/shared' import React from 'react' +import { formatTimestamp } from '../../../../../utils/formatting/formatTimestamp' import { InlineEllipsis } from '../../../../components/InlineEllipsis' import { Link } from '../../../../components/Link' import { TransactionField } from '../../../transaction/components/TransactionField' @@ -71,6 +72,9 @@ export function PerpetualForcedTradeDetails( amount: props.data.collateralAmount, }} /> + + {props.timestamp ? formatTimestamp(props.timestamp) : '-'} + ) } diff --git a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualForcedWithdrawalDetails.tsx b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualForcedWithdrawalDetails.tsx index f39369116..c3a0f36cb 100644 --- a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualForcedWithdrawalDetails.tsx +++ b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualForcedWithdrawalDetails.tsx @@ -1,5 +1,6 @@ import React from 'react' +import { formatTimestamp } from '../../../../../utils/formatting/formatTimestamp' import { AssetAmountCard } from '../../../../components/AssetAmountCard' import { Link } from '../../../../components/Link' import { TransactionField } from '../../../transaction/components/TransactionField' @@ -28,6 +29,9 @@ export function PerpetualForcedWithdrawalDetails( asset={{ hashOrId: props.collateralAsset.assetId }} amount={props.data.amount} /> + + {props.timestamp ? formatTimestamp(props.timestamp) : '-'} + ) } diff --git a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualFundingTickDetails.tsx b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualFundingTickDetails.tsx index 348d2811b..da15be53d 100644 --- a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualFundingTickDetails.tsx +++ b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualFundingTickDetails.tsx @@ -32,6 +32,9 @@ export function PerpetualFundingTickDetails( })} + + {props.timestamp ? formatTimestamp(props.timestamp) : '-'} + ) } diff --git a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualLiquidateDetails.tsx b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualLiquidateDetails.tsx index 932b09f94..47f8a1606 100644 --- a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualLiquidateDetails.tsx +++ b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualLiquidateDetails.tsx @@ -1,5 +1,6 @@ import React from 'react' +import { formatTimestamp } from '../../../../../utils/formatting/formatTimestamp' import { TransactionField, TransactionYesOrNoField, @@ -42,6 +43,9 @@ export function PerpetualLiquidateDetails( amount: props.data.actualCollateral, }} /> + + {props.timestamp ? formatTimestamp(props.timestamp) : '-'} + ) } diff --git a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualMultiTransactionDetails.tsx b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualMultiTransactionDetails.tsx index b09765b39..bd55fa2fc 100644 --- a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualMultiTransactionDetails.tsx +++ b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualMultiTransactionDetails.tsx @@ -1,4 +1,5 @@ import { CollateralAsset, PerpetualL2TransactionData } from '@explorer/shared' +import { Timestamp } from '@explorer/types' import React from 'react' import { Card } from '../../../../components/Card' @@ -10,6 +11,7 @@ export interface PerpetualMultiTransactionDetailsProps { transactionId: number stateUpdateId: number | undefined data: Extract + timestamp?: Timestamp | undefined collateralAsset: CollateralAsset altIndex: number | undefined } diff --git a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualOraclePricesTickDetails.tsx b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualOraclePricesTickDetails.tsx index d469c94b3..1431c0624 100644 --- a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualOraclePricesTickDetails.tsx +++ b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualOraclePricesTickDetails.tsx @@ -31,6 +31,9 @@ export function PerpetualOraclePricesTickDetails( })} + + {props.timestamp ? formatTimestamp(props.timestamp) : '-'} + ) } diff --git a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualTradeDetails.tsx b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualTradeDetails.tsx index 3182fe69e..11f52b050 100644 --- a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualTradeDetails.tsx +++ b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualTradeDetails.tsx @@ -1,6 +1,7 @@ import { getCollateralAssetIdFromHash } from '@explorer/shared' import React from 'react' +import { formatTimestamp } from '../../../../../utils/formatting/formatTimestamp' import { InlineEllipsis } from '../../../../components/InlineEllipsis' import { Link } from '../../../../components/Link' import { TransactionField } from '../../../transaction/components/TransactionField' @@ -67,6 +68,9 @@ export function PerpetualTradeDetails( amount: props.data.actualCollateral, }} /> + + {props.timestamp ? formatTimestamp(props.timestamp) : '-'} + ) } diff --git a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualTransferDetails.tsx b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualTransferDetails.tsx index b314849b5..6cc3964dc 100644 --- a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualTransferDetails.tsx +++ b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualTransferDetails.tsx @@ -52,6 +52,9 @@ export function PerpetualTransferDetails( {props.data.nonce.toString()} + + {props.timestamp ? formatTimestamp(props.timestamp) : '-'} + ) } diff --git a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualWithdrawalToAddressDetails.tsx b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualWithdrawalToAddressDetails.tsx index 69a06800e..ed8075c13 100644 --- a/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualWithdrawalToAddressDetails.tsx +++ b/packages/frontend/src/view/pages/l2-transaction/components/details/PerpetualWithdrawalToAddressDetails.tsx @@ -47,6 +47,9 @@ export function PerpetualWithdrawalToAddressDetails( {props.data.nonce.toString()} + + {props.timestamp ? formatTimestamp(props.timestamp) : '-'} + ) } diff --git a/packages/frontend/src/view/pages/l2-transaction/components/details/index.tsx b/packages/frontend/src/view/pages/l2-transaction/components/details/index.tsx index 2c7cd14d6..2a32b611a 100644 --- a/packages/frontend/src/view/pages/l2-transaction/components/details/index.tsx +++ b/packages/frontend/src/view/pages/l2-transaction/components/details/index.tsx @@ -3,6 +3,7 @@ import { CollateralAsset, PerpetualL2TransactionData, } from '@explorer/shared' +import { Timestamp } from '@explorer/types' import React from 'react' import { PerpetualConditionalTransferDetails } from './PerpetualConditionalTransferDetails' @@ -20,7 +21,7 @@ import { PerpetualWithdrawalToAddressDetails } from './PerpetualWithdrawalToAddr interface PerpetualTransactionDetailsProps { stateUpdateId: number | undefined - data: PerpetualL2TransactionData + data: PerpetualL2TransactionData & { timestamp: Timestamp | undefined } collateralAsset: CollateralAsset transactionId: number altIndex: number | undefined @@ -36,6 +37,7 @@ export function PerpetualTransactionDetails( @@ -45,6 +47,7 @@ export function PerpetualTransactionDetails( @@ -74,6 +79,7 @@ export function PerpetualTransactionDetails( @@ -83,6 +89,7 @@ export function PerpetualTransactionDetails( @@ -92,6 +99,7 @@ export function PerpetualTransactionDetails( @@ -101,6 +109,7 @@ export function PerpetualTransactionDetails( @@ -110,6 +119,7 @@ export function PerpetualTransactionDetails( @@ -119,6 +129,7 @@ export function PerpetualTransactionDetails( @@ -128,6 +139,7 @@ export function PerpetualTransactionDetails( @@ -138,6 +150,7 @@ export function PerpetualTransactionDetails( data={props.data} collateralAsset={props.collateralAsset} stateUpdateId={props.stateUpdateId} + timestamp={props.data.timestamp} transactionId={props.transactionId} altIndex={props.altIndex} /> From 95c2741539eb5a5f2685220e83260395bcbac4a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20T=C3=B3rz?= Date: Tue, 8 Aug 2023 13:38:11 +0200 Subject: [PATCH 2/2] remove console log --- .../backend/src/peripherals/database/L2TransactionRepository.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/src/peripherals/database/L2TransactionRepository.ts b/packages/backend/src/peripherals/database/L2TransactionRepository.ts index 314693649..c6f264e27 100644 --- a/packages/backend/src/peripherals/database/L2TransactionRepository.ts +++ b/packages/backend/src/peripherals/database/L2TransactionRepository.ts @@ -123,7 +123,6 @@ export class L2TransactionRepository extends BaseRepository { if (record.state !== 'alternative') { if (existing) { - console.log(record, existing, record.state) throw new Error( 'L2 Transaction already exists when adding from Feeder Gatway' )