feat: Add solana vaa logs support (#656)
This commit is contained in:
parent
503a9da5d3
commit
23d349c9b2
|
@ -47,13 +47,13 @@ export const evmChains: EVMChainName[] = [
|
|||
|
||||
export const supportedChains: ChainName[] = [
|
||||
...evmChains,
|
||||
'algorand',
|
||||
'aptos',
|
||||
'injective',
|
||||
'near',
|
||||
// 'algorand',
|
||||
// 'aptos',
|
||||
// 'injective',
|
||||
// 'near',
|
||||
'solana',
|
||||
'sui',
|
||||
'terra',
|
||||
'terra2',
|
||||
'xpla',
|
||||
// 'sui',
|
||||
// 'terra',
|
||||
// 'terra2',
|
||||
// 'xpla',
|
||||
];
|
||||
|
|
|
@ -7,13 +7,13 @@ import { VaaLog } from './types';
|
|||
const ENCODING = 'utf8';
|
||||
|
||||
export default class JsonDB extends BaseDB {
|
||||
db: {} | null = null;
|
||||
db: VaaLog[] = [];
|
||||
dbFile: string;
|
||||
dbLastBlockFile: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.db = {};
|
||||
this.db = [];
|
||||
this.lastBlockByChain = {};
|
||||
this.dbFile = env.JSON_DB_FILE;
|
||||
this.dbLastBlockFile = env.JSON_LAST_BLOCK_FILE;
|
||||
|
@ -22,11 +22,11 @@ export default class JsonDB extends BaseDB {
|
|||
async connect(): Promise<void> {
|
||||
try {
|
||||
const rawDb = readFileSync(this.dbFile, ENCODING);
|
||||
this.db = rawDb ? JSON.parse(rawDb) : {};
|
||||
this.db = rawDb ? JSON.parse(rawDb) : [];
|
||||
console.log('---CONNECTED TO JsonDB---');
|
||||
} catch (e) {
|
||||
this.logger.warn(`${this.dbFile} does not exists, creating new file`);
|
||||
this.db = {};
|
||||
this.db = [];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,7 @@ export default class JsonDB extends BaseDB {
|
|||
}
|
||||
|
||||
override async storeVaaLogs(chain: ChainName, vaaLogs: VaaLog[]): Promise<void> {
|
||||
this.db = [{ ...this.db, ...vaaLogs }];
|
||||
this.db = [...this.db, ...vaaLogs];
|
||||
writeFileSync(this.dbFile, JSON.stringify(this.db, null, 2), ENCODING);
|
||||
}
|
||||
|
||||
|
|
|
@ -13,16 +13,17 @@ export interface DBImplementation {
|
|||
storeLatestProcessBlock(chain: ChainName, lastBlock: number): Promise<void>;
|
||||
}
|
||||
|
||||
export type VaasByBlock = { [blockInfo: string]: string[] };
|
||||
export interface VaaLog {
|
||||
vaaId: string;
|
||||
chainId: number;
|
||||
chainName: string;
|
||||
emitter: string;
|
||||
sequence: number;
|
||||
txHash: string;
|
||||
sender: string;
|
||||
sequence: number | string;
|
||||
txHash: string | null;
|
||||
sender: string | null;
|
||||
payload: any;
|
||||
blockNumber: number;
|
||||
blockNumber: number | string | null;
|
||||
indexedAt?: string | number;
|
||||
updatedAt?: string | number;
|
||||
createdAt?: string | number;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { MAX_UINT_64, padUint16, padUint64 } from '../common';
|
|||
import JsonDB from './JsonDB';
|
||||
import MongoDB from './MongoDB';
|
||||
import { env } from '../config';
|
||||
import { DBOptionTypes } from './types';
|
||||
import { DBOptionTypes, VaaLog } from './types';
|
||||
|
||||
// Bigtable Message ID format
|
||||
// chain/MAX_UINT64-block/emitter/sequence
|
||||
|
@ -49,3 +49,30 @@ export const makeVaaKey = (
|
|||
emitter: string,
|
||||
seq: string,
|
||||
): string => `${transactionHash}:${coalesceChainId(chain)}/${emitter}/${seq}`;
|
||||
|
||||
export const makeVaaLog = ({
|
||||
chainName,
|
||||
emitter,
|
||||
sequence,
|
||||
txHash,
|
||||
sender,
|
||||
blockNumber,
|
||||
payload,
|
||||
}: Omit<VaaLog, 'vaaId' | 'chainId'>): VaaLog => {
|
||||
const chainId = coalesceChainId(chainName as ChainName);
|
||||
|
||||
return {
|
||||
vaaId: `${chainId}/${emitter}/${sequence}`,
|
||||
chainId: chainId,
|
||||
chainName,
|
||||
emitter,
|
||||
sequence,
|
||||
txHash,
|
||||
sender,
|
||||
payload,
|
||||
blockNumber,
|
||||
indexedAt: new Date().getTime(),
|
||||
updatedAt: new Date().getTime(),
|
||||
createdAt: new Date().getTime(),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@ import { getSNS } from './services/SNS/utils';
|
|||
import { makeFinalizedWatcher } from './watchers/utils';
|
||||
import { InfrastructureController } from './infrastructure/infrastructure.controller';
|
||||
import { createServer } from './builder/server';
|
||||
import { env, evmChains } from './config';
|
||||
import { env, supportedChains } from './config';
|
||||
import { DBOptionTypes } from './databases/types';
|
||||
import { SNSOptionTypes } from './services/SNS/types';
|
||||
class EventWatcher {
|
||||
|
@ -36,13 +36,18 @@ class EventWatcher {
|
|||
async run() {
|
||||
await this.db.start();
|
||||
|
||||
// for (const chain of supportedChains) {
|
||||
for (const chain of evmChains) {
|
||||
for (const chain of supportedChains) {
|
||||
const watcher = makeFinalizedWatcher(chain);
|
||||
watcher.setDB(this.db);
|
||||
watcher.setServices(this.sns);
|
||||
watcher.watch();
|
||||
}
|
||||
|
||||
// TEST
|
||||
// const watcher = makeFinalizedWatcher('solana');
|
||||
// watcher.setDB(this.db);
|
||||
// watcher.setServices(this.sns);
|
||||
// watcher.watch();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { ChainName } from '@certusone/wormhole-sdk/lib/cjs/utils/consts';
|
|||
import { INITIAL_DEPLOYMENT_BLOCK_BY_CHAIN, sleep } from '../common';
|
||||
import { z } from 'zod';
|
||||
import { TIMEOUT } from '../consts';
|
||||
import { DBOptionTypes, VaaLog } from '../databases/types';
|
||||
import { DBOptionTypes, VaaLog, VaasByBlock } from '../databases/types';
|
||||
import { getLogger, WormholeLogger } from '../utils/logger';
|
||||
import { SNSInput, SNSOptionTypes } from '../services/SNS/types';
|
||||
import { WatcherImplementation } from './types';
|
||||
|
@ -25,6 +25,10 @@ abstract class BaseWatcher implements WatcherImplementation {
|
|||
this.sns = sns;
|
||||
}
|
||||
|
||||
getMessagesForBlocks(fromBlock: number, toBlock: number): Promise<VaasByBlock> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
abstract getFinalizedBlockNumber(): Promise<number>;
|
||||
abstract getVaaLogs(fromBlock: number, toBlock: number): Promise<VaaLog[]>;
|
||||
|
||||
|
|
|
@ -8,8 +8,9 @@ import { Log } from '@ethersproject/abstract-provider';
|
|||
import axios from 'axios';
|
||||
import { BigNumber } from 'ethers';
|
||||
import { AXIOS_CONFIG_JSON, RPCS_BY_CHAIN } from '../consts';
|
||||
import { VaaLog } from '../databases/types';
|
||||
import { VaaLog, VaasByBlock } from '../databases/types';
|
||||
import BaseWatcher from './BaseWatcher';
|
||||
import { makeBlockKey, makeVaaKey, makeVaaLog } from '../databases/utils';
|
||||
|
||||
// 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
|
||||
|
@ -207,6 +208,36 @@ export class EVMWatcher extends BaseWatcher {
|
|||
return block.number;
|
||||
}
|
||||
|
||||
override async getMessagesForBlocks(fromBlock: number, toBlock: number): Promise<VaasByBlock> {
|
||||
const address = CONTRACTS.MAINNET[this.chain].core;
|
||||
if (!address) {
|
||||
throw new Error(`Core contract not defined for ${this.chain}`);
|
||||
}
|
||||
const logs = await this.getLogs(fromBlock, toBlock, address, [LOG_MESSAGE_PUBLISHED_TOPIC]);
|
||||
const timestampsByBlock: { [block: number]: string } = {};
|
||||
// fetch timestamps for each block
|
||||
const vaasByBlock: VaasByBlock = {};
|
||||
this.logger.info(`fetching info for blocks ${fromBlock} to ${toBlock}`);
|
||||
const blocks = await this.getBlocks(fromBlock, toBlock);
|
||||
for (const block of blocks) {
|
||||
const timestamp = new Date(block.timestamp * 1000).toISOString();
|
||||
timestampsByBlock[block.number] = timestamp;
|
||||
vaasByBlock[makeBlockKey(block.number.toString(), timestamp)] = [];
|
||||
}
|
||||
this.logger.info(`processing ${logs.length} logs`);
|
||||
for (const log of logs) {
|
||||
const blockNumber = log.blockNumber;
|
||||
const emitter = log.topics[1].slice(2);
|
||||
const {
|
||||
args: { sequence },
|
||||
} = wormholeInterface.parseLog(log);
|
||||
const vaaKey = makeVaaKey(log.transactionHash, this.chain, emitter, sequence.toString());
|
||||
const blockKey = makeBlockKey(blockNumber.toString(), timestampsByBlock[blockNumber]);
|
||||
vaasByBlock[blockKey] = [...(vaasByBlock[blockKey] || []), vaaKey];
|
||||
}
|
||||
return vaasByBlock;
|
||||
}
|
||||
|
||||
override async getVaaLogs(fromBlock: number, toBlock: number): Promise<VaaLog[]> {
|
||||
const vaaLogs: VaaLog[] = [];
|
||||
const address = CONTRACTS.MAINNET[this.chain].core;
|
||||
|
@ -224,26 +255,21 @@ export class EVMWatcher extends BaseWatcher {
|
|||
|
||||
const { args } = wormholeInterface.parseLog(log);
|
||||
const { sequence, sender, payload } = args || {};
|
||||
const chainName = this.chain;
|
||||
const blockNumber = log.blockNumber;
|
||||
const chainName = this.chain;
|
||||
const emitter = log.topics[1].slice(2);
|
||||
const chainId = coalesceChainId(this.chain);
|
||||
const vaaId = `${chainId}/${emitter}/${sequence.toString()}`;
|
||||
const parseSequence = sequence.toString();
|
||||
const txHash = log.transactionHash;
|
||||
|
||||
const vaaLog: VaaLog = {
|
||||
vaaId,
|
||||
const vaaLog = makeVaaLog({
|
||||
chainName,
|
||||
chainId,
|
||||
emitter,
|
||||
sequence: sequence.toString(),
|
||||
txHash: log.transactionHash,
|
||||
sequence: parseSequence,
|
||||
txHash,
|
||||
sender,
|
||||
payload,
|
||||
blockNumber,
|
||||
indexedAt: new Date().getTime(),
|
||||
updatedAt: new Date().getTime(),
|
||||
createdAt: new Date().getTime(),
|
||||
};
|
||||
payload,
|
||||
});
|
||||
|
||||
vaaLogs.push(vaaLog);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { getPostedMessage } from '@certusone/wormhole-sdk/lib/cjs/solana/wormhole';
|
||||
import { CONTRACTS } from '@certusone/wormhole-sdk/lib/cjs/utils/consts';
|
||||
import {
|
||||
coalesceChainId,
|
||||
CONTRACTS,
|
||||
ChainName,
|
||||
} from '@certusone/wormhole-sdk/lib/cjs/utils/consts';
|
||||
import {
|
||||
Commitment,
|
||||
ConfirmedSignatureInfo,
|
||||
|
@ -11,8 +15,8 @@ import {
|
|||
import { decode } from 'bs58';
|
||||
import { z } from 'zod';
|
||||
import { RPCS_BY_CHAIN } from '../consts';
|
||||
import { VaaLog } from '../databases/types';
|
||||
import { makeBlockKey, makeVaaKey } from '../databases/utils';
|
||||
import { VaaLog, VaasByBlock } from '../databases/types';
|
||||
import { makeBlockKey, makeVaaKey, makeVaaLog } from '../databases/utils';
|
||||
import { isLegacyMessage, normalizeCompileInstruction } from '../utils/solana';
|
||||
import BaseWatcher from './BaseWatcher';
|
||||
|
||||
|
@ -41,143 +45,284 @@ export class SolanaWatcher extends BaseWatcher {
|
|||
return connection.getSlot();
|
||||
}
|
||||
|
||||
// async getMessagesForBlocks(fromSlot: number, toSlot: number): Promise<VaasByBlock> {
|
||||
// const connection = new Connection(this.rpc, COMMITMENT);
|
||||
// // in the rare case of maximumBatchSize skipped blocks in a row,
|
||||
// // you might hit this error due to the recursion below
|
||||
// if (fromSlot > toSlot) throw new Error('solana: invalid block range');
|
||||
// this.logger.info(`fetching info for blocks ${fromSlot} to ${toSlot}`);
|
||||
// const vaasByBlock: VaasByBlock = {};
|
||||
override async getMessagesForBlocks(fromSlot: number, toSlot: number): Promise<VaasByBlock> {
|
||||
const connection = new Connection(this.rpc, COMMITMENT);
|
||||
// in the rare case of maximumBatchSize skipped blocks in a row,
|
||||
// you might hit this error due to the recursion below
|
||||
if (fromSlot > toSlot) throw new Error('solana: invalid block range');
|
||||
this.logger.info(`fetching info for blocks ${fromSlot} to ${toSlot}`);
|
||||
const vaasByBlock: VaasByBlock = {};
|
||||
|
||||
// // identify block range by fetching signatures of the first and last transactions
|
||||
// // getSignaturesForAddress walks backwards so fromSignature occurs after toSignature
|
||||
// let toBlock: VersionedBlockResponse | null = null;
|
||||
// try {
|
||||
// toBlock = await connection.getBlock(toSlot, { maxSupportedTransactionVersion: 0 });
|
||||
// } catch (e) {
|
||||
// if (e instanceof SolanaJSONRPCError && (e.code === -32007 || e.code === -32009)) {
|
||||
// // failed to get confirmed block: slot was skipped or missing in long-term storage
|
||||
// return this.getMessagesForBlocks(fromSlot, toSlot - 1);
|
||||
// } else {
|
||||
// throw e;
|
||||
// }
|
||||
// }
|
||||
// if (!toBlock || !toBlock.blockTime || toBlock.transactions.length === 0) {
|
||||
// return this.getMessagesForBlocks(fromSlot, toSlot - 1);
|
||||
// }
|
||||
// const fromSignature =
|
||||
// toBlock.transactions[toBlock.transactions.length - 1].transaction.signatures[0];
|
||||
// identify block range by fetching signatures of the first and last transactions
|
||||
// getSignaturesForAddress walks backwards so fromSignature occurs after toSignature
|
||||
let toBlock: VersionedBlockResponse | null = null;
|
||||
try {
|
||||
toBlock = await connection.getBlock(toSlot, { maxSupportedTransactionVersion: 0 });
|
||||
} catch (e) {
|
||||
if (e instanceof SolanaJSONRPCError && (e.code === -32007 || e.code === -32009)) {
|
||||
// failed to get confirmed block: slot was skipped or missing in long-term storage
|
||||
return this.getMessagesForBlocks(fromSlot, toSlot - 1);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (!toBlock || !toBlock.blockTime || toBlock.transactions.length === 0) {
|
||||
return this.getMessagesForBlocks(fromSlot, toSlot - 1);
|
||||
}
|
||||
const fromSignature =
|
||||
toBlock.transactions[toBlock.transactions.length - 1].transaction.signatures[0];
|
||||
|
||||
// let fromBlock: VersionedBlockResponse | null = null;
|
||||
// try {
|
||||
// fromBlock = await connection.getBlock(fromSlot, { maxSupportedTransactionVersion: 0 });
|
||||
// } catch (e) {
|
||||
// if (e instanceof SolanaJSONRPCError && (e.code === -32007 || e.code === -32009)) {
|
||||
// // failed to get confirmed block: slot was skipped or missing in long-term storage
|
||||
// return this.getMessagesForBlocks(fromSlot + 1, toSlot);
|
||||
// } else {
|
||||
// throw e;
|
||||
// }
|
||||
// }
|
||||
// if (!fromBlock || !fromBlock.blockTime || fromBlock.transactions.length === 0) {
|
||||
// return this.getMessagesForBlocks(fromSlot + 1, toSlot);
|
||||
// }
|
||||
// const toSignature = fromBlock.transactions[0].transaction.signatures[0];
|
||||
let fromBlock: VersionedBlockResponse | null = null;
|
||||
try {
|
||||
fromBlock = await connection.getBlock(fromSlot, { maxSupportedTransactionVersion: 0 });
|
||||
} catch (e) {
|
||||
if (e instanceof SolanaJSONRPCError && (e.code === -32007 || e.code === -32009)) {
|
||||
// failed to get confirmed block: slot was skipped or missing in long-term storage
|
||||
return this.getMessagesForBlocks(fromSlot + 1, toSlot);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (!fromBlock || !fromBlock.blockTime || fromBlock.transactions.length === 0) {
|
||||
return this.getMessagesForBlocks(fromSlot + 1, toSlot);
|
||||
}
|
||||
const toSignature = fromBlock.transactions[0].transaction.signatures[0];
|
||||
|
||||
// // get all core bridge signatures between fromTransaction and toTransaction
|
||||
// let numSignatures = this.getSignaturesLimit;
|
||||
// let currSignature: string | undefined = fromSignature;
|
||||
// while (numSignatures === this.getSignaturesLimit) {
|
||||
// const signatures: ConfirmedSignatureInfo[] = await connection.getSignaturesForAddress(
|
||||
// new PublicKey(WORMHOLE_PROGRAM_ID),
|
||||
// {
|
||||
// before: currSignature,
|
||||
// until: toSignature,
|
||||
// limit: this.getSignaturesLimit,
|
||||
// }
|
||||
// get all core bridge signatures between fromTransaction and toTransaction
|
||||
let numSignatures = this.getSignaturesLimit;
|
||||
let currSignature: string | undefined = fromSignature;
|
||||
while (numSignatures === this.getSignaturesLimit) {
|
||||
const signatures: ConfirmedSignatureInfo[] = await connection.getSignaturesForAddress(
|
||||
new PublicKey(WORMHOLE_PROGRAM_ID),
|
||||
{
|
||||
before: currSignature,
|
||||
until: toSignature,
|
||||
limit: this.getSignaturesLimit,
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.info(`processing ${signatures.length} transactions`);
|
||||
|
||||
// In order to determine if a transaction has a Wormhole message, we normalize and iterate
|
||||
// through all instructions in the transaction. Only PostMessage instructions are relevant
|
||||
// when looking for messages. PostMessageUnreliable instructions are ignored because there
|
||||
// are no data availability guarantees (ie the associated message accounts may have been
|
||||
// reused, overwriting previous data). Then, the message account is the account given by
|
||||
// the second index in the instruction's account key indices. From here, we can fetch the
|
||||
// message data from the account and parse out the emitter and sequence.
|
||||
const results = await connection.getTransactions(
|
||||
signatures.map((s) => s.signature),
|
||||
{
|
||||
maxSupportedTransactionVersion: 0,
|
||||
},
|
||||
);
|
||||
if (results.length !== signatures.length) {
|
||||
throw new Error(`solana: failed to fetch tx for signatures`);
|
||||
}
|
||||
for (const res of results) {
|
||||
if (res?.meta?.err) {
|
||||
// skip errored txs
|
||||
continue;
|
||||
}
|
||||
if (!res || !res.blockTime) {
|
||||
throw new Error(
|
||||
`solana: failed to fetch tx for signature ${
|
||||
res?.transaction.signatures[0] || 'unknown'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
const message = res.transaction.message;
|
||||
const accountKeys = isLegacyMessage(message)
|
||||
? message.accountKeys
|
||||
: message.staticAccountKeys;
|
||||
const programIdIndex = accountKeys.findIndex((i) => i.toBase58() === WORMHOLE_PROGRAM_ID);
|
||||
const instructions = message.compiledInstructions;
|
||||
const innerInstructions =
|
||||
res.meta?.innerInstructions?.flatMap((i) =>
|
||||
i.instructions.map(normalizeCompileInstruction),
|
||||
) || [];
|
||||
const whInstructions = innerInstructions
|
||||
.concat(instructions)
|
||||
.filter((i) => i.programIdIndex === programIdIndex);
|
||||
for (const instruction of whInstructions) {
|
||||
// skip if not postMessage instruction
|
||||
const instructionId = instruction.data;
|
||||
if (instructionId[0] !== 0x01) continue;
|
||||
|
||||
const accountId = accountKeys[instruction.accountKeyIndexes[1]];
|
||||
const {
|
||||
message: { emitterAddress, sequence },
|
||||
} = await getPostedMessage(connection, accountId.toBase58(), COMMITMENT);
|
||||
const blockKey = makeBlockKey(
|
||||
res.slot.toString(),
|
||||
new Date(res.blockTime * 1000).toISOString(),
|
||||
);
|
||||
const vaaKey = makeVaaKey(
|
||||
res.transaction.signatures[0],
|
||||
this.chain,
|
||||
emitterAddress.toString('hex'),
|
||||
sequence.toString(),
|
||||
);
|
||||
vaasByBlock[blockKey] = [...(vaasByBlock[blockKey] || []), vaaKey];
|
||||
}
|
||||
}
|
||||
|
||||
numSignatures = signatures.length;
|
||||
currSignature = signatures.at(-1)?.signature;
|
||||
}
|
||||
|
||||
// add last block for storeVaasByBlock
|
||||
const lastBlockKey = makeBlockKey(
|
||||
toSlot.toString(),
|
||||
new Date(toBlock.blockTime * 1000).toISOString(),
|
||||
);
|
||||
return { [lastBlockKey]: [], ...vaasByBlock };
|
||||
}
|
||||
|
||||
override async getVaaLogs(fromSlot: number, toSlot: number): Promise<VaaLog[]> {
|
||||
const vaaLogs: VaaLog[] = [];
|
||||
const connection = new Connection(this.rpc, COMMITMENT);
|
||||
// in the rare case of maximumBatchSize skipped blocks in a row,
|
||||
// you might hit this error due to the recursion below
|
||||
if (fromSlot > toSlot) throw new Error('solana: invalid block range');
|
||||
this.logger.info(`fetching info for blocks ${fromSlot} to ${toSlot}`);
|
||||
|
||||
// identify block range by fetching signatures of the first and last transactions
|
||||
// getSignaturesForAddress walks backwards so fromSignature occurs after toSignature
|
||||
let toBlock: VersionedBlockResponse | null = null;
|
||||
try {
|
||||
toBlock = await connection.getBlock(toSlot, { maxSupportedTransactionVersion: 0 });
|
||||
} catch (e) {
|
||||
if (e instanceof SolanaJSONRPCError && (e.code === -32007 || e.code === -32009)) {
|
||||
// failed to get confirmed block: slot was skipped or missing in long-term storage
|
||||
return this.getVaaLogs(fromSlot, toSlot - 1);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (!toBlock || !toBlock.blockTime || toBlock.transactions.length === 0) {
|
||||
return this.getVaaLogs(fromSlot, toSlot - 1);
|
||||
}
|
||||
const fromSignature =
|
||||
toBlock.transactions[toBlock.transactions.length - 1].transaction.signatures[0];
|
||||
|
||||
let fromBlock: VersionedBlockResponse | null = null;
|
||||
try {
|
||||
fromBlock = await connection.getBlock(fromSlot, { maxSupportedTransactionVersion: 0 });
|
||||
} catch (e) {
|
||||
if (e instanceof SolanaJSONRPCError && (e.code === -32007 || e.code === -32009)) {
|
||||
// failed to get confirmed block: slot was skipped or missing in long-term storage
|
||||
return this.getVaaLogs(fromSlot + 1, toSlot);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (!fromBlock || !fromBlock.blockTime || fromBlock.transactions.length === 0) {
|
||||
return this.getVaaLogs(fromSlot + 1, toSlot);
|
||||
}
|
||||
const toSignature = fromBlock.transactions[0].transaction.signatures[0];
|
||||
|
||||
// get all core bridge signatures between fromTransaction and toTransaction
|
||||
let numSignatures = this.getSignaturesLimit;
|
||||
let currSignature: string | undefined = fromSignature;
|
||||
while (numSignatures === this.getSignaturesLimit) {
|
||||
const signatures: ConfirmedSignatureInfo[] = await connection.getSignaturesForAddress(
|
||||
new PublicKey(WORMHOLE_PROGRAM_ID),
|
||||
{
|
||||
before: currSignature,
|
||||
until: toSignature,
|
||||
limit: this.getSignaturesLimit,
|
||||
},
|
||||
);
|
||||
|
||||
this.logger.info(`processing ${signatures.length} transactions`);
|
||||
|
||||
// In order to determine if a transaction has a Wormhole message, we normalize and iterate
|
||||
// through all instructions in the transaction. Only PostMessage instructions are relevant
|
||||
// when looking for messages. PostMessageUnreliable instructions are ignored because there
|
||||
// are no data availability guarantees (ie the associated message accounts may have been
|
||||
// reused, overwriting previous data). Then, the message account is the account given by
|
||||
// the second index in the instruction's account key indices. From here, we can fetch the
|
||||
// message data from the account and parse out the emitter and sequence.
|
||||
const results = await connection.getTransactions(
|
||||
signatures.map((s) => s.signature),
|
||||
{
|
||||
maxSupportedTransactionVersion: 0,
|
||||
},
|
||||
);
|
||||
if (results.length !== signatures.length) {
|
||||
throw new Error(`solana: failed to fetch tx for signatures`);
|
||||
}
|
||||
for (const res of results) {
|
||||
if (res?.meta?.err) {
|
||||
// skip errored txs
|
||||
continue;
|
||||
}
|
||||
if (!res || !res.blockTime) {
|
||||
throw new Error(
|
||||
`solana: failed to fetch tx for signature ${
|
||||
res?.transaction.signatures[0] || 'unknown'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
const message = res.transaction.message;
|
||||
const accountKeys = isLegacyMessage(message)
|
||||
? message.accountKeys
|
||||
: message.staticAccountKeys;
|
||||
const programIdIndex = accountKeys.findIndex((i) => i.toBase58() === WORMHOLE_PROGRAM_ID);
|
||||
const instructions = message.compiledInstructions;
|
||||
const innerInstructions =
|
||||
res.meta?.innerInstructions?.flatMap((i) =>
|
||||
i.instructions.map(normalizeCompileInstruction),
|
||||
) || [];
|
||||
const whInstructions = innerInstructions
|
||||
.concat(instructions)
|
||||
.filter((i) => i.programIdIndex === programIdIndex);
|
||||
for (const instruction of whInstructions) {
|
||||
// skip if not postMessage instruction
|
||||
|
||||
const instructionId = instruction.data;
|
||||
if (instructionId[0] !== 0x01) continue;
|
||||
|
||||
const accountId = accountKeys[instruction.accountKeyIndexes[1]];
|
||||
const { message } = await getPostedMessage(connection, accountId.toBase58(), COMMITMENT);
|
||||
const { sequence, emitterAddress, payload } = message || {};
|
||||
// console.log('res', res);
|
||||
// console.log('instruction', instruction);
|
||||
// console.log(
|
||||
// 'parseLog',
|
||||
// await getPostedMessage(connection, accountId.toBase58(), COMMITMENT),
|
||||
// );
|
||||
|
||||
// this.logger.info(`processing ${signatures.length} transactions`);
|
||||
const blockNumber = res.slot.toString();
|
||||
const chainName = this.chain;
|
||||
const emitter = emitterAddress.toString('hex');
|
||||
const parsePayload = payload.toString('hex');
|
||||
const parseSequence = sequence.toString();
|
||||
const sender = null;
|
||||
const txHash = res.transaction.signatures[0];
|
||||
|
||||
// // In order to determine if a transaction has a Wormhole message, we normalize and iterate
|
||||
// // through all instructions in the transaction. Only PostMessage instructions are relevant
|
||||
// // when looking for messages. PostMessageUnreliable instructions are ignored because there
|
||||
// // are no data availability guarantees (ie the associated message accounts may have been
|
||||
// // reused, overwriting previous data). Then, the message account is the account given by
|
||||
// // the second index in the instruction's account key indices. From here, we can fetch the
|
||||
// // message data from the account and parse out the emitter and sequence.
|
||||
// const results = await connection.getTransactions(
|
||||
// signatures.map((s) => s.signature),
|
||||
// {
|
||||
// maxSupportedTransactionVersion: 0,
|
||||
// }
|
||||
// );
|
||||
// if (results.length !== signatures.length) {
|
||||
// throw new Error(`solana: failed to fetch tx for signatures`);
|
||||
// }
|
||||
// for (const res of results) {
|
||||
// if (res?.meta?.err) {
|
||||
// // skip errored txs
|
||||
// continue;
|
||||
// }
|
||||
// if (!res || !res.blockTime) {
|
||||
// throw new Error(
|
||||
// `solana: failed to fetch tx for signature ${
|
||||
// res?.transaction.signatures[0] || 'unknown'
|
||||
// }`
|
||||
// );
|
||||
// }
|
||||
const vaaLog = makeVaaLog({
|
||||
chainName,
|
||||
emitter,
|
||||
sequence: parseSequence,
|
||||
txHash,
|
||||
sender,
|
||||
blockNumber,
|
||||
payload: parsePayload,
|
||||
});
|
||||
|
||||
// const message = res.transaction.message;
|
||||
// const accountKeys = isLegacyMessage(message)
|
||||
// ? message.accountKeys
|
||||
// : message.staticAccountKeys;
|
||||
// const programIdIndex = accountKeys.findIndex((i) => i.toBase58() === WORMHOLE_PROGRAM_ID);
|
||||
// const instructions = message.compiledInstructions;
|
||||
// const innerInstructions =
|
||||
// res.meta?.innerInstructions?.flatMap((i) =>
|
||||
// i.instructions.map(normalizeCompileInstruction)
|
||||
// ) || [];
|
||||
// const whInstructions = innerInstructions
|
||||
// .concat(instructions)
|
||||
// .filter((i) => i.programIdIndex === programIdIndex);
|
||||
// for (const instruction of whInstructions) {
|
||||
// // skip if not postMessage instruction
|
||||
// const instructionId = instruction.data;
|
||||
// if (instructionId[0] !== 0x01) continue;
|
||||
vaaLogs.push(vaaLog);
|
||||
}
|
||||
}
|
||||
|
||||
// const accountId = accountKeys[instruction.accountKeyIndexes[1]];
|
||||
// const {
|
||||
// message: { emitterAddress, sequence },
|
||||
// } = await getPostedMessage(connection, accountId.toBase58(), COMMITMENT);
|
||||
// const blockKey = makeBlockKey(
|
||||
// res.slot.toString(),
|
||||
// new Date(res.blockTime * 1000).toISOString()
|
||||
// );
|
||||
// const vaaKey = makeVaaKey(
|
||||
// res.transaction.signatures[0],
|
||||
// this.chain,
|
||||
// emitterAddress.toString('hex'),
|
||||
// sequence.toString()
|
||||
// );
|
||||
// vaasByBlock[blockKey] = [...(vaasByBlock[blockKey] || []), vaaKey];
|
||||
// }
|
||||
// }
|
||||
numSignatures = signatures.length;
|
||||
currSignature = signatures.at(-1)?.signature;
|
||||
}
|
||||
|
||||
// numSignatures = signatures.length;
|
||||
// currSignature = signatures.at(-1)?.signature;
|
||||
// }
|
||||
|
||||
// // add last block for storeVaasByBlock
|
||||
// const lastBlockKey = makeBlockKey(
|
||||
// toSlot.toString(),
|
||||
// new Date(toBlock.blockTime * 1000).toISOString()
|
||||
// );
|
||||
// return { [lastBlockKey]: [], ...vaasByBlock };
|
||||
// }
|
||||
|
||||
override getVaaLogs(fromBlock: number, toBlock: number): Promise<VaaLog[]> {
|
||||
throw new Error('Not Implemented');
|
||||
return vaaLogs;
|
||||
}
|
||||
|
||||
override isValidVaaKey(key: string) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { ChainName } from '@certusone/wormhole-sdk';
|
||||
import BaseDB from '../databases/BaseDB';
|
||||
import { VaaLog } from '../databases/types';
|
||||
import { VaaLog, VaasByBlock } from '../databases/types';
|
||||
import BaseSNS from '../services/SNS/BaseSNS';
|
||||
import { WormholeLogger } from '../utils/logger';
|
||||
import { AlgorandWatcher } from './AlgorandWatcher';
|
||||
|
@ -32,6 +32,7 @@ export interface WatcherImplementation {
|
|||
sns?: BaseSNS | null;
|
||||
db?: BaseDB;
|
||||
getFinalizedBlockNumber(): Promise<number>;
|
||||
getMessagesForBlocks(fromBlock: number, toBlock: number): Promise<VaasByBlock>;
|
||||
getVaaLogs(fromBlock: number, toBlock: number): Promise<VaaLog[]>;
|
||||
isValidBlockKey(key: string): boolean;
|
||||
isValidVaaKey(key: string): boolean;
|
||||
|
|
Loading…
Reference in New Issue