Skip to content

Commit 393b218

Browse files
committed
watcher: make near archive watcher work
1 parent beb6118 commit 393b218

25 files changed

+260
-86
lines changed

watcher/scripts/backfillArbitrum.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { Chain, contracts } from '@wormhole-foundation/sdk-base';
3030
const watcher = new ArbitrumWatcher('Mainnet');
3131
for (const blockNumber of blockNumbers) {
3232
log.text = `Fetching block ${blockNumber}`;
33-
const vaasByBlock = await watcher.getMessagesForBlocks(blockNumber, blockNumber);
33+
const { vaasByBlock } = await watcher.getMessagesForBlocks(blockNumber, blockNumber);
3434
await db.storeVaasByBlock(chain, vaasByBlock);
3535
}
3636
log.succeed('Uploaded messages to db successfully');

watcher/scripts/locateMessageGaps.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ import { ChainId, Network, toChain, toChainId } from '@wormhole-foundation/sdk-b
115115
while (fromBlock <= rangeEnd && !found) {
116116
const toBlock = Math.min(fromBlock + watcher.maximumBatchSize - 1, rangeEnd);
117117
const messages = await watcher.getMessagesForBlocks(fromBlock, toBlock);
118-
for (const message of Object.entries(messages).filter(([key, value]) => value.length > 0)) {
118+
for (const message of Object.entries(messages.vaasByBlock).filter(
119+
([key, value]) => value.length > 0
120+
)) {
119121
const locatedMessages = message[1].filter((msgKey) => {
120122
const [_transaction, vaaKey] = msgKey.split(':');
121123
const [_chain, msgEmitter, msgSeq] = vaaKey.split('/');

watcher/src/consts.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Mode } from '@wormhole-foundation/wormhole-monitor-common';
33
import { AxiosRequestConfig } from 'axios';
44

55
export const TIMEOUT = 0.5 * 1000;
6-
export const HB_INTERVAL = 5 * 60 * 1000; // 5 Minutes
6+
export const HB_INTERVAL = 15 * 60 * 1000; // 15 Minutes
77
export type WorkerData = {
88
network: Network;
99
chain: Chain;

watcher/src/watchers/AlgorandWatcher.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,10 @@ export class AlgorandWatcher extends Watcher {
9898
return messages;
9999
}
100100

101-
async getMessagesForBlocks(fromBlock: number, toBlock: number): Promise<VaasByBlock> {
101+
async getMessagesForBlocks(
102+
fromBlock: number,
103+
toBlock: number
104+
): Promise<{ vaasByBlock: VaasByBlock; optionalBlockHeight?: number }> {
102105
const txIds = await this.getApplicationLogTransactionIds(fromBlock, toBlock);
103106
const transactions = [];
104107
for (const txId of txIds) {
@@ -124,6 +127,6 @@ export class AlgorandWatcher extends Watcher {
124127
if (!vaasByBlock[toBlockKey]) {
125128
vaasByBlock[toBlockKey] = [];
126129
}
127-
return vaasByBlock;
130+
return { vaasByBlock };
128131
}
129132
}

watcher/src/watchers/AptosWatcher.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ export class AptosWatcher extends Watcher {
4040
);
4141
}
4242

43-
async getMessagesForBlocks(fromSequence: number, toSequence: number): Promise<VaasByBlock> {
43+
async getMessagesForBlocks(
44+
fromSequence: number,
45+
toSequence: number
46+
): Promise<{ vaasByBlock: VaasByBlock; optionalBlockHeight?: number }> {
4447
const limit = toSequence - fromSequence + 1;
4548
const events: AptosEvent[] = (await this.client.getEventsByEventHandle(
4649
this.coreBridgeAddress,
@@ -63,7 +66,7 @@ export class AptosWatcher extends Watcher {
6366
vaasByBlock[blockKey] = [...(vaasByBlock[blockKey] ?? []), vaaKey];
6467
})
6568
);
66-
return vaasByBlock;
69+
return { vaasByBlock };
6770
}
6871

6972
isValidBlockKey(key: string) {

watcher/src/watchers/CosmwasmWatcher.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ export class CosmwasmWatcher extends Watcher {
5555
throw new Error(`Unable to parse result of ${this.latestBlockTag} on ${this.rpc}`);
5656
}
5757

58-
async getMessagesForBlocks(fromBlock: number, toBlock: number): Promise<VaasByBlock> {
58+
async getMessagesForBlocks(
59+
fromBlock: number,
60+
toBlock: number
61+
): Promise<{ vaasByBlock: VaasByBlock; optionalBlockHeight?: number }> {
5962
const address = contracts.coreBridge.get(this.network, this.chain);
6063
if (!address) {
6164
throw new Error(`Core contract not defined for ${this.chain}`);
@@ -153,7 +156,7 @@ export class CosmwasmWatcher extends Watcher {
153156
}
154157
}
155158
}
156-
return vaasByBlock;
159+
return { vaasByBlock };
157160
}
158161
}
159162

watcher/src/watchers/EVMWatcher.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export class EVMWatcher extends Watcher {
4040
this.lastTimestamp = 0;
4141
this.latestFinalizedBlockNumber = 0;
4242
this.finalizedBlockTag = finalizedBlockTag;
43+
// Special cases for batch size
4344
if (chain === 'Acala' || chain === 'Karura' || chain === 'Berachain') {
4445
this.maximumBatchSize = 50;
4546
} else if (
@@ -55,6 +56,10 @@ export class EVMWatcher extends Watcher {
5556
) {
5657
this.maximumBatchSize = 10;
5758
}
59+
// Special cases for watch loop delay
60+
if (chain === 'Berachain') {
61+
this.watchLoopDelay = 1000;
62+
}
5863
}
5964

6065
async getBlock(blockNumberOrTag: number | BlockTag): Promise<Block> {
@@ -221,7 +226,10 @@ export class EVMWatcher extends Watcher {
221226
return block.number;
222227
}
223228

224-
async getMessagesForBlocks(fromBlock: number, toBlock: number): Promise<VaasByBlock> {
229+
async getMessagesForBlocks(
230+
fromBlock: number,
231+
toBlock: number
232+
): Promise<{ vaasByBlock: VaasByBlock; optionalBlockHeight?: number }> {
225233
const address = contracts.coreBridge.get(this.network, this.chain);
226234
if (!address) {
227235
throw new Error(`Core contract not defined for ${this.chain} on ${this.network}!`);
@@ -252,6 +260,6 @@ export class EVMWatcher extends Watcher {
252260
const blockKey = makeBlockKey(blockNumber.toString(), timestampsByBlock[blockNumber]);
253261
vaasByBlock[blockKey] = [...(vaasByBlock[blockKey] || []), vaaKey];
254262
}
255-
return vaasByBlock;
263+
return { vaasByBlock };
256264
}
257265
}

watcher/src/watchers/InjectiveExplorerWatcher.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ export class InjectiveExplorerWatcher extends Watcher {
4747
// should be core, but the explorer doesn't support it yet
4848
// use "to": as the pagination key
4949
// compare block height ("block_number":) with what is passed in.
50-
async getMessagesForBlocks(fromBlock: number, toBlock: number): Promise<VaasByBlock> {
50+
async getMessagesForBlocks(
51+
fromBlock: number,
52+
toBlock: number
53+
): Promise<{ vaasByBlock: VaasByBlock; optionalBlockHeight?: number }> {
5154
const coreAddress = contracts.coreBridge.get(this.network, this.chain);
5255
const address = contracts.tokenBridge.get(this.network, this.chain);
5356
if (!coreAddress || !address) {
@@ -169,7 +172,7 @@ export class InjectiveExplorerWatcher extends Watcher {
169172
);
170173
vaasByBlock[blockKey] = [];
171174
}
172-
return vaasByBlock;
175+
return { vaasByBlock };
173176
}
174177
}
175178

watcher/src/watchers/NearArchiveWatcher.ts

+107-13
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,29 @@
11
import { decode } from 'bs58';
22
import { Provider } from 'near-api-js/lib/providers';
3-
import { BlockResult } from 'near-api-js/lib/providers/provider';
3+
import { BlockResult, ExecutionStatus } from 'near-api-js/lib/providers/provider';
44
import { z } from 'zod';
55
import { VaasByBlock } from '../databases/types';
6-
import { makeBlockKey } from '../databases/utils';
6+
import { makeBlockKey, makeVaaKey } from '../databases/utils';
77
import {
88
fetchBlockByBlockId,
9-
getMessagesFromBlockResults,
109
getNearProvider,
1110
getTimestampByBlock,
11+
isWormholePublishEventLog,
1212
} from '../utils/near';
1313
import { Watcher } from './Watcher';
1414
import { assertEnvironmentVariable, sleep } from '@wormhole-foundation/wormhole-monitor-common';
1515
import { Network, contracts } from '@wormhole-foundation/sdk-base';
1616
import axios from 'axios';
17-
import { AXIOS_CONFIG_JSON } from '../consts';
17+
import { AXIOS_CONFIG_JSON, HB_INTERVAL } from '../consts';
18+
import { EventLog } from 'src/types/near';
1819

1920
export class NearArchiveWatcher extends Watcher {
2021
provider: Provider | null = null;
2122

2223
constructor(network: Network) {
2324
super(network, 'Near');
24-
this.maximumBatchSize = 1000;
25+
this.maximumBatchSize = 1_000_000;
26+
this.watchLoopDelay = 60 * 60 * 1000; // 1 hour
2527
}
2628

2729
async getFinalizedBlockNumber(): Promise<number> {
@@ -37,7 +39,11 @@ export class NearArchiveWatcher extends Watcher {
3739
}
3840
}
3941

40-
async getMessagesForBlocks(fromBlock: number, toBlock: number): Promise<VaasByBlock> {
42+
async getMessagesForBlocks(
43+
fromBlock: number,
44+
toBlock: number
45+
): Promise<{ vaasByBlock: VaasByBlock; optionalBlockHeight?: number }> {
46+
const quittingTimestamp = Date.now() + HB_INTERVAL * 0.75;
4147
const origFromBlock = fromBlock;
4248
const origToBlock = toBlock;
4349
this.logger.info(`fetching info for blocks ${origFromBlock} to ${origToBlock}`);
@@ -114,21 +120,26 @@ export class NearArchiveWatcher extends Watcher {
114120
}
115121

116122
this.logger.info(`Fetched ${blocks.length} blocks`);
117-
const vaasByBlock: VaasByBlock = await getMessagesFromBlockResults(
123+
const response: ConstrainedResponse = await this.getMessagesFromBlockResultsConstrained(
118124
this.network,
119125
provider,
120126
blocks,
121-
true
127+
quittingTimestamp
122128
);
129+
// This is the case where there are no transactions in the time window.
130+
if (response.lastBlockHeight === 0) {
131+
response.lastBlockHeight = toBlock;
132+
}
133+
const lastBlockInfo = await fetchBlockByBlockId(provider, response.lastBlockHeight);
123134
// Make a block for the to_block, if it isn't already there
124135
const blockKey = makeBlockKey(
125-
toBlockInfo.header.height.toString(),
126-
new Date(toBlockInfo.header.timestamp / 1_000_000).toISOString()
136+
response.lastBlockHeight.toString(),
137+
new Date(lastBlockInfo.header.timestamp / 1_000_000).toISOString()
127138
);
128-
if (!vaasByBlock[blockKey]) {
129-
vaasByBlock[blockKey] = [];
139+
if (!response.vaasByBlock[blockKey]) {
140+
response.vaasByBlock[blockKey] = [];
130141
}
131-
return vaasByBlock;
142+
return response;
132143
}
133144

134145
async getProvider(): Promise<Provider> {
@@ -191,7 +202,90 @@ export class NearArchiveWatcher extends Watcher {
191202
}
192203
return txs.reverse();
193204
}
205+
206+
async getMessagesFromBlockResultsConstrained(
207+
network: Network,
208+
provider: Provider,
209+
blocks: BlockResult[],
210+
quittingTime: number
211+
): Promise<ConstrainedResponse> {
212+
const vaasByBlock: VaasByBlock = {};
213+
let lastBlockHeight = 0;
214+
let prevLastBlockHeight = 0;
215+
this.logger.debug(`Fetching messages from ${blocks.length} blocks...`);
216+
try {
217+
for (let i = 0; i < blocks.length; i++) {
218+
this.logger.debug(`Fetching messages from block ${i + 1}/${blocks.length}...`);
219+
const { height, timestamp } = blocks[i].header;
220+
prevLastBlockHeight = lastBlockHeight;
221+
lastBlockHeight = height;
222+
const blockKey = makeBlockKey(
223+
height.toString(),
224+
new Date(timestamp / 1_000_000).toISOString()
225+
);
226+
let localVaasByBlock: VaasByBlock = {};
227+
localVaasByBlock[blockKey] = [];
228+
229+
const chunks = [];
230+
this.logger.debug('attempting to fetch chunks');
231+
for (const chunk of blocks[i].chunks) {
232+
chunks.push(await provider.chunk(chunk.chunk_hash));
233+
}
234+
235+
const transactions = chunks.flatMap(({ transactions }) => transactions);
236+
const coreBridge = contracts.coreBridge.get(network, 'Near');
237+
if (!coreBridge) {
238+
throw new Error('Unable to get contract address for Near');
239+
}
240+
this.logger.debug(`attempting to fetch ${transactions.length} transactions`);
241+
const totTx = transactions.length;
242+
let txCount = 1;
243+
for (const tx of transactions) {
244+
this.logger.debug(`fetching transaction ${txCount}/${totTx}`);
245+
txCount++;
246+
const outcome = await provider.txStatus(tx.hash, coreBridge);
247+
const logs = outcome.receipts_outcome
248+
.filter(
249+
({ outcome }) =>
250+
(outcome as any).executor_id === coreBridge &&
251+
(outcome.status as ExecutionStatus).SuccessValue
252+
)
253+
.flatMap(({ outcome }) => outcome.logs)
254+
.filter((log) => log.startsWith('EVENT_JSON:')) // https://nomicon.io/Standards/EventsFormat
255+
.map((log) => JSON.parse(log.slice(11)) as EventLog)
256+
.filter(isWormholePublishEventLog);
257+
for (const log of logs) {
258+
const vaaKey = makeVaaKey(tx.hash, 'Near', log.emitter, log.seq.toString());
259+
localVaasByBlock[blockKey] = [...localVaasByBlock[blockKey], vaaKey];
260+
}
261+
}
262+
this.logger.debug(
263+
`Fetched ${localVaasByBlock[blockKey].length} messages from block ${blockKey}`
264+
);
265+
vaasByBlock[blockKey] = localVaasByBlock[blockKey];
266+
if (Date.now() >= quittingTime) {
267+
this.logger.warn(`Quitting early due to time constraint.`);
268+
break;
269+
}
270+
}
271+
} catch (e) {
272+
this.logger.error(`Near block getMessagesFromBlockResultsConstrained error: ${e}`);
273+
this.logger.warn(`Quitting early due to error.`);
274+
lastBlockHeight = prevLastBlockHeight;
275+
}
276+
277+
const numMessages = Object.values(vaasByBlock).flat().length;
278+
this.logger.debug(`Fetched ${numMessages} messages from ${blocks.length} blocks`);
279+
280+
return { vaasByBlock, lastBlockHeight };
281+
}
194282
}
283+
284+
type ConstrainedResponse = {
285+
vaasByBlock: VaasByBlock;
286+
lastBlockHeight: number;
287+
};
288+
195289
type GetTransactionsByAccountIdResponse = {
196290
txns: NearTxn[];
197291
};

watcher/src/watchers/NearWatcher.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ export class NearWatcher extends Watcher {
2222
return block.header.height;
2323
}
2424

25-
async getMessagesForBlocks(fromBlock: number, toBlock: number): Promise<VaasByBlock> {
25+
async getMessagesForBlocks(
26+
fromBlock: number,
27+
toBlock: number
28+
): Promise<{ vaasByBlock: VaasByBlock; optionalBlockHeight?: number }> {
2629
// assume toBlock was retrieved from getFinalizedBlockNumber and is finalized
2730
this.logger.info(`fetching info for blocks ${fromBlock} to ${toBlock}`);
2831
const provider = await this.getProvider();
@@ -48,7 +51,9 @@ export class NearWatcher extends Watcher {
4851
}
4952
}
5053

51-
return getMessagesFromBlockResults(this.network, provider, blocks);
54+
return {
55+
vaasByBlock: await getMessagesFromBlockResults(this.network, provider, blocks),
56+
};
5257
}
5358

5459
async getProvider(): Promise<Provider> {

watcher/src/watchers/SeiExplorerWatcher.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,10 @@ export class SeiExplorerWatcher extends CosmwasmWatcher {
107107

108108
// retrieve blocks for core contract
109109
// compare block height with what is passed in
110-
async getMessagesForBlocks(fromBlock: number, toBlock: number): Promise<VaasByBlock> {
110+
async getMessagesForBlocks(
111+
fromBlock: number,
112+
toBlock: number
113+
): Promise<{ vaasByBlock: VaasByBlock; optionalBlockHeight?: number }> {
111114
const address = contracts.coreBridge.get(this.network, this.chain);
112115
if (!address) {
113116
throw new Error(`Core contract not defined for ${this.chain}`);
@@ -237,6 +240,6 @@ export class SeiExplorerWatcher extends CosmwasmWatcher {
237240
}
238241
// NOTE: this does not set an empty entry for the latest block since we don't know if the graphql response
239242
// is synced with the block height. Therefore, the latest block will only update when a new transaction appears.
240-
return vaasByBlock;
243+
return { vaasByBlock };
241244
}
242245
}

watcher/src/watchers/SolanaWatcher.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ export class SolanaWatcher extends Watcher {
8181
return block;
8282
}
8383

84-
async getMessagesForBlocks(fromSlot: number, toSlot: number): Promise<VaasByBlock> {
84+
async getMessagesForBlocks(
85+
fromSlot: number,
86+
toSlot: number
87+
): Promise<{ vaasByBlock: VaasByBlock; optionalBlockHeight?: number }> {
8588
// in the rare case of maximumBatchSize skipped blocks in a row,
8689
// you might hit this error due to the recursion below
8790
if (fromSlot > toSlot) throw new Error('solana: invalid block range');
@@ -251,7 +254,7 @@ export class SolanaWatcher extends Watcher {
251254
toSlot.toString(),
252255
new Date(toBlock.blockTime! * 1000).toISOString()
253256
);
254-
return { [lastBlockKey]: [], ...vaasByBlock };
257+
return { vaasByBlock: { [lastBlockKey]: [], ...vaasByBlock } };
255258
}
256259

257260
isValidVaaKey(key: string) {

0 commit comments

Comments
 (0)