diff --git a/watcher/scripts/backfillArbitrum.ts b/watcher/scripts/backfillArbitrum.ts index 88b1642a..bc607746 100644 --- a/watcher/scripts/backfillArbitrum.ts +++ b/watcher/scripts/backfillArbitrum.ts @@ -4,8 +4,7 @@ import axios from 'axios'; import ora from 'ora'; import { initDb } from '../src/databases/utils'; import { AXIOS_CONFIG_JSON } from '../src/consts'; -import { ArbitrumWatcher } from '../src/watchers/ArbitrumWatcher'; -import { LOG_MESSAGE_PUBLISHED_TOPIC } from '../src/watchers/EVMWatcher'; +import { EVMWatcher, LOG_MESSAGE_PUBLISHED_TOPIC } from '../src/watchers/EVMWatcher'; import { Chain, contracts } from '@wormhole-foundation/sdk-base'; // This script exists because the Arbitrum RPC node only supports a 10 block range which is super slow @@ -27,7 +26,7 @@ import { Chain, contracts } from '@wormhole-foundation/sdk-base'; log.succeed(`Fetched ${blockNumbers.length} logs from Arbiscan`); // use the watcher to fetch corresponding blocks log = ora('Fetching blocks...').start(); - const watcher = new ArbitrumWatcher('Mainnet'); + const watcher = new EVMWatcher('Mainnet', 'Arbitrum', 'finalized', 'vaa'); for (const blockNumber of blockNumbers) { log.text = `Fetching block ${blockNumber}`; const { vaasByBlock } = await watcher.getMessagesForBlocks(blockNumber, blockNumber); diff --git a/watcher/scripts/locateMessageGaps.ts b/watcher/scripts/locateMessageGaps.ts index cf770b6f..c535d12a 100644 --- a/watcher/scripts/locateMessageGaps.ts +++ b/watcher/scripts/locateMessageGaps.ts @@ -8,7 +8,7 @@ import { import { TIMEOUT } from '../src/consts'; import { BigtableDatabase } from '../src/databases/BigtableDatabase'; import { parseMessageId } from '../src/databases/utils'; -import { makeFinalizedWatcher } from '../src/watchers/utils'; +import { makeFinalizedVaaWatcher } from '../src/watchers/utils'; import { Watcher } from '../src/watchers/Watcher'; import { ChainId, Network, toChain, toChainId } from '@wormhole-foundation/sdk-base'; @@ -95,7 +95,7 @@ import { ChainId, Network, toChain, toChainId } from '@wormhole-foundation/sdk-b } let watcher: Watcher; try { - watcher = makeFinalizedWatcher(network, chainName); + watcher = makeFinalizedVaaWatcher(network, chainName); } catch (e) { console.error('skipping gap for unsupported chain', chainName); continue; diff --git a/watcher/src/watchers/AlgorandWatcher.ts b/watcher/src/watchers/AlgorandWatcher.ts index ce1f1ab8..53e8e18f 100644 --- a/watcher/src/watchers/AlgorandWatcher.ts +++ b/watcher/src/watchers/AlgorandWatcher.ts @@ -18,7 +18,7 @@ export class AlgorandWatcher extends Watcher { indexerClient: algosdk.Indexer; constructor(network: Network) { - super(network, 'Algorand'); + super(network, 'Algorand', 'vaa'); if (!ALGORAND_INFO[this.network].algodServer) { throw new Error('ALGORAND_INFO.algodServer is not defined!'); diff --git a/watcher/src/watchers/AptosWatcher.ts b/watcher/src/watchers/AptosWatcher.ts index a08caf1e..5d71d775 100644 --- a/watcher/src/watchers/AptosWatcher.ts +++ b/watcher/src/watchers/AptosWatcher.ts @@ -21,7 +21,7 @@ export class AptosWatcher extends Watcher { eventHandle: string; constructor(network: Network) { - super(network, 'Aptos'); + super(network, 'Aptos', 'vaa'); this.client = new AptosClient(RPCS_BY_CHAIN[this.network][this.chain]!); this.coreBridgeAddress = contracts.coreBridge(network, 'Aptos'); this.eventHandle = `${this.coreBridgeAddress}::state::WormholeMessageHandle`; diff --git a/watcher/src/watchers/ArbitrumWatcher.ts b/watcher/src/watchers/ArbitrumWatcher.ts deleted file mode 100644 index a55ab066..00000000 --- a/watcher/src/watchers/ArbitrumWatcher.ts +++ /dev/null @@ -1,135 +0,0 @@ -import axios from 'axios'; -import { AXIOS_CONFIG_JSON, RPCS_BY_CHAIN } from '../consts'; -import { EVMWatcher } from './EVMWatcher'; -import { Network } from '@wormhole-foundation/sdk-base'; - -export class ArbitrumWatcher extends EVMWatcher { - rpc: string | undefined; - evmWatcher: EVMWatcher; - latestL2Finalized: number; - l1L2Map: Map; - lastEthTime: number; - - constructor(network: Network) { - if (network === 'Mainnet') { - super(network, 'Arbitrum'); - } else { - super(network, 'ArbitrumSepolia'); - } - - this.rpc = RPCS_BY_CHAIN[this.network][this.chain]; - if (!this.rpc) { - throw new Error(`${this.chain} RPC is not defined!`); - } - this.evmWatcher = - network === 'Mainnet' - ? new EVMWatcher(network, 'Ethereum', 'finalized') - : new EVMWatcher(network, 'Sepolia', 'finalized'); - this.latestL2Finalized = 0; - this.l1L2Map = new Map(); - this.lastEthTime = 0; - this.maximumBatchSize = 1000; - } - - async getFinalizedBlockNumber(): Promise { - if (!this.rpc) { - throw new Error(`${this.chain} RPC is not defined!`); - } - - // This gets the latest L2 block so we can get the associated L1 block number - const l1Result: BlockByNumberResult = ( - await axios.post( - this.rpc, - [ - { - jsonrpc: '2.0', - id: 1, - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - ], - AXIOS_CONFIG_JSON - ) - )?.data?.[0]?.result; - if (!l1Result || !l1Result.l1BlockNumber || !l1Result.number) { - throw new Error( - `Unable to parse result of ArbitrumWatcher::eth_getBlockByNumber for latest on ${this.rpc}` - ); - } - const associatedL1: number = parseInt(l1Result.l1BlockNumber, 16); - const l2BlkNum: number = parseInt(l1Result.number, 16); - this.logger.debug( - 'getFinalizedBlockNumber() checking map L1Block: ' + associatedL1 + ' => L2Block: ' + l2BlkNum - ); - - // Only update the map, if the L2 block number is newer - const inMapL2 = this.l1L2Map.get(associatedL1); - if (!inMapL2 || inMapL2 < l2BlkNum) { - this.logger.debug( - `Updating map with ${associatedL1} => ${l2BlkNum}, size = ${this.l1L2Map.size}` - ); - this.l1L2Map.set(associatedL1, l2BlkNum); - } - - // Only check every 30 seconds - const now = Date.now(); - if (now - this.lastEthTime < 30_000) { - return this.latestL2Finalized; - } - this.lastEthTime = now; - - // Get the latest finalized L1 block number - const evmFinal = await this.evmWatcher.getFinalizedBlockNumber(); - this.logger.debug(`Finalized EVM block number = ${evmFinal}`); - - this.logger.debug('Size of map = ' + this.l1L2Map.size); - // Walk the map looking for finalized L2 block number - for (let [l1, l2] of this.l1L2Map) { - if (l1 <= evmFinal) { - this.latestL2Finalized = l2; - this.logger.debug(`Removing key ${l1} from map`); - this.l1L2Map.delete(l1); - } - } - - this.logger.debug(`LatestL2Finalized = ${this.latestL2Finalized}`); - return this.latestL2Finalized; - } - - // This function is only used in test code. - getFirstMapEntry(): number[] { - if (this.l1L2Map.size > 0) { - for (let [l1, l2] of this.l1L2Map) { - return [l1, l2]; - } - } - return [0, 0]; - } -} - -type BlockByNumberResult = { - baseFeePerGas: string; - difficulty: string; - extraData: string; - gasLimit: string; - gasUsed: string; - hash: string; - l1BlockNumber: string; - logsBloom: string; - miner: string; - mixHash: string; - nonce: string; - number: string; - parentHash: string; - receiptsRoot: string; - sendCount: string; - sendRoot: string; - sha3Uncles: string; - size: string; - stateRoot: string; - timestamp: string; - totalDifficulty: string; - transactions: string[]; - transactionsRoot: string; - uncles: string[]; -}; diff --git a/watcher/src/watchers/BSCWatcher.ts b/watcher/src/watchers/BSCWatcher.ts deleted file mode 100644 index 61b41757..00000000 --- a/watcher/src/watchers/BSCWatcher.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Network } from '@wormhole-foundation/sdk-base'; -import { EVMWatcher } from './EVMWatcher'; - -export class BSCWatcher extends EVMWatcher { - constructor(network: Network) { - super(network, 'Bsc'); - } - async getFinalizedBlockNumber(): Promise { - const latestBlock = await super.getFinalizedBlockNumber(); - return Math.max(latestBlock - 15, 0); - } -} diff --git a/watcher/src/watchers/CosmwasmWatcher.ts b/watcher/src/watchers/CosmwasmWatcher.ts index f9b0a8c3..6266dfb1 100644 --- a/watcher/src/watchers/CosmwasmWatcher.ts +++ b/watcher/src/watchers/CosmwasmWatcher.ts @@ -16,7 +16,7 @@ export class CosmwasmWatcher extends Watcher { latestBlockHeight: number; constructor(network: Network, chain: PlatformToChains<'Cosmwasm'>) { - super(network, chain); + super(network, chain, 'vaa'); if (chain === 'Injective') { throw new Error('Please use InjectiveExplorerWatcher for injective'); } diff --git a/watcher/src/watchers/EVMWatcher.ts b/watcher/src/watchers/EVMWatcher.ts index 7075d2e9..2115ede7 100644 --- a/watcher/src/watchers/EVMWatcher.ts +++ b/watcher/src/watchers/EVMWatcher.ts @@ -7,6 +7,7 @@ import { makeBlockKey, makeVaaKey } from '../databases/utils'; import { Watcher } from './Watcher'; import { Network, PlatformToChains, contracts } from '@wormhole-foundation/sdk-base'; import { ethers_contracts } from '@wormhole-foundation/sdk-evm-core'; +import { Mode } from '@wormhole-foundation/wormhole-monitor-common'; // This is the hash for topic[0] of the core contract event LogMessagePublished // https://github.com/wormhole-foundation/wormhole/blob/main/ethereum/contracts/Implementation.sol#L12 @@ -34,34 +35,13 @@ export class EVMWatcher extends Watcher { constructor( network: Network, chain: PlatformToChains<'Evm'>, - finalizedBlockTag: BlockTag = 'latest' + finalizedBlockTag: BlockTag, + mode: Mode ) { - super(network, chain); + super(network, chain, mode); this.lastTimestamp = 0; this.latestFinalizedBlockNumber = 0; this.finalizedBlockTag = finalizedBlockTag; - // Special cases for batch size - if (chain === 'Acala' || chain === 'Karura' || chain === 'Berachain') { - this.maximumBatchSize = 50; - } else if ( - chain === 'Blast' || - chain === 'Klaytn' || - chain === 'Scroll' || - chain === 'Snaxchain' || - chain === 'Unichain' || - chain === 'Worldchain' || - chain === 'Monad' || - chain === 'Ink' || - chain === 'HyperEVM' || - chain === 'Seievm' || - chain === 'Xlayer' - ) { - this.maximumBatchSize = 10; - } - // Special cases for watch loop delay - if (chain === 'Berachain') { - this.watchLoopDelay = 1000; - } } async getBlock(blockNumberOrTag: number | BlockTag): Promise { diff --git a/watcher/src/watchers/FTEVMWatcher.ts b/watcher/src/watchers/FTEVMWatcher.ts index 5e07cb9d..8e6dd84a 100644 --- a/watcher/src/watchers/FTEVMWatcher.ts +++ b/watcher/src/watchers/FTEVMWatcher.ts @@ -1,32 +1,19 @@ import knex, { Knex } from 'knex'; -import { Watcher } from './Watcher'; import { chainToChainId, contracts, Network, toChainId } from '@wormhole-foundation/sdk-base'; import { assertEnvironmentVariable } from '@wormhole-foundation/wormhole-monitor-common'; import { FAST_TRANSFER_CONTRACTS, FTEVMChain } from '../fastTransfer/consts'; import { ethers } from 'ethers'; -import { AXIOS_CONFIG_JSON, RPCS_BY_CHAIN } from '../consts'; +import { RPCS_BY_CHAIN } from '../consts'; import { makeBlockKey } from '../databases/utils'; -import { Block } from './EVMWatcher'; -import { BigNumber } from 'ethers'; -import axios from 'axios'; +import { BlockTag, EVMWatcher, LOG_MESSAGE_PUBLISHED_TOPIC, wormholeInterface } from './EVMWatcher'; import { MarketOrder } from '../fastTransfer/types'; -import { Log } from '@ethersproject/abstract-provider'; -import { ethers_contracts } from '@wormhole-foundation/sdk-evm-core'; import { EvmTokenRouter, LiquidityLayerTransactionResult, } from '@wormhole-foundation/example-liquidity-layer-evm'; import isNotNull from '../utils/isNotNull'; -export type BlockTag = 'finalized' | 'safe' | 'latest'; -export const LOG_MESSAGE_PUBLISHED_TOPIC = - '0x6eb224fb001ed210e379b335e35efe88672a8ce935d981a6896b27ffdf52a3b2'; -export const wormholeInterface = ethers_contracts.Implementation__factory.createInterface(); - -export class FTEVMWatcher extends Watcher { - finalizedBlockTag: BlockTag; - lastTimestamp: number; - latestFinalizedBlockNumber: number; +export class FTEVMWatcher extends EVMWatcher { rpc: string; provider: ethers.providers.JsonRpcProvider; tokenRouterContract: string; @@ -36,13 +23,10 @@ export class FTEVMWatcher extends Watcher { constructor( network: Network, chain: FTEVMChain, - finalizedBlockTag: BlockTag = 'latest', + finalizedBlockTag: BlockTag = 'finalized', isTest = false ) { - super(network, chain, 'ft'); - this.lastTimestamp = 0; - this.latestFinalizedBlockNumber = 0; - this.finalizedBlockTag = finalizedBlockTag; + super(network, chain, finalizedBlockTag, 'ft'); this.provider = new ethers.providers.JsonRpcProvider(RPCS_BY_CHAIN[network][chain]); this.rpc = RPCS_BY_CHAIN[network][chain]!; this.evmTokenRouter = new EvmTokenRouter( @@ -74,71 +58,6 @@ export class FTEVMWatcher extends Watcher { this.logger.debug(`Initialized FTEVMWatcher for ${network} ${chain}`); } - async getBlock(blockNumberOrTag: number | BlockTag): Promise { - const rpc = RPCS_BY_CHAIN[this.network][this.chain]; - if (!rpc) { - throw new Error(`${this.chain} RPC is not defined!`); - } - let result = ( - await axios.post( - rpc, - [ - { - jsonrpc: '2.0', - id: 1, - method: 'eth_getBlockByNumber', - params: [ - typeof blockNumberOrTag === 'number' - ? `0x${blockNumberOrTag.toString(16)}` - : blockNumberOrTag, - false, - ], - }, - ], - AXIOS_CONFIG_JSON - ) - )?.data?.[0]; - if (result && result.result === null) { - // Found null block - if ( - typeof blockNumberOrTag === 'number' && - blockNumberOrTag < this.latestFinalizedBlockNumber - 1000 - ) { - return { - hash: '', - number: BigNumber.from(blockNumberOrTag).toNumber(), - timestamp: BigNumber.from(this.lastTimestamp).toNumber(), - }; - } - } else if (result && result.error && result.error.code === 6969) { - return { - hash: '', - number: BigNumber.from(blockNumberOrTag).toNumber(), - timestamp: BigNumber.from(this.lastTimestamp).toNumber(), - }; - } - result = result?.result; - if (result && result.hash && result.number && result.timestamp) { - // Convert to Ethers compatible type - this.lastTimestamp = result.timestamp; - return { - hash: result.hash, - number: BigNumber.from(result.number).toNumber(), - timestamp: BigNumber.from(result.timestamp).toNumber(), - }; - } - throw new Error( - `Unable to parse result of eth_getBlockByNumber for ${blockNumberOrTag} on ${rpc}` - ); - } - - async getFinalizedBlockNumber(): Promise { - this.logger.info(`fetching block ${this.finalizedBlockTag}`); - const block: Block = await this.getBlock(this.finalizedBlockTag); - this.latestFinalizedBlockNumber = block.number; - return block.number; - } - async getFtMessagesForBlocks(fromBlock: number, toBlock: number): Promise { const tokenRouterResults = await this.getFTResultsInRange(fromBlock, toBlock); this.logger.debug(`number of tokenRouterResults: ${tokenRouterResults.results.length}`); @@ -361,49 +280,6 @@ export class FTEVMWatcher extends Watcher { return block.timestamp; } - async getLogs( - fromBlock: number, - toBlock: number, - address: string, - topics: string[] - ): Promise> { - const rpc = RPCS_BY_CHAIN[this.network][this.chain]; - if (!rpc) { - throw new Error(`${this.chain} RPC is not defined!`); - } - const result = ( - await axios.post( - rpc, - [ - { - jsonrpc: '2.0', - id: 1, - method: 'eth_getLogs', - params: [ - { - fromBlock: `0x${fromBlock.toString(16)}`, - toBlock: `0x${toBlock.toString(16)}`, - address, - topics, - }, - ], - }, - ], - AXIOS_CONFIG_JSON - ) - )?.data?.[0]?.result; - if (result) { - // Convert to Ethers compatible type - return result.map((l: Log) => ({ - ...l, - blockNumber: BigNumber.from(l.blockNumber).toNumber(), - transactionIndex: BigNumber.from(l.transactionIndex).toNumber(), - logIndex: BigNumber.from(l.logIndex).toNumber(), - })); - } - throw new Error(`Unable to parse result of eth_getLogs for ${fromBlock}-${toBlock} on ${rpc}`); - } - normalizeAddress(address: string): string { // If the address is already 66 characters long and starts with '0x', return it as is if (address.length === 66 && address.startsWith('0x')) { diff --git a/watcher/src/watchers/InjectiveExplorerWatcher.ts b/watcher/src/watchers/InjectiveExplorerWatcher.ts index fcd1c6f2..30e08d95 100644 --- a/watcher/src/watchers/InjectiveExplorerWatcher.ts +++ b/watcher/src/watchers/InjectiveExplorerWatcher.ts @@ -18,7 +18,7 @@ export class InjectiveExplorerWatcher extends Watcher { latestBlockHeight: number; constructor(network: Network) { - super(network, 'Injective'); + super(network, 'Injective', 'vaa'); this.rpc = RPCS_BY_CHAIN[this.network][this.chain]; if (!this.rpc) { throw new Error(`${this.chain} RPC is not defined!`); diff --git a/watcher/src/watchers/MoonbeamWatcher.ts b/watcher/src/watchers/MoonbeamWatcher.ts deleted file mode 100644 index 7a8b7d1b..00000000 --- a/watcher/src/watchers/MoonbeamWatcher.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { sleep } from '@wormhole-foundation/wormhole-monitor-common'; -import axios from 'axios'; -import { AXIOS_CONFIG_JSON, RPCS_BY_CHAIN } from '../consts'; -import { EVMWatcher } from './EVMWatcher'; -import { Network } from '@wormhole-foundation/sdk-base'; - -export class MoonbeamWatcher extends EVMWatcher { - constructor(network: Network) { - super(network, 'Moonbeam'); - } - async getFinalizedBlockNumber(): Promise { - const latestBlock = await super.getFinalizedBlockNumber(); - let isBlockFinalized = false; - const rpc = RPCS_BY_CHAIN[this.network].Moonbeam; - if (!rpc) { - throw new Error('Moonbeam RPC is not defined!'); - } - while (!isBlockFinalized) { - await sleep(100); - // refetch the block by number to get an up-to-date hash - try { - const blockFromNumber = await this.getBlock(latestBlock); - isBlockFinalized = - ( - await axios.post( - rpc, - [ - { - jsonrpc: '2.0', - id: 1, - method: 'moon_isBlockFinalized', - params: [blockFromNumber.hash], - }, - ], - AXIOS_CONFIG_JSON - ) - )?.data?.[0]?.result || false; - } catch (e) { - this.logger.error(`error while trying to check for finality of block ${latestBlock}`); - } - } - return latestBlock; - } -} diff --git a/watcher/src/watchers/NTTArbitrumWatcher.ts b/watcher/src/watchers/NTTArbitrumWatcher.ts deleted file mode 100644 index cb25b523..00000000 --- a/watcher/src/watchers/NTTArbitrumWatcher.ts +++ /dev/null @@ -1,136 +0,0 @@ -import axios from 'axios'; -import { AXIOS_CONFIG_JSON, RPCS_BY_CHAIN } from '../consts'; -import { EVMWatcher } from './EVMWatcher'; -import { NTTWatcher } from './NTTWatcher'; -import { Network } from '@wormhole-foundation/sdk-base'; - -export class NTTArbitrumWatcher extends NTTWatcher { - rpc: string | undefined; - nttWatcher: EVMWatcher; - latestL2Finalized: number; - l1L2Map: Map; - lastEthTime: number; - - constructor(network: Network) { - if (network === 'Mainnet') { - super(network, 'Arbitrum'); - } else { - super(network, 'ArbitrumSepolia'); - } - - this.rpc = RPCS_BY_CHAIN[this.network][this.chain]; - if (!this.rpc) { - throw new Error(`${this.chain} RPC is not defined!`); - } - this.nttWatcher = - network === 'Mainnet' - ? new NTTWatcher(network, 'Ethereum', 'finalized') - : new NTTWatcher(network, 'Sepolia', 'finalized'); - this.latestL2Finalized = 0; - this.l1L2Map = new Map(); - this.lastEthTime = 0; - this.maximumBatchSize = 1000; - } - - async getFinalizedBlockNumber(): Promise { - if (!this.rpc) { - throw new Error(`${this.chain} RPC is not defined!`); - } - - // This gets the latest L2 block so we can get the associated L1 block number - const l1Result: BlockByNumberResult = ( - await axios.post( - this.rpc, - [ - { - jsonrpc: '2.0', - id: 1, - method: 'eth_getBlockByNumber', - params: ['latest', false], - }, - ], - AXIOS_CONFIG_JSON - ) - )?.data?.[0]?.result; - if (!l1Result || !l1Result.l1BlockNumber || !l1Result.number) { - throw new Error( - `Unable to parse result of ArbitrumWatcher::eth_getBlockByNumber for latest on ${this.rpc}` - ); - } - const associatedL1: number = parseInt(l1Result.l1BlockNumber, 16); - const l2BlkNum: number = parseInt(l1Result.number, 16); - this.logger.debug( - 'getFinalizedBlockNumber() checking map L1Block: ' + associatedL1 + ' => L2Block: ' + l2BlkNum - ); - - // Only update the map, if the L2 block number is newer - const inMapL2 = this.l1L2Map.get(associatedL1); - if (!inMapL2 || inMapL2 < l2BlkNum) { - this.logger.debug( - `Updating map with ${associatedL1} => ${l2BlkNum}, size = ${this.l1L2Map.size}` - ); - this.l1L2Map.set(associatedL1, l2BlkNum); - } - - // Only check every 30 seconds - const now = Date.now(); - if (now - this.lastEthTime < 30_000) { - return this.latestL2Finalized; - } - this.lastEthTime = now; - - // Get the latest finalized L1 block number - const evmFinal = await this.nttWatcher.getFinalizedBlockNumber(); - this.logger.debug(`Finalized EVM block number = ${evmFinal}`); - - this.logger.debug('Size of map = ' + this.l1L2Map.size); - // Walk the map looking for finalized L2 block number - for (let [l1, l2] of this.l1L2Map) { - if (l1 <= evmFinal) { - this.latestL2Finalized = l2; - this.logger.debug(`Removing key ${l1} from map`); - this.l1L2Map.delete(l1); - } - } - - this.logger.debug(`LatestL2Finalized = ${this.latestL2Finalized}`); - return this.latestL2Finalized; - } - - // This function is only used in test code. - getFirstMapEntry(): number[] { - if (this.l1L2Map.size > 0) { - for (let [l1, l2] of this.l1L2Map) { - return [l1, l2]; - } - } - return [0, 0]; - } -} - -type BlockByNumberResult = { - baseFeePerGas: string; - difficulty: string; - extraData: string; - gasLimit: string; - gasUsed: string; - hash: string; - l1BlockNumber: string; - logsBloom: string; - miner: string; - mixHash: string; - nonce: string; - number: string; - parentHash: string; - receiptsRoot: string; - sendCount: string; - sendRoot: string; - sha3Uncles: string; - size: string; - stateRoot: string; - timestamp: string; - totalDifficulty: string; - transactions: string[]; - transactionsRoot: string; - uncles: string[]; -}; diff --git a/watcher/src/watchers/NTTWatcher.ts b/watcher/src/watchers/NTTWatcher.ts index 394e2671..f19d0c62 100644 --- a/watcher/src/watchers/NTTWatcher.ts +++ b/watcher/src/watchers/NTTWatcher.ts @@ -1,19 +1,10 @@ -import { ethers_contracts } from '@wormhole-foundation/sdk-evm-core'; import { Log } from '@ethersproject/abstract-provider'; -import { - Chain, - Network, - PlatformToChains, - chainToChainId, - contracts, -} from '@wormhole-foundation/sdk-base'; +import { Chain, Network, chainToChainId, contracts } from '@wormhole-foundation/sdk-base'; import { assertEnvironmentVariable, NTTChain, NTTEvmChain, } from '@wormhole-foundation/wormhole-monitor-common'; -import axios from 'axios'; -import { BigNumber } from 'ethers'; import knex, { Knex } from 'knex'; import { InboundTransferQueuedTopic, @@ -28,42 +19,20 @@ import { getNttManagerMessageDigest, } from '../NTTConsts'; import { NTT_MANAGER_CONTRACT_ARRAY } from '@wormhole-foundation/wormhole-monitor-common'; -import { AXIOS_CONFIG_JSON, RPCS_BY_CHAIN } from '../consts'; import { extractBlockFromKey, makeBlockKey } from '../databases/utils'; import { WormholeLogger } from '../utils/logger'; import { formatIntoTimestamp } from '../utils/timestamp'; import { NativeTokenTransfer, NttManagerMessage, WormholeTransceiverMessage } from './NTTPayloads'; -import { Watcher } from './Watcher'; import { parseWormholeLog } from './NTTUtils'; +import { BlockTag, EVMWatcher, LOG_MESSAGE_PUBLISHED_TOPIC, wormholeInterface } from './EVMWatcher'; -export const LOG_MESSAGE_PUBLISHED_TOPIC = - '0x6eb224fb001ed210e379b335e35efe88672a8ce935d981a6896b27ffdf52a3b2'; -export const wormholeInterface = ethers_contracts.Implementation__factory.createInterface(); - -export type BlockTag = 'finalized' | 'safe' | 'latest'; -export type Block = { - hash: string; - number: number; - timestamp: number; -}; -export type ErrorBlock = { - code: number; //6969, - message: string; //'Error: No response received from RPC endpoint in 60s' -}; - -export class NTTWatcher extends Watcher { +export class NTTWatcher extends EVMWatcher { chain: NTTChain; - finalizedBlockTag: BlockTag; - lastTimestamp: number; - latestFinalizedBlockNumber: number; pg: Knex; - constructor(network: Network, chain: NTTEvmChain, finalizedBlockTag: BlockTag = 'latest') { - super(network, chain, 'ntt'); + constructor(network: Network, chain: NTTEvmChain, finalizedBlockTag: BlockTag = 'finalized') { + super(network, chain, finalizedBlockTag, 'ntt'); this.chain = chain; - this.lastTimestamp = 0; - this.latestFinalizedBlockNumber = 0; - this.finalizedBlockTag = finalizedBlockTag; this.pg = knex({ client: 'pg', connection: { @@ -77,170 +46,6 @@ export class NTTWatcher extends Watcher { this.logger.debug('NTTWatcher', network, chain, finalizedBlockTag); } - async getBlock(blockNumberOrTag: number | BlockTag): Promise { - const rpc = RPCS_BY_CHAIN[this.network][this.chain]; - if (!rpc) { - throw new Error(`${this.chain} RPC is not defined!`); - } - let result = ( - await axios.post( - rpc, - [ - { - jsonrpc: '2.0', - id: 1, - method: 'eth_getBlockByNumber', - params: [ - typeof blockNumberOrTag === 'number' - ? `0x${blockNumberOrTag.toString(16)}` - : blockNumberOrTag, - false, - ], - }, - ], - AXIOS_CONFIG_JSON - ) - )?.data?.[0]; - if (result && result.result === null) { - // Found null block - if ( - typeof blockNumberOrTag === 'number' && - blockNumberOrTag < this.latestFinalizedBlockNumber - 1000 - ) { - return { - hash: '', - number: BigNumber.from(blockNumberOrTag).toNumber(), - timestamp: BigNumber.from(this.lastTimestamp).toNumber(), - }; - } - } else if (result && result.error && result.error.code === 6969) { - return { - hash: '', - number: BigNumber.from(blockNumberOrTag).toNumber(), - timestamp: BigNumber.from(this.lastTimestamp).toNumber(), - }; - } - result = result?.result; - if (result && result.hash && result.number && result.timestamp) { - // Convert to Ethers compatible type - this.lastTimestamp = result.timestamp; - return { - hash: result.hash, - number: BigNumber.from(result.number).toNumber(), - timestamp: BigNumber.from(result.timestamp).toNumber(), - }; - } - throw new Error( - `Unable to parse result of eth_getBlockByNumber for ${blockNumberOrTag} on ${rpc}` - ); - } - async getBlocks(fromBlock: number, toBlock: number): Promise { - const rpc = RPCS_BY_CHAIN[this.network][this.chain]; - if (!rpc) { - throw new Error(`${this.chain} RPC is not defined!`); - } - const reqs: any[] = []; - for (let blockNumber = fromBlock; blockNumber <= toBlock; blockNumber++) { - reqs.push({ - jsonrpc: '2.0', - id: (blockNumber - fromBlock).toString(), - method: 'eth_getBlockByNumber', - params: [`0x${blockNumber.toString(16)}`, false], - }); - } - const results = (await axios.post(rpc, reqs, AXIOS_CONFIG_JSON))?.data; - if (results && results.length) { - // Convert to Ethers compatible type - return results.map( - (response: undefined | { result?: Block; error?: ErrorBlock }, idx: number) => { - // Karura is getting 6969 errors for some blocks, so we'll just return empty blocks for those instead of throwing an error. - // We take the timestamp from the previous block, which is not ideal but should be fine. - if ( - (response && - response.result === null && - fromBlock + idx < this.latestFinalizedBlockNumber - 1000) || - (response?.error && response.error?.code && response.error.code === 6969) - ) { - return { - hash: '', - number: BigNumber.from(fromBlock + idx).toNumber(), - timestamp: BigNumber.from(this.lastTimestamp).toNumber(), - }; - } - if ( - response?.result && - response.result?.hash && - response.result.number && - response.result.timestamp - ) { - this.lastTimestamp = response.result.timestamp; - return { - hash: response.result.hash, - number: BigNumber.from(response.result.number).toNumber(), - timestamp: BigNumber.from(response.result.timestamp).toNumber(), - }; - } - this.logger.error(reqs[idx], response, idx); - throw new Error( - `Unable to parse result of eth_getBlockByNumber for ${fromBlock + idx} on ${rpc}` - ); - } - ); - } - throw new Error( - `Unable to parse result of eth_getBlockByNumber for range ${fromBlock}-${toBlock} on ${rpc}` - ); - } - async getLogs( - fromBlock: number, - toBlock: number, - address: string, - topics: string[] - ): Promise> { - const rpc = RPCS_BY_CHAIN[this.network][this.chain]; - if (!rpc) { - throw new Error(`${this.chain} RPC is not defined!`); - } - const result = ( - await axios.post( - rpc, - [ - { - jsonrpc: '2.0', - id: 1, - method: 'eth_getLogs', - params: [ - { - fromBlock: `0x${fromBlock.toString(16)}`, - toBlock: `0x${toBlock.toString(16)}`, - address, - topics, - }, - ], - }, - ], - AXIOS_CONFIG_JSON - ) - )?.data?.[0]?.result; - if (result) { - // Convert to Ethers compatible type - return result.map((l: Log) => ({ - ...l, - blockNumber: BigNumber.from(l.blockNumber).toNumber(), - transactionIndex: BigNumber.from(l.transactionIndex).toNumber(), - logIndex: BigNumber.from(l.logIndex).toNumber(), - })); - } - throw new Error(`Unable to parse result of eth_getLogs for ${fromBlock}-${toBlock} on ${rpc}`); - } - - async getFinalizedBlockNumber(): Promise { - this.logger.info(`fetching block ${this.finalizedBlockTag}`); - const block: Block = await this.getBlock(this.finalizedBlockTag); - this.latestFinalizedBlockNumber = block.number; - return block.number; - } - // This only needs to return the latest block looked at. async getNttMessagesForBlocks(fromBlock: number, toBlock: number): Promise { const nttAddresses = NTT_MANAGER_CONTRACT_ARRAY[this.network][this.chain]; diff --git a/watcher/src/watchers/NearArchiveWatcher.ts b/watcher/src/watchers/NearArchiveWatcher.ts index 8ee6cd59..796aceef 100644 --- a/watcher/src/watchers/NearArchiveWatcher.ts +++ b/watcher/src/watchers/NearArchiveWatcher.ts @@ -21,7 +21,7 @@ export class NearArchiveWatcher extends Watcher { provider: Provider | null = null; constructor(network: Network) { - super(network, 'Near'); + super(network, 'Near', 'vaa'); this.maximumBatchSize = 1_000_000; this.watchLoopDelay = 60 * 60 * 1000; // 1 hour } diff --git a/watcher/src/watchers/NearWatcher.ts b/watcher/src/watchers/NearWatcher.ts index ccb04045..9fd23da4 100644 --- a/watcher/src/watchers/NearWatcher.ts +++ b/watcher/src/watchers/NearWatcher.ts @@ -12,7 +12,7 @@ export class NearWatcher extends Watcher { provider: Provider | null = null; constructor(network: Network) { - super(network, 'Near'); + super(network, 'Near', 'vaa'); } async getFinalizedBlockNumber(): Promise { diff --git a/watcher/src/watchers/PolygonWatcher.ts b/watcher/src/watchers/PolygonWatcher.ts deleted file mode 100644 index e0944670..00000000 --- a/watcher/src/watchers/PolygonWatcher.ts +++ /dev/null @@ -1,38 +0,0 @@ -import axios from 'axios'; -import { ethers } from 'ethers'; -import { AXIOS_CONFIG_JSON, POLYGON_ROOT_CHAIN_INFO } from '../consts'; -import { EVMWatcher } from './EVMWatcher'; -import { Network } from '@wormhole-foundation/sdk-base'; - -export class PolygonWatcher extends EVMWatcher { - constructor(network: Network) { - super(network, 'Polygon'); - } - async getFinalizedBlockNumber(): Promise { - this.logger.info('fetching last child block from Ethereum'); - const rootChain = new ethers.utils.Interface([ - `function getLastChildBlock() external view returns (uint256)`, - ]); - const callData = rootChain.encodeFunctionData('getLastChildBlock'); - const callResult = ( - await axios.post( - POLYGON_ROOT_CHAIN_INFO[this.network].rpc, - [ - { - jsonrpc: '2.0', - id: 1, - method: 'eth_call', - params: [ - { to: POLYGON_ROOT_CHAIN_INFO[this.network].address, data: callData }, - 'latest', // does the guardian use latest? - ], - }, - ], - AXIOS_CONFIG_JSON - ) - )?.data?.[0]?.result; - const block = rootChain.decodeFunctionResult('getLastChildBlock', callResult)[0].toNumber(); - this.logger.info(`rooted child block ${block}`); - return block; - } -} diff --git a/watcher/src/watchers/SuiWatcher.ts b/watcher/src/watchers/SuiWatcher.ts index 01b62ea2..901a43ec 100644 --- a/watcher/src/watchers/SuiWatcher.ts +++ b/watcher/src/watchers/SuiWatcher.ts @@ -27,7 +27,7 @@ export class SuiWatcher extends Watcher { maximumBatchSize: number = 100000; // arbitrarily large as this pages back by events constructor(network: Network) { - super(network, 'Sui'); + super(network, 'Sui', 'vaa'); this.client = new JsonRpcClient(RPCS_BY_CHAIN[this.network][this.chain]!); } diff --git a/watcher/src/watchers/TerraExplorerWatcher.ts b/watcher/src/watchers/TerraExplorerWatcher.ts index 4b87ee9e..0769e250 100644 --- a/watcher/src/watchers/TerraExplorerWatcher.ts +++ b/watcher/src/watchers/TerraExplorerWatcher.ts @@ -16,7 +16,7 @@ export class TerraExplorerWatcher extends Watcher { latestBlockHeight: number; constructor(network: Network, chain: PlatformToChains<'Cosmwasm'>) { - super(network, chain); + super(network, chain, 'vaa'); this.rpc = RPCS_BY_CHAIN[this.network][this.chain]; if (!this.rpc) { throw new Error(`${this.chain} RPC is not defined!`); diff --git a/watcher/src/watchers/VAAWatcher.ts b/watcher/src/watchers/VAAWatcher.ts new file mode 100644 index 00000000..68b4c846 --- /dev/null +++ b/watcher/src/watchers/VAAWatcher.ts @@ -0,0 +1,12 @@ +import { Network, PlatformToChains } from '@wormhole-foundation/sdk-base'; +import { BlockTag, EVMWatcher } from './EVMWatcher'; + +export class VAAWatcher extends EVMWatcher { + constructor( + network: Network, + chain: PlatformToChains<'Evm'>, + finalizedBlockTag: BlockTag = 'finalized' + ) { + super(network, chain, finalizedBlockTag, 'vaa'); + } +} diff --git a/watcher/src/watchers/Watcher.ts b/watcher/src/watchers/Watcher.ts index c5c41fbd..553e5f88 100644 --- a/watcher/src/watchers/Watcher.ts +++ b/watcher/src/watchers/Watcher.ts @@ -19,7 +19,7 @@ export class Watcher { mode: Mode; watchLoopDelay: number = 0; // in milliseconds - constructor(network: Network, chain: Chain, mode: Mode = 'vaa') { + constructor(network: Network, chain: Chain, mode: Mode) { this.network = network; this.chain = chain; this.mode = mode; @@ -29,6 +29,28 @@ export class Watcher { // `ft` -> 'FT_' const loggerPrefix = mode.toUpperCase() + '_'; this.logger = getLogger(loggerPrefix + chain); + // Special cases for batch size + if (chain === 'Acala' || chain === 'Karura' || chain === 'Berachain') { + this.maximumBatchSize = 50; + } else if ( + chain === 'Blast' || + chain === 'Klaytn' || + chain === 'Scroll' || + chain === 'Snaxchain' || + chain === 'Unichain' || + chain === 'Worldchain' || + chain === 'Monad' || + chain === 'Ink' || + chain === 'HyperEVM' || + chain === 'Seievm' || + chain === 'Xlayer' + ) { + this.maximumBatchSize = 10; + } + // Special cases for watch loop delay + if (chain === 'Berachain') { + this.watchLoopDelay = 1000; + } } async getFinalizedBlockNumber(): Promise { diff --git a/watcher/src/watchers/__tests__/ArbitrumWatcher.test.ts b/watcher/src/watchers/__tests__/ArbitrumWatcher.test.ts deleted file mode 100644 index b1ab6b08..00000000 --- a/watcher/src/watchers/__tests__/ArbitrumWatcher.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { expect, jest, test } from '@jest/globals'; -import { INITIAL_DEPLOYMENT_BLOCK_BY_NETWORK_AND_CHAIN } from '@wormhole-foundation/wormhole-monitor-common'; -import { ArbitrumWatcher } from '../ArbitrumWatcher'; - -jest.setTimeout(60000); - -const initialArbitrumBlock = Number( - INITIAL_DEPLOYMENT_BLOCK_BY_NETWORK_AND_CHAIN['Mainnet'].Arbitrum -); -const initialEthBlock = Number(INITIAL_DEPLOYMENT_BLOCK_BY_NETWORK_AND_CHAIN['Mainnet'].Ethereum); - -test('getFinalizedBlockNumber', async () => { - const watcher = new ArbitrumWatcher('Mainnet'); - const blockNumber = await watcher.getFinalizedBlockNumber(); - // The blockNumber is 0 because the most recent L2 block isn't in - // a finalized L1 block, yet. It's in an L1 block. That L1 block - // just isn't finalized, yet. Now, if we had this test run for like - // 20 minutes, then we would get a non-zero result. - expect(blockNumber).toEqual(0); - let retval: number[] = watcher.getFirstMapEntry(); - expect(retval[0]).toBeGreaterThan(initialEthBlock); - expect(retval[1]).toBeGreaterThan(initialArbitrumBlock); -}); - -test('getMessagesForBlocks', async () => { - const watcher = new ArbitrumWatcher('Mainnet'); - const { vaasByBlock } = await watcher.getMessagesForBlocks(114500582, 114500584); - const entries = Object.entries(vaasByBlock); - expect(entries.length).toEqual(3); - expect(entries.filter(([block, vaas]) => vaas.length === 0).length).toEqual(2); - expect(entries.filter(([block, vaas]) => vaas.length === 1).length).toEqual(1); - expect(entries.filter(([block, vaas]) => vaas.length === 2).length).toEqual(0); - expect(vaasByBlock['114500583/2023-07-24T15:12:14.000Z']).toBeDefined(); - expect(vaasByBlock['114500583/2023-07-24T15:12:14.000Z'].length).toEqual(1); - expect(vaasByBlock['114500583/2023-07-24T15:12:14.000Z'][0]).toEqual( - '0x39da3b500e5d65e82ca20cc8c4737fc0aa6c4e2c6c5f7e657834bd607c7109d9:23/0000000000000000000000000b2402144bb366a632d14b83f244d2e0e21bd39c/7628' - ); -}); diff --git a/watcher/src/watchers/__tests__/BaseWatcher.test.ts b/watcher/src/watchers/__tests__/BaseWatcher.test.ts index f621921c..d95d09f1 100644 --- a/watcher/src/watchers/__tests__/BaseWatcher.test.ts +++ b/watcher/src/watchers/__tests__/BaseWatcher.test.ts @@ -7,13 +7,13 @@ jest.setTimeout(60000); const initialBaseBlock = Number(INITIAL_DEPLOYMENT_BLOCK_BY_NETWORK_AND_CHAIN['Mainnet'].Base); test('getFinalizedBlockNumber', async () => { - const watcher = new EVMWatcher('Mainnet', 'Base'); + const watcher = new EVMWatcher('Mainnet', 'Base', 'finalized', 'vaa'); const blockNumber = await watcher.getFinalizedBlockNumber(); expect(blockNumber).toBeGreaterThan(initialBaseBlock); }); test('getMessagesForBlocks', async () => { - const watcher = new EVMWatcher('Mainnet', 'Base'); + const watcher = new EVMWatcher('Mainnet', 'Base', 'finalized', 'vaa'); const { vaasByBlock } = await watcher.getMessagesForBlocks(1544175, 1544185); expect(vaasByBlock).toMatchObject({ '1544175/2023-07-20T18:28:17.000Z': [], @@ -31,7 +31,7 @@ test('getMessagesForBlocks', async () => { }); test('getMessagesForBlockWithWHMsg', async () => { - const watcher = new EVMWatcher('Mainnet', 'Base'); + const watcher = new EVMWatcher('Mainnet', 'Base', 'finalized', 'vaa'); const { vaasByBlock } = await watcher.getMessagesForBlocks(1557420, 1557429); expect(vaasByBlock).toMatchObject({ '1557420/2023-07-21T01:49:47.000Z': [], diff --git a/watcher/src/watchers/__tests__/EVMWatcher.test.ts b/watcher/src/watchers/__tests__/EVMWatcher.test.ts index 3a2b3df2..23d09282 100644 --- a/watcher/src/watchers/__tests__/EVMWatcher.test.ts +++ b/watcher/src/watchers/__tests__/EVMWatcher.test.ts @@ -8,11 +8,17 @@ const initialAvalancheBlock = Number( ); const initialCeloBlock = Number(INITIAL_DEPLOYMENT_BLOCK_BY_NETWORK_AND_CHAIN['Mainnet'].Celo); const initialOasisBlock = Number(INITIAL_DEPLOYMENT_BLOCK_BY_NETWORK_AND_CHAIN['Mainnet'].Oasis); +const initialMoonbeamBlock = Number( + INITIAL_DEPLOYMENT_BLOCK_BY_NETWORK_AND_CHAIN['Mainnet'].Moonbeam +); +const initialPolygonBlock = Number( + INITIAL_DEPLOYMENT_BLOCK_BY_NETWORK_AND_CHAIN['Mainnet'].Polygon +); jest.setTimeout(60000); test('getBlock by tag', async () => { - const watcher = new EVMWatcher('Mainnet', 'Avalanche'); + const watcher = new EVMWatcher('Mainnet', 'Avalanche', 'finalized', 'vaa'); const block = await watcher.getBlock('latest'); expect(block.number).toBeGreaterThan(initialAvalancheBlock); expect(block.timestamp).toBeGreaterThan(1671672811); @@ -20,7 +26,7 @@ test('getBlock by tag', async () => { }); test('getBlock by number', async () => { - const watcher = new EVMWatcher('Mainnet', 'Avalanche'); + const watcher = new EVMWatcher('Mainnet', 'Avalanche', 'finalized', 'vaa'); const block = await watcher.getBlock(46997506); expect(block.number).toEqual(46997506); expect(block.hash).toEqual('0x2d40ae06ad17d62b8e500cc451bc296e1e20c91140ce3cd6f2504bd3377e602f'); @@ -29,7 +35,7 @@ test('getBlock by number', async () => { }); test('getBlocks', async () => { - const watcher = new EVMWatcher('Mainnet', 'Avalanche'); + const watcher = new EVMWatcher('Mainnet', 'Avalanche', 'finalized', 'vaa'); const maxBatchSize = watcher.maximumBatchSize; const maxSizeMinusOne = maxBatchSize - 1; const avalancheBlock = 46997506; @@ -49,7 +55,7 @@ test('getBlocks', async () => { }); test('getLogs', async () => { - const watcher = new EVMWatcher('Mainnet', 'Avalanche'); + const watcher = new EVMWatcher('Mainnet', 'Avalanche', 'finalized', 'vaa'); const logs = await watcher.getLogs( 46997506, 46997599, @@ -65,13 +71,13 @@ test('getLogs', async () => { }); test('getFinalizedBlockNumber', async () => { - const watcher = new EVMWatcher('Mainnet', 'Avalanche'); + const watcher = new EVMWatcher('Mainnet', 'Avalanche', 'finalized', 'vaa'); const blockNumber = await watcher.getFinalizedBlockNumber(); expect(blockNumber).toBeGreaterThan(initialAvalancheBlock); }); test('getMessagesForBlocks', async () => { - const watcher = new EVMWatcher('Mainnet', 'Avalanche'); + const watcher = new EVMWatcher('Mainnet', 'Avalanche', 'finalized', 'vaa'); const { vaasByBlock } = await watcher.getMessagesForBlocks(46997500, 46997599); const entries = Object.entries(vaasByBlock); expect(entries.length).toEqual(100); @@ -86,7 +92,7 @@ test('getMessagesForBlocks', async () => { }); test('getBlock by tag (Oasis compatibility)', async () => { - const watcher = new EVMWatcher('Mainnet', 'Oasis'); + const watcher = new EVMWatcher('Mainnet', 'Oasis', 'finalized', 'vaa'); const block = await watcher.getBlock('latest'); expect(block.number).toBeGreaterThan(initialOasisBlock); expect(block.timestamp).toBeGreaterThan(3895665); @@ -94,7 +100,7 @@ test('getBlock by tag (Oasis compatibility)', async () => { }); test('getBlock by tag (Celo compatibility)', async () => { - const watcher = new EVMWatcher('Mainnet', 'Celo'); + const watcher = new EVMWatcher('Mainnet', 'Celo', 'finalized', 'vaa'); const block = await watcher.getBlock('latest'); expect(block.number).toBeGreaterThan(initialCeloBlock); expect(block.timestamp).toBeGreaterThan(1671672811); @@ -102,7 +108,7 @@ test('getBlock by tag (Celo compatibility)', async () => { }); test('getBlock by number (Celo compatibility)', async () => { - const watcher = new EVMWatcher('Mainnet', 'Celo'); + const watcher = new EVMWatcher('Mainnet', 'Celo', 'finalized', 'vaa'); const block = await watcher.getBlock(initialCeloBlock); expect(block.number).toEqual(initialCeloBlock); expect(block.timestamp).toEqual(1652314820); @@ -110,7 +116,7 @@ test('getBlock by number (Celo compatibility)', async () => { }); test('getMessagesForBlocks (Celo compatibility)', async () => { - const watcher = new EVMWatcher('Mainnet', 'Celo'); + const watcher = new EVMWatcher('Mainnet', 'Celo', 'finalized', 'vaa'); const { vaasByBlock } = await watcher.getMessagesForBlocks(13322450, 13322549); const entries = Object.entries(vaasByBlock); expect(entries.length).toEqual(100); @@ -125,7 +131,7 @@ test('getMessagesForBlocks (Celo compatibility)', async () => { }); test('getBlock by number (Karura compatibility)', async () => { - const watcher = new EVMWatcher('Mainnet', 'Karura'); + const watcher = new EVMWatcher('Mainnet', 'Karura', 'finalized', 'vaa'); const latestBlock = await watcher.getFinalizedBlockNumber(); const moreRecentBlockNumber = 4646601; // block { @@ -141,7 +147,7 @@ test('getBlock by number (Karura compatibility)', async () => { }); test('getMessagesForBlocks (Karura compatibility)', async () => { - const watcher = new EVMWatcher('Mainnet', 'Karura'); + const watcher = new EVMWatcher('Mainnet', 'Karura', 'finalized', 'vaa'); const { vaasByBlock } = await watcher.getMessagesForBlocks(4582511, 4582513); const entries = Object.entries(vaasByBlock); expect(entries.length).toEqual(3); @@ -153,7 +159,7 @@ test('getMessagesForBlocks (Karura compatibility)', async () => { }); test('getMessagesForBlocks (Karura compatibility 2)', async () => { - const watcher = new EVMWatcher('Mainnet', 'Karura'); + const watcher = new EVMWatcher('Mainnet', 'Karura', 'finalized', 'vaa'); await watcher.getFinalizedBlockNumber(); // This has the side effect of initializing the latestFinalizedBlockNumber const { vaasByBlock } = await watcher.getMessagesForBlocks(4595356, 4595358); const entries = Object.entries(vaasByBlock); @@ -162,7 +168,7 @@ test('getMessagesForBlocks (Karura compatibility 2)', async () => { // skipped as it was timing out the test test.skip('getBlock (Karura compatibility)', async () => { - const watcher = new EVMWatcher('Mainnet', 'Karura'); + const watcher = new EVMWatcher('Mainnet', 'Karura', 'finalized', 'vaa'); await watcher.getFinalizedBlockNumber(); // This has the side effect of initializing the latestFinalizedBlockNumber let block: Block = await watcher.getBlock(4582512); // 6969 block console.log('block', block); @@ -173,3 +179,30 @@ test.skip('getBlock (Karura compatibility)', async () => { // block = await watcher.getBlock(4619551); // good luck // console.log('block', block); }); + +test('getMessagesForBlocks', async () => { + const watcher = new EVMWatcher('Mainnet', 'Arbitrum', 'finalized', 'vaa'); + const { vaasByBlock } = await watcher.getMessagesForBlocks(114500582, 114500584); + const entries = Object.entries(vaasByBlock); + expect(entries.length).toEqual(3); + expect(entries.filter(([block, vaas]) => vaas.length === 0).length).toEqual(2); + expect(entries.filter(([block, vaas]) => vaas.length === 1).length).toEqual(1); + expect(entries.filter(([block, vaas]) => vaas.length === 2).length).toEqual(0); + expect(vaasByBlock['114500583/2023-07-24T15:12:14.000Z']).toBeDefined(); + expect(vaasByBlock['114500583/2023-07-24T15:12:14.000Z'].length).toEqual(1); + expect(vaasByBlock['114500583/2023-07-24T15:12:14.000Z'][0]).toEqual( + '0x39da3b500e5d65e82ca20cc8c4737fc0aa6c4e2c6c5f7e657834bd607c7109d9:23/0000000000000000000000000b2402144bb366a632d14b83f244d2e0e21bd39c/7628' + ); +}); + +test('getFinalizedBlockNumber', async () => { + const watcher = new EVMWatcher('Mainnet', 'Moonbeam', 'finalized', 'vaa'); + const blockNumber = await watcher.getFinalizedBlockNumber(); + expect(blockNumber).toBeGreaterThan(initialMoonbeamBlock); +}); + +test('getFinalizedBlockNumber', async () => { + const watcher = new EVMWatcher('Mainnet', 'Polygon', 'finalized', 'vaa'); + const blockNumber = await watcher.getFinalizedBlockNumber(); + expect(blockNumber).toBeGreaterThan(initialPolygonBlock); +}); diff --git a/watcher/src/watchers/__tests__/MoonbeamWatcher.test.ts b/watcher/src/watchers/__tests__/MoonbeamWatcher.test.ts deleted file mode 100644 index 402816fc..00000000 --- a/watcher/src/watchers/__tests__/MoonbeamWatcher.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { expect, jest, test } from '@jest/globals'; -import { INITIAL_DEPLOYMENT_BLOCK_BY_NETWORK_AND_CHAIN } from '@wormhole-foundation/wormhole-monitor-common'; -import { MoonbeamWatcher } from '../MoonbeamWatcher'; - -jest.setTimeout(60000); - -const initialMoonbeamBlock = Number( - INITIAL_DEPLOYMENT_BLOCK_BY_NETWORK_AND_CHAIN['Mainnet'].Moonbeam -); - -test('getFinalizedBlockNumber', async () => { - const watcher = new MoonbeamWatcher('Mainnet'); - const blockNumber = await watcher.getFinalizedBlockNumber(); - expect(blockNumber).toBeGreaterThan(initialMoonbeamBlock); -}); diff --git a/watcher/src/watchers/__tests__/OptimismWatcher.test.ts b/watcher/src/watchers/__tests__/OptimismWatcher.test.ts index 55dd73d5..20ba796e 100644 --- a/watcher/src/watchers/__tests__/OptimismWatcher.test.ts +++ b/watcher/src/watchers/__tests__/OptimismWatcher.test.ts @@ -9,14 +9,14 @@ const initialOptimismBlock = Number( ); test('getFinalizedBlockNumber', async () => { - const watcher = new EVMWatcher('Mainnet', 'Optimism'); + const watcher = new EVMWatcher('Mainnet', 'Optimism', 'finalized', 'vaa'); const blockNumber = await watcher.getFinalizedBlockNumber(); console.log('blockNumber', blockNumber); expect(blockNumber).toBeGreaterThan(105235062); }); test('getMessagesForBlocks', async () => { - const watcher = new EVMWatcher('Mainnet', 'Optimism'); + const watcher = new EVMWatcher('Mainnet', 'Optimism', 'finalized', 'vaa'); const { vaasByBlock } = await watcher.getMessagesForBlocks(105235070, 105235080); expect(vaasByBlock).toMatchObject({ '105235070/2023-06-06T16:28:37.000Z': [], diff --git a/watcher/src/watchers/__tests__/PolygonWatcher.test.ts b/watcher/src/watchers/__tests__/PolygonWatcher.test.ts deleted file mode 100644 index e07dd481..00000000 --- a/watcher/src/watchers/__tests__/PolygonWatcher.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { expect, jest, test } from '@jest/globals'; -import { INITIAL_DEPLOYMENT_BLOCK_BY_NETWORK_AND_CHAIN } from '@wormhole-foundation/wormhole-monitor-common'; -import { PolygonWatcher } from '../PolygonWatcher'; - -jest.setTimeout(60000); - -const initialPolygonBlock = Number( - INITIAL_DEPLOYMENT_BLOCK_BY_NETWORK_AND_CHAIN['Mainnet'].Polygon -); - -test('getFinalizedBlockNumber', async () => { - const watcher = new PolygonWatcher('Mainnet'); - const blockNumber = await watcher.getFinalizedBlockNumber(); - expect(blockNumber).toBeGreaterThan(initialPolygonBlock); -}); diff --git a/watcher/src/watchers/index.ts b/watcher/src/watchers/index.ts index cf69e9b1..9f9c3642 100644 --- a/watcher/src/watchers/index.ts +++ b/watcher/src/watchers/index.ts @@ -1,7 +1,4 @@ export { AptosWatcher } from './AptosWatcher'; -export { BSCWatcher } from './BSCWatcher'; export { CosmwasmWatcher } from './CosmwasmWatcher'; export { EVMWatcher } from './EVMWatcher'; export { InjectiveExplorerWatcher } from './InjectiveExplorerWatcher'; -export { MoonbeamWatcher } from './MoonbeamWatcher'; -export { PolygonWatcher } from './PolygonWatcher'; diff --git a/watcher/src/watchers/utils.ts b/watcher/src/watchers/utils.ts index d0e0829d..9667f327 100644 --- a/watcher/src/watchers/utils.ts +++ b/watcher/src/watchers/utils.ts @@ -1,11 +1,7 @@ import { AlgorandWatcher } from './AlgorandWatcher'; import { AptosWatcher } from './AptosWatcher'; -import { ArbitrumWatcher } from './ArbitrumWatcher'; -import { BSCWatcher } from './BSCWatcher'; import { CosmwasmWatcher } from './CosmwasmWatcher'; -import { EVMWatcher } from './EVMWatcher'; import { InjectiveExplorerWatcher } from './InjectiveExplorerWatcher'; -import { MoonbeamWatcher } from './MoonbeamWatcher'; import { SolanaWatcher } from './SolanaWatcher'; import { TerraExplorerWatcher } from './TerraExplorerWatcher'; import { Watcher } from './Watcher'; @@ -14,79 +10,75 @@ import { SeiExplorerWatcher } from './SeiExplorerWatcher'; import { WormchainWatcher } from './WormchainWatcher'; import { NearArchiveWatcher } from './NearArchiveWatcher'; import { NTTWatcher } from './NTTWatcher'; -import { NTTArbitrumWatcher } from './NTTArbitrumWatcher'; import { NTTSolanaWatcher } from './NTTSolanaWatcher'; import { Chain, Network } from '@wormhole-foundation/sdk-base'; import { FTEVMWatcher } from './FTEVMWatcher'; import { FTSolanaWatcher } from './FTSolanaWatcher'; import { isFTEVMChain } from '../fastTransfer/consts'; +import { VAAWatcher } from './VAAWatcher'; -export function makeFinalizedWatcher(network: Network, chainName: Chain): Watcher { +export function makeFinalizedVaaWatcher(network: Network, chainName: Chain): Watcher { if (chainName === 'Solana') { return new SolanaWatcher(network); - } else if (chainName === 'Ethereum' || chainName === 'Karura' || chainName === 'Acala') { - return new EVMWatcher(network, chainName, 'finalized'); - } else if (chainName === 'Bsc') { - return new BSCWatcher(network); } else if ( + chainName === 'Acala' || + chainName === 'Arbitrum' || chainName === 'Avalanche' || - chainName === 'Oasis' || - chainName === 'Fantom' || - chainName === 'Klaytn' || - chainName === 'Polygon' || + chainName === 'Base' || + chainName === 'Berachain' || + chainName === 'Blast' || + chainName === 'Bsc' || chainName === 'Celo' || + chainName === 'Ethereum' || + chainName === 'HyperEVM' || + chainName === 'Ink' || + chainName === 'Karura' || + chainName === 'Mantle' || + chainName === 'Monad' || + chainName === 'Moonbeam' || chainName === 'Optimism' || + chainName === 'Polygon' || chainName === 'Scroll' || - chainName === 'Mantle' || - chainName === 'Blast' || - chainName === 'Xlayer' || - chainName === 'Berachain' || chainName === 'Seievm' || chainName === 'Snaxchain' || chainName === 'Unichain' || chainName === 'Worldchain' || - chainName === 'Monad' || - chainName === 'Ink' || - chainName === 'HyperEVM' || - chainName === 'Base' + chainName === 'Xlayer' ) { - return new EVMWatcher(network, chainName); + return new VAAWatcher(network, chainName); + } else if (chainName === 'Fantom' || chainName === 'Klaytn' || chainName === 'Oasis') { + return new VAAWatcher(network, chainName, 'latest'); } else if (chainName === 'Algorand') { return new AlgorandWatcher(network); - } else if (chainName === 'Moonbeam') { - return new MoonbeamWatcher(network); - } else if (chainName === 'Arbitrum') { - return new ArbitrumWatcher(network); } else if (chainName === 'Aptos') { return new AptosWatcher(network); - } else if (chainName === 'Near') { - return new NearArchiveWatcher(network); } else if (chainName === 'Injective') { return new InjectiveExplorerWatcher(network); + } else if (chainName === 'Near') { + return new NearArchiveWatcher(network); } else if (chainName === 'Sei') { return new SeiExplorerWatcher(network); + } else if (chainName === 'Sui') { + return new SuiWatcher(network); } else if (chainName === 'Terra') { return new TerraExplorerWatcher(network, chainName); } else if (chainName === 'Terra2') { return new CosmwasmWatcher(network, chainName); - } else if (chainName === 'Xpla') { - return new CosmwasmWatcher(network, chainName); - } else if (chainName === 'Sui') { - return new SuiWatcher(network); } else if (chainName === 'Wormchain') { return new WormchainWatcher(network); + } else if (chainName === 'Xpla') { + return new CosmwasmWatcher(network, chainName); } else if (network === 'Testnet') { // These are testnet only chains - if (chainName === 'Sepolia' || chainName === 'Holesky') { - return new EVMWatcher(network, chainName, 'finalized'); - } else if (chainName === 'ArbitrumSepolia') { - return new ArbitrumWatcher(network); - } else if ( - chainName === 'OptimismSepolia' || + if ( + chainName === 'ArbitrumSepolia' || chainName === 'BaseSepolia' || - chainName === 'PolygonSepolia' + chainName === 'Holesky' || + chainName === 'OptimismSepolia' || + chainName === 'PolygonSepolia' || + chainName === 'Sepolia' ) { - return new EVMWatcher(network, chainName); + return new VAAWatcher(network, chainName); } else { throw new Error( `Attempted to create finalized watcher for unsupported testnet chain ${chainName}` @@ -99,12 +91,14 @@ export function makeFinalizedWatcher(network: Network, chainName: Chain): Watche export function makeFinalizedNTTWatcher(network: Network, chainName: Chain): Watcher { if (network === 'Mainnet') { - if (chainName === 'Ethereum') { - return new NTTWatcher(network, chainName, 'finalized'); - } else if (chainName === 'Fantom' || chainName === 'Base' || chainName === 'Optimism') { + if ( + chainName === 'Arbitrum' || + chainName === 'Base' || + chainName === 'Ethereum' || + chainName === 'Fantom' || + chainName === 'Optimism' + ) { return new NTTWatcher(network, chainName); - } else if (chainName === 'Arbitrum') { - return new NTTArbitrumWatcher(network); } else if (chainName === 'Solana') { return new NTTSolanaWatcher(network); } else { @@ -114,12 +108,14 @@ export function makeFinalizedNTTWatcher(network: Network, chainName: Chain): Wat } } else if (network === 'Testnet') { // These are testnet only chains - if (chainName === 'Sepolia' || chainName === 'Holesky') { - return new NTTWatcher(network, chainName, 'finalized'); - } else if (chainName === 'BaseSepolia' || chainName === 'OptimismSepolia') { + if ( + chainName === 'ArbitrumSepolia' || + chainName === 'BaseSepolia' || + chainName === 'Holesky' || + chainName === 'OptimismSepolia' || + chainName === 'Sepolia' + ) { return new NTTWatcher(network, chainName); - } else if (chainName === 'ArbitrumSepolia') { - return new NTTArbitrumWatcher(network); } else if (chainName === 'Solana') { return new NTTSolanaWatcher(network); } else { @@ -138,7 +134,7 @@ export function makeFinalizedFTWatcher(network: Network, chainName: Chain): Watc if (chainName === 'Solana') { return new FTSolanaWatcher(network); } else if (isFTEVMChain(chainName, network)) { - return new FTEVMWatcher(network, chainName, 'finalized'); + return new FTEVMWatcher(network, chainName); } else { throw new Error( `Attempted to create finalized FT watcher for unsupported chain ${chainName} on ${network}` diff --git a/watcher/src/workers/worker.ts b/watcher/src/workers/worker.ts index 6ceff912..f1a02e68 100644 --- a/watcher/src/workers/worker.ts +++ b/watcher/src/workers/worker.ts @@ -1,7 +1,7 @@ import { initDb } from '../databases/utils'; import { makeFinalizedNTTWatcher, - makeFinalizedWatcher, + makeFinalizedVaaWatcher, makeFinalizedFTWatcher, } from '../watchers/utils'; import { workerData } from 'worker_threads'; @@ -12,7 +12,7 @@ const chain = workerData.chain; const mode = workerData.mode; console.log(`Making watcher for ${network}, ${chain}, ${mode}...`); if (mode === 'vaa') { - makeFinalizedWatcher(network, chain).watch(); + makeFinalizedVaaWatcher(network, chain).watch(); } else if (mode === 'ntt') { makeFinalizedNTTWatcher(network, chain).watch(); } else if (mode === 'ft') {