diff --git a/package.json b/package.json index 9c1931ae..911a7b95 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hathor-wallet-service", - "version": "1.6.4", + "version": "1.7.0", "workspaces": [ "packages/common", "packages/daemon", diff --git a/packages/daemon/__tests__/guards/guards.test.ts b/packages/daemon/__tests__/guards/guards.test.ts index 2726888c..77f61376 100644 --- a/packages/daemon/__tests__/guards/guards.test.ts +++ b/packages/daemon/__tests__/guards/guards.test.ts @@ -1,4 +1,4 @@ -import { Context, Event, FullNodeEventTypes } from '../../src/types'; +import { Context, Event, FullNodeEventTypes, FullNodeEvent, StandardFullNodeEvent } from '../../src/types'; import { metadataIgnore, metadataVoided, @@ -12,6 +12,7 @@ import { voided, unchanged, invalidNetwork, + reorgStarted, } from '../../src/guards'; import { EventTypes } from '../../src/types'; @@ -45,7 +46,7 @@ const mockContext: Context = { txCache: TxCache, }; -const generateFullNodeEvent = (type: FullNodeEventTypes, data = {} as any): Event => ({ +const generateStandardFullNodeEvent = (type: Exclude, data = {} as any): Event => ({ type: EventTypes.FULLNODE_EVENT, event: { type: 'EVENT', @@ -62,15 +63,79 @@ const generateFullNodeEvent = (type: FullNodeEventTypes, data = {} as any): Even }, }); -const generateMetadataDecidedEvent = (type: string): Event => ({ - type: EventTypes.METADATA_DECIDED, +const generateReorgStartedEvent = (data = { + reorg_size: 1, + previous_best_block: 'prev', + new_best_block: 'new', + common_block: 'common', +}): Event => ({ + type: EventTypes.FULLNODE_EVENT, event: { - type, - // @ts-ignore - originalEvent: {} as any, + type: 'EVENT', + network: 'mainnet', + peer_id: '', + stream_id: '', + event: { + id: 0, + timestamp: 0, + type: FullNodeEventTypes.REORG_STARTED, + data, + group_id: 1, + }, + latest_event_id: 0, }, }); +const generateFullNodeEvent = (type: FullNodeEventTypes, data = {} as any): Event => { + if (type === FullNodeEventTypes.REORG_STARTED) { + return generateReorgStartedEvent(data); + } + return generateStandardFullNodeEvent(type, data); +}; + +const generateMetadataDecidedEvent = (type: 'TX_VOIDED' | 'TX_UNVOIDED' | 'TX_NEW' | 'TX_FIRST_BLOCK' | 'IGNORE'): Event => { + const fullNodeEvent: StandardFullNodeEvent = { + stream_id: '', + peer_id: '', + network: 'mainnet', + type: 'EVENT', + latest_event_id: 0, + event: { + id: 0, + timestamp: 0, + type: FullNodeEventTypes.VERTEX_METADATA_CHANGED, + data: { + hash: 'hash', + timestamp: 0, + version: 1, + weight: 1, + nonce: 1, + inputs: [], + outputs: [], + parents: [], + tokens: [], + token_name: null, + token_symbol: null, + signal_bits: 1, + metadata: { + hash: 'hash', + voided_by: [], + first_block: null, + height: 1, + }, + }, + }, + }; + + return { + type: EventTypes.METADATA_DECIDED, + event: { + type, + originalEvent: fullNodeEvent, + }, + }; +}; + describe('metadata decided tests', () => { test('metadataIgnore', async () => { expect(metadataIgnore(mockContext, generateMetadataDecidedEvent('IGNORE'))).toBe(true); @@ -171,6 +236,14 @@ describe('fullnode event guards', () => { // Any event other than FULLNODE_EVENT should return false expect(() => unchanged(mockContext, generateMetadataDecidedEvent('TX_NEW'))).toThrow('Invalid event type on unchanged guard: METADATA_DECIDED'); }); + + test('reorgStarted', () => { + expect(reorgStarted(mockContext, generateFullNodeEvent(FullNodeEventTypes.REORG_STARTED))).toBe(true); + expect(reorgStarted(mockContext, generateFullNodeEvent(FullNodeEventTypes.VERTEX_METADATA_CHANGED))).toBe(false); + + // Any event other than FULLNODE_EVENT should throw + expect(() => reorgStarted(mockContext, generateMetadataDecidedEvent('TX_NEW'))).toThrow('Invalid event type on reorgStarted guard: METADATA_DECIDED'); + }); }); describe('fullnode validation guards', () => { diff --git a/packages/daemon/__tests__/machines/SyncMachine.test.ts b/packages/daemon/__tests__/machines/SyncMachine.test.ts index 288ba5e3..abb203d6 100644 --- a/packages/daemon/__tests__/machines/SyncMachine.test.ts +++ b/packages/daemon/__tests__/machines/SyncMachine.test.ts @@ -519,7 +519,7 @@ describe('Event handling', () => { expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.handlingUnhandledEvent}`)).toBeTruthy(); }); - it('should ignore REORG_STARTED event but still send ack', () => { + it('should handle REORG_STARTED event', () => { const MockedFetchMachine = SyncMachine.withConfig({ guards: { invalidPeerId: () => false, @@ -535,6 +535,6 @@ describe('Event handling', () => { event: REORG_STARTED as unknown as FullNodeEvent, }); - expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.handlingUnhandledEvent}`)).toBeTruthy(); + expect(currentState.matches(`${SYNC_MACHINE_STATES.CONNECTED}.${CONNECTED_STATES.handlingReorgStarted}`)).toBeTruthy(); }); }); diff --git a/packages/daemon/__tests__/services/services.test.ts b/packages/daemon/__tests__/services/services.test.ts index a57ea954..1390f79e 100644 --- a/packages/daemon/__tests__/services/services.test.ts +++ b/packages/daemon/__tests__/services/services.test.ts @@ -29,6 +29,7 @@ import { handleVoidedTx, handleVertexAccepted, metadataDiff, + handleReorgStarted, } from '../../src/services'; import logger from '../../src/logger'; import { @@ -42,13 +43,10 @@ import { generateAddresses, } from '../../src/utils'; import getConfig from '../../src/config'; - -jest.mock('../../src/config', () => { - return { - __esModule: true, // This property is needed for mocking a default export - default: jest.fn(() => ({})), - }; -}); +import { addAlert, Severity } from '@wallet-service/common'; +import { FullNodeEventTypes } from '../../src/types'; +import { Context } from '../../src/types'; +import { generateFullNodeEvent } from '../utils'; jest.mock('@hathor/wallet-lib'); jest.mock('../../src/logger', () => ({ @@ -109,6 +107,41 @@ jest.mock('../../src/utils', () => ({ generateAddresses: jest.fn(), })); +jest.mock('@wallet-service/common', () => { + const addAlertMock = jest.fn(); + return { + addAlert: addAlertMock, + Severity: { + INFO: 'INFO', + MINOR: 'MINOR', + MAJOR: 'MAJOR', + CRITICAL: 'CRITICAL', + }, + NftUtils: { + shouldInvokeNftHandlerForTx: jest.fn().mockReturnValue(false), + invokeNftHandlerLambda: jest.fn(), + }, + }; +}); + +jest.mock('../../src/config', () => { + return { + __esModule: true, // This property is needed for mocking a default export + default: jest.fn(() => ({ + REORG_SIZE_INFO: 1, + REORG_SIZE_MINOR: 3, + REORG_SIZE_MAJOR: 5, + REORG_SIZE_CRITICAL: 10, + })), + getConfig: jest.fn(() => ({ + REORG_SIZE_INFO: 1, + REORG_SIZE_MINOR: 3, + REORG_SIZE_MAJOR: 5, + REORG_SIZE_CRITICAL: 10, + })), + }; +}); + beforeEach(() => { jest.clearAllMocks(); }); @@ -838,3 +871,165 @@ describe('metadataDiff', () => { expect(result.type).toBe('TX_UNVOIDED'); }); }); + +describe('handleReorgStarted', () => { + beforeEach(() => { + (getConfig as jest.Mock).mockReturnValue({ + REORG_SIZE_INFO: 1, + REORG_SIZE_MINOR: 3, + REORG_SIZE_MAJOR: 5, + REORG_SIZE_CRITICAL: 10, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should add INFO alert when reorg size equals REORG_SIZE_INFO', async () => { + const event = generateFullNodeEvent({ + type: FullNodeEventTypes.REORG_STARTED, + data: { + reorg_size: 1, + previous_best_block: 'prev', + new_best_block: 'new', + common_block: 'common', + }, + }); + + // @ts-ignore + await handleReorgStarted({ event } as Context); + + expect(addAlert).toHaveBeenCalledWith( + 'Reorg Detected', + 'A reorg of size 1 has occurred.', + Severity.INFO, + { + reorg_size: 1, + previous_best_block: 'prev', + new_best_block: 'new', + common_block: 'common', + }, + expect.anything(), + ); + }); + + it('should add MINOR alert when reorg size is between REORG_SIZE_MINOR and REORG_SIZE_MAJOR', async () => { + const event = generateFullNodeEvent({ + type: FullNodeEventTypes.REORG_STARTED, + data: { + reorg_size: 3, + previous_best_block: 'prev', + new_best_block: 'new', + common_block: 'common', + }, + }); + + // @ts-ignore + await handleReorgStarted({ event } as Context); + + expect(addAlert).toHaveBeenCalledWith( + 'Minor Reorg Detected', + 'A minor reorg of size 3 has occurred.', + Severity.MINOR, + { + reorg_size: 3, + previous_best_block: 'prev', + new_best_block: 'new', + common_block: 'common', + }, + expect.anything(), + ); + }); + + it('should add MAJOR alert when reorg size is between REORG_SIZE_MAJOR and REORG_SIZE_CRITICAL', async () => { + const event = generateFullNodeEvent({ + type: FullNodeEventTypes.REORG_STARTED, + data: { + reorg_size: 7, + previous_best_block: 'prev', + new_best_block: 'new', + common_block: 'common', + }, + }); + + // @ts-ignore + await handleReorgStarted({ event } as Context); + + expect(addAlert).toHaveBeenCalledWith( + 'Major Reorg Detected', + 'A major reorg of size 7 has occurred.', + Severity.MAJOR, + { + reorg_size: 7, + previous_best_block: 'prev', + new_best_block: 'new', + common_block: 'common', + }, + expect.anything(), + ); + }); + + it('should add CRITICAL alert when reorg size is greater than REORG_SIZE_CRITICAL', async () => { + const event = generateFullNodeEvent({ + type: FullNodeEventTypes.REORG_STARTED, + data: { + reorg_size: 11, + previous_best_block: 'prev', + new_best_block: 'new', + common_block: 'common', + }, + }); + + // @ts-ignore + await handleReorgStarted({ event } as Context); + + expect(addAlert).toHaveBeenCalledWith( + 'Critical Reorg Detected', + 'A critical reorg of size 11 has occurred.', + Severity.CRITICAL, + { + reorg_size: 11, + previous_best_block: 'prev', + new_best_block: 'new', + common_block: 'common', + }, + expect.anything(), + ); + }); + + it('should not add alert when reorg size is less than REORG_SIZE_INFO', async () => { + const event = generateFullNodeEvent({ + type: FullNodeEventTypes.REORG_STARTED, + data: { + reorg_size: 0, + previous_best_block: 'prev', + new_best_block: 'new', + common_block: 'common', + }, + }); + + // @ts-ignore + await handleReorgStarted({ event } as Context); + + expect(addAlert).not.toHaveBeenCalled(); + }); + + it('should throw error when event is missing', async () => { + await expect(handleReorgStarted({} as Context)) + .rejects + .toThrow('No event in context'); + }); + + it('should throw error when event type is incorrect', async () => { + const event = generateFullNodeEvent({ + type: FullNodeEventTypes.VERTEX_METADATA_CHANGED, + data: {}, + }); + + // @ts-ignore + await expect(handleReorgStarted({ event } as Context)) + .rejects + .toThrow('Invalid event type for REORG_STARTED'); + }); +}); diff --git a/packages/daemon/__tests__/utils.ts b/packages/daemon/__tests__/utils.ts index a5a0f7ab..e70ed8ca 100644 --- a/packages/daemon/__tests__/utils.ts +++ b/packages/daemon/__tests__/utils.ts @@ -27,6 +27,7 @@ import { TransactionTableEntry } from './types'; import { isEqual } from 'lodash'; +import { EventTypes } from '../src/types'; export const XPUBKEY = 'xpub6CsZPtBWMkwxVxyBTKT8AWZcYqzwZ5K2qMkqjFpibMbBZ72JAvLMz7LquJNs4svfTiNYy6GbLo8gqECWsC6hTRt7imnphUFNEMz6VuRSjww'; export const ADDRESSES = [ @@ -775,3 +776,16 @@ export const addToAddressTxHistoryTable = async ( VALUES ?`, [payload]); }; + +export const generateFullNodeEvent = (event: any) => ({ + type: EventTypes.FULLNODE_EVENT, + event: { + peer_id: 'peer123', + stream_id: 'stream456', + network: 'mainnet', + id: 123, + timestamp: Date.now(), + type: event.type, + data: event.data, + }, +}); diff --git a/packages/daemon/src/actions/index.ts b/packages/daemon/src/actions/index.ts index a46b9170..91d6f4bd 100644 --- a/packages/daemon/src/actions/index.ts +++ b/packages/daemon/src/actions/index.ts @@ -6,7 +6,7 @@ */ import { assign, AssignAction, raise, sendTo } from 'xstate'; -import { Context, Event, EventTypes } from '../types'; +import { Context, Event, EventTypes, StandardFullNodeEvent } from '../types'; import { get } from 'lodash'; import logger from '../logger'; import { hashTxData } from '../utils'; @@ -164,7 +164,7 @@ export const metadataDecided = raise((_context: Context, event: Event) => ({ * Updates the cache with the last processed event (from the context) */ export const updateCache = (context: Context) => { - const fullNodeEvent = context.event; + const fullNodeEvent = context.event as StandardFullNodeEvent; if (!fullNodeEvent) { return; } diff --git a/packages/daemon/src/config.ts b/packages/daemon/src/config.ts index 1abac86c..4f6e7814 100644 --- a/packages/daemon/src/config.ts +++ b/packages/daemon/src/config.ts @@ -86,6 +86,12 @@ export const HEALTHCHECK_PING_INTERVAL = parseInt(process.env.HEALTHCHECK_PING_I // Other export const USE_SSL = process.env.USE_SSL; +// Reorg size thresholds for different alert levels +export const REORG_SIZE_INFO = parseInt(process.env.REORG_SIZE_INFO ?? '1', 10); +export const REORG_SIZE_MINOR = parseInt(process.env.REORG_SIZE_MINOR ?? '3', 10); +export const REORG_SIZE_MAJOR = parseInt(process.env.REORG_SIZE_MAJOR ?? '5', 10); +export const REORG_SIZE_CRITICAL = parseInt(process.env.REORG_SIZE_CRITICAL ?? '10', 10); + export default () => ({ SERVICE_NAME, CONSOLE_LEVEL, @@ -115,4 +121,8 @@ export default () => ({ HEALTHCHECK_SERVER_URL, HEALTHCHECK_SERVER_API_KEY, HEALTHCHECK_PING_INTERVAL, + REORG_SIZE_INFO, + REORG_SIZE_MINOR, + REORG_SIZE_MAJOR, + REORG_SIZE_CRITICAL, }); diff --git a/packages/daemon/src/guards/index.ts b/packages/daemon/src/guards/index.ts index 4a062741..e6162c4a 100644 --- a/packages/daemon/src/guards/index.ts +++ b/packages/daemon/src/guards/index.ts @@ -237,3 +237,14 @@ export const unchanged = (context: Context, event: Event) => { return txHashFromCache === txHashFromEvent; }; + +/* + * This guard is used to detect if the event is a REORG_STARTED event + */ +export const reorgStarted = (_context: Context, event: Event) => { + if (event.type !== EventTypes.FULLNODE_EVENT) { + throw new Error(`Invalid event type on reorgStarted guard: ${event.type}`); + } + + return event.event.event.type === FullNodeEventTypes.REORG_STARTED; +}; diff --git a/packages/daemon/src/machines/SyncMachine.ts b/packages/daemon/src/machines/SyncMachine.ts index 1a125d11..668a002a 100644 --- a/packages/daemon/src/machines/SyncMachine.ts +++ b/packages/daemon/src/machines/SyncMachine.ts @@ -25,6 +25,7 @@ import { updateLastSyncedEvent, fetchInitialState, handleUnvoidedTx, + handleReorgStarted, } from '../services'; import { metadataIgnore, @@ -41,6 +42,7 @@ import { voided, unchanged, vertexRemoved, + reorgStarted, } from '../guards'; import { storeInitialState, @@ -76,11 +78,12 @@ export const CONNECTED_STATES = { handlingVoidedTx: 'handlingVoidedTx', handlingUnvoidedTx: 'handlingUnvoidedTx', handlingFirstBlock: 'handlingFirstBlock', + handlingReorgStarted: 'handlingReorgStarted', }; const { TX_CACHE_SIZE } = getConfig(); -const SyncMachine = Machine({ +export const SyncMachine = Machine({ id: 'SyncMachine', initial: SYNC_MACHINE_STATES.INITIALIZING, context: { @@ -166,6 +169,10 @@ const SyncMachine = Machine({ actions: ['storeEvent'], cond: 'vertexAccepted', target: CONNECTED_STATES.handlingVertexAccepted, + }, { + actions: ['storeEvent'], + cond: 'reorgStarted', + target: CONNECTED_STATES.handlingReorgStarted, }, { actions: ['storeEvent'], target: CONNECTED_STATES.handlingUnhandledEvent, @@ -268,6 +275,18 @@ const SyncMachine = Machine({ onError: `#${SYNC_MACHINE_STATES.ERROR}`, }, }, + [CONNECTED_STATES.handlingReorgStarted]: { + id: CONNECTED_STATES.handlingReorgStarted, + invoke: { + src: 'handleReorgStarted', + data: (_context: Context, event: Event) => event, + onDone: { + target: 'idle', + actions: ['sendAck', 'storeEvent'], + }, + onError: `#${SYNC_MACHINE_STATES.ERROR}`, + }, + }, }, on: { WEBSOCKET_EVENT: [{ @@ -283,10 +302,18 @@ const SyncMachine = Machine({ }, }, }, { + services: { + handleVertexAccepted, + handleVertexRemoved, + handleVoidedTx, + handleTxFirstBlock, + handleUnvoidedTx, + handleReorgStarted, + fetchInitialState, + metadataDiff, + updateLastSyncedEvent, + }, guards: { - invalidStreamId, - invalidPeerId, - invalidNetwork, metadataIgnore, metadataVoided, metadataUnvoided, @@ -294,10 +321,14 @@ const SyncMachine = Machine({ metadataFirstBlock, metadataChanged, vertexAccepted, + invalidPeerId, + invalidStreamId, + invalidNetwork, websocketDisconnected, voided, unchanged, vertexRemoved, + reorgStarted, }, delays: { BACKOFF_DELAYED_RECONNECT }, actions: { @@ -314,16 +345,6 @@ const SyncMachine = Machine({ startHealthcheckPing, stopHealthcheckPing, }, - services: { - handleVoidedTx, - handleUnvoidedTx, - handleVertexAccepted, - handleVertexRemoved, - handleTxFirstBlock, - metadataDiff, - updateLastSyncedEvent, - fetchInitialState, - }, }); export default SyncMachine; diff --git a/packages/daemon/src/services/index.ts b/packages/daemon/src/services/index.ts index 2a00280a..89f11e61 100644 --- a/packages/daemon/src/services/index.ts +++ b/packages/daemon/src/services/index.ts @@ -19,10 +19,11 @@ import { LastSyncedEvent, Event, Context, - FullNodeEvent, EventTxInput, EventTxOutput, WalletStatus, + FullNodeEventTypes, + StandardFullNodeEvent, } from '../types'; import { TxInput, @@ -73,6 +74,7 @@ import { import getConfig from '../config'; import logger from '../logger'; import { invokeOnTxPushNotificationRequestedLambda } from '../utils'; +import { addAlert, Severity } from '@wallet-service/common'; export const METADATA_DIFF_EVENT_TYPES = { IGNORE: 'IGNORE', @@ -86,7 +88,7 @@ export const metadataDiff = async (_context: Context, event: Event) => { const mysql = await getDbConnection(); try { - const fullNodeEvent = event.event as FullNodeEvent; + const fullNodeEvent = event.event as StandardFullNodeEvent; const { hash, metadata: { voided_by, first_block }, @@ -173,7 +175,7 @@ export const handleVertexAccepted = async (context: Context, _event: Event) => { } = getConfig(); try { - const fullNodeEvent = context.event as FullNodeEvent; + const fullNodeEvent = context.event as StandardFullNodeEvent; const now = getUnixTimestamp(); const blockRewardLock = context.rewardMinBlocks; @@ -437,7 +439,7 @@ export const handleVertexRemoved = async (context: Context, _event: Event) => { await mysql.beginTransaction(); try { - const fullNodeEvent = context.event as FullNodeEvent; + const fullNodeEvent = context.event as StandardFullNodeEvent; const { hash, @@ -512,7 +514,7 @@ export const handleVoidedTx = async (context: Context) => { await mysql.beginTransaction(); try { - const fullNodeEvent = context.event as FullNodeEvent; + const fullNodeEvent = context.event as StandardFullNodeEvent; const { hash, @@ -548,7 +550,7 @@ export const handleUnvoidedTx = async (context: Context) => { await mysql.beginTransaction(); try { - const fullNodeEvent = context.event as FullNodeEvent; + const fullNodeEvent = context.event as StandardFullNodeEvent; const { hash } = fullNodeEvent.event.data; @@ -574,7 +576,7 @@ export const handleTxFirstBlock = async (context: Context) => { await mysql.beginTransaction(); try { - const fullNodeEvent = context.event as FullNodeEvent; + const fullNodeEvent = context.event as StandardFullNodeEvent; const { hash, @@ -658,3 +660,58 @@ export const fetchInitialState = async () => { rewardMinBlocks, }; }; + +export const handleReorgStarted = async (context: Context): Promise => { + if (!context.event) { + throw new Error('No event in context'); + } + + const fullNodeEvent = context.event; + if (fullNodeEvent.event.type !== FullNodeEventTypes.REORG_STARTED) { + throw new Error('Invalid event type for REORG_STARTED'); + } + + const { reorg_size, previous_best_block, new_best_block, common_block } = fullNodeEvent.event.data; + const { REORG_SIZE_INFO, REORG_SIZE_MINOR, REORG_SIZE_MAJOR, REORG_SIZE_CRITICAL } = getConfig(); + + const metadata = { + reorg_size, + previous_best_block, + new_best_block, + common_block, + }; + + if (reorg_size >= REORG_SIZE_CRITICAL) { + await addAlert( + 'Critical Reorg Detected', + `A critical reorg of size ${reorg_size} has occurred.`, + Severity.CRITICAL, + metadata, + logger, + ); + } else if (reorg_size >= REORG_SIZE_MAJOR) { + await addAlert( + 'Major Reorg Detected', + `A major reorg of size ${reorg_size} has occurred.`, + Severity.MAJOR, + metadata, + logger, + ); + } else if (reorg_size >= REORG_SIZE_MINOR) { + await addAlert( + 'Minor Reorg Detected', + `A minor reorg of size ${reorg_size} has occurred.`, + Severity.MINOR, + metadata, + logger, + ); + } else if (reorg_size >= REORG_SIZE_INFO) { + await addAlert( + 'Reorg Detected', + `A reorg of size ${reorg_size} has occurred.`, + Severity.INFO, + metadata, + logger, + ); + } +}; diff --git a/packages/daemon/src/types/event.ts b/packages/daemon/src/types/event.ts index 83cdc958..a4bcb2a8 100644 --- a/packages/daemon/src/types/event.ts +++ b/packages/daemon/src/types/event.ts @@ -60,16 +60,19 @@ export interface VertexRemovedEventData { vertex_id: string; } -export type FullNodeEvent = { +export type FullNodeEventBase = { stream_id: string; peer_id: string; network: string; type: string; latest_event_id: number; +}; + +export type StandardFullNodeEvent = FullNodeEventBase & { event: { id: number; timestamp: number; - type: FullNodeEventTypes; + type: Exclude; // All types except "REORG_STARTED" data: { hash: string; timestamp: number; @@ -89,9 +92,26 @@ export type FullNodeEvent = { first_block: null | string; height: number; }; - } - } -} + }; + }; +}; + +export type ReorgFullNodeEvent = FullNodeEventBase & { + event: { + id: number; + timestamp: number; + type: "REORG_STARTED"; + data: { + reorg_size: number; + previous_best_block: string; + new_best_block: string; + common_block: string; + }; + group_id: number; + }; +}; + +export type FullNodeEvent = StandardFullNodeEvent | ReorgFullNodeEvent; export interface EventTxInput { tx_id: string;