Skip to content

Commit

Permalink
cloud_function: solana uses acct instead of txhash
Browse files Browse the repository at this point in the history
  • Loading branch information
panoel committed Mar 11, 2024
1 parent 1ac3460 commit 2878541
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 44 deletions.
4 changes: 2 additions & 2 deletions cloud_functions/src/alarmMissingVaas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export async function alarmMissingVaas(req: any, res: any) {
console.log(`skipping over ${vaaKey} because it is governed`);
continue;
}
if (await isVAASigned(vaaKey)) {
if (await isVAASigned(getEnvironment(), vaaKey)) {
console.log(`skipping over ${vaaKey} because it is signed`);
continue;
}
Expand Down Expand Up @@ -249,7 +249,7 @@ async function getAndProcessReobsVAAs(): Promise<Map<string, ReobserveInfo>> {
if (data) {
const vaas: ReobserveInfo[] = data.VAAs;
vaas.forEach(async (vaa) => {
if (!(await isVAASigned(vaa.vaaKey))) {
if (!(await isVAASigned(getEnvironment(), vaa.vaaKey))) {
console.log('keeping reobserved VAA in firestore', vaa.vaaKey);
current.set(vaa.txhash, vaa);
} else {
Expand Down
64 changes: 56 additions & 8 deletions cloud_functions/src/getReobserveVaas.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { assertEnvironmentVariable, isVAASigned } from './utils';
import { ReobserveInfo } from './types';
import { Firestore } from 'firebase-admin/firestore';
import axios from 'axios';
import { CHAIN_ID_SOLANA } from '@certusone/wormhole-sdk';
import { convertSolanaTxToAccts } from '@wormhole-foundation/wormhole-monitor-common';
import { getEnvironment } from '@wormhole-foundation/wormhole-monitor-common';

const MAX_VAAS_TO_REOBSERVE = 25;

Expand All @@ -27,11 +29,20 @@ export async function getReobserveVaas(req: any, res: any) {
console.log('could not get missing VAAs', e);
res.sendStatus(500);
}
let reobs: ReobserveInfo[] = [];
reobsMap.forEach((vaa) => {
reobs.push(vaa);
});
res.status(200).send(JSON.stringify(reobs));

let reobs: (ReobserveInfo[] | null)[] = [];
try {
const vaaArray = Array.from(reobsMap.values());
// Process each VAA asynchronously and filter out any null results
reobs = (await Promise.all(vaaArray.map(processVaa))).filter((vaa) => vaa !== null); // Remove any VAA that failed conversion
} catch (e) {
console.error('error processing reobservations', e);
console.error('reobs', reobs);
res.sendStatus(500);
}
// Need to flatten the array of arrays before returning
const retVal = reobs.flat();
res.status(200).send(JSON.stringify(retVal));
return;
}

Expand All @@ -44,6 +55,7 @@ async function getAndProcessReobsVAAs(): Promise<Map<string, ReobserveInfo>> {
let current = new Map<string, ReobserveInfo>();
let putBack: ReobserveInfo[] = [];
let vaas: ReobserveInfo[] = [];
let realVaas: ReobserveInfo[] = [];

try {
const res = await firestore.runTransaction(async (t) => {
Expand All @@ -59,18 +71,54 @@ async function getAndProcessReobsVAAs(): Promise<Map<string, ReobserveInfo>> {
}
vaas = data.VAAs.slice(0, MAX_VAAS_TO_REOBSERVE);
console.log('number of reobserved VAAs', vaas.length);
const MAX_SOLANA_VAAS_TO_REOBSERVE = 2;
// Can only process 2 Solana VAAs at a time due to rpc rate limits
// So we put the rest back in the collection
let solanaCount = 0;
for (const vaa of vaas) {
if (vaa.chain === CHAIN_ID_SOLANA) {
solanaCount++;
if (solanaCount > MAX_SOLANA_VAAS_TO_REOBSERVE) {
putBack.push(vaa);
continue;
}
}
realVaas.push(vaa);
}
console.log('number of real VAAs', realVaas.length);
}
t.update(collectionRef, { VAAs: putBack });
});
} catch (e) {
console.error('error getting reobserved VAAs', e);
return current;
}
for (const vaa of vaas) {
if (!(await isVAASigned(vaa.vaaKey))) {
for (const vaa of realVaas) {
if (!(await isVAASigned(getEnvironment(), vaa.vaaKey))) {
current.set(vaa.txhash, vaa);
}
}
console.log('number of reobservable VAAs that are not signed', current.size);
return current;
}

async function processVaa(vaa: ReobserveInfo): Promise<ReobserveInfo[] | null> {
let vaas: ReobserveInfo[] = [];

if (vaa.chain === CHAIN_ID_SOLANA) {
const origTxHash = vaa.txhash;
const convertedTxHash: string[] = await convertSolanaTxToAccts(origTxHash);
console.log(`Converted solana txHash ${origTxHash} to account ${convertedTxHash}`);

if (convertedTxHash.length === 0) {
console.error(`Failed to convert solana txHash ${origTxHash} to an account.`);
return null; // Indicate failure to convert
}
for (const account of convertedTxHash) {
vaas.push({ ...vaa, txhash: account }); // Return a new object with the updated txhash
}
} else {
vaas.push(vaa); // Return the original object for non-Solana chains
}
return vaas;
}
7 changes: 4 additions & 3 deletions cloud_functions/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import axios from 'axios';
import { PagerDutyInfo, SlackInfo } from './types';
import { Environment } from '@wormhole-foundation/wormhole-monitor-common';

export async function sleep(timeout: number) {
return new Promise((resolve) => setTimeout(resolve, timeout));
Expand Down Expand Up @@ -86,14 +87,14 @@ export async function formatAndSendToSlack(info: SlackInfo): Promise<any> {
return responseData;
}

export async function isVAASigned(vaaKey: string): Promise<boolean> {
const url: string = WormholescanRPC + 'v1/signed_vaa/' + vaaKey;
export async function isVAASigned(env: Environment, vaaKey: string): Promise<boolean> {
const url: string = WormholescanRPC + 'v1/signed_vaa/' + vaaKey + '?network=' + env.toUpperCase();
try {
const response = await axios.get(url);
// curl -X 'GET' \
// 'https://api.wormholescan.io/v1/signed_vaa/1/ec7372995d5cc8732397fb0ad35c0121e0eaa90d26f828a534cab54391b3a4f5/319118' \
// -H 'accept: application/json'
// This function will return true if the get returns 200
// This function will return true if the GET returns 200
// Otherwise, it will return false
if (response.status === 200) {
return true;
Expand Down
2 changes: 1 addition & 1 deletion common/src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export const CHAIN_INFO_MAP: { [key in Environment]: { [key: string]: CHAIN_INFO
evm: false,
chainId: CHAIN_ID_SOLANA,
endpointUrl: process.env.REACT_APP_SOLANA_RPC || 'https://api.mainnet-beta.solana.com',
explorerStem: `https://solscan.io`,
explorerStem: `https://solana.fm`,
},
2: {
name: 'eth',
Expand Down
4 changes: 2 additions & 2 deletions common/src/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { CHAIN_INFO_MAP, Environment } from './consts';
export const explorerBlock = (network: Environment, chainId: ChainId, block: string) =>
network === 'mainnet'
? chainId === CHAIN_ID_SOLANA
? `https://solscan.io/block/${block}`
? `https://solana.fm/block/${block}`
: chainId === CHAIN_ID_ETH
? `https://etherscan.io/block/${block}`
: chainId === CHAIN_ID_TERRA
Expand Down Expand Up @@ -135,7 +135,7 @@ export const explorerBlock = (network: Environment, chainId: ChainId, block: str
export const explorerTx = (network: Environment, chainId: ChainId, tx: string) =>
network === 'mainnet'
? chainId === CHAIN_ID_SOLANA
? `https://solscan.io/account/${tx}`
? `https://solana.fm/tx/${tx}`
: chainId === CHAIN_ID_ETH
? `https://etherscan.io/tx/${tx}`
: chainId === CHAIN_ID_TERRA
Expand Down
3 changes: 2 additions & 1 deletion common/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './arrays';
export * from './consts';
export * from './utils';
export * from './explorer';
export * from './solana';
export * from './utils';
62 changes: 62 additions & 0 deletions common/src/solana.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
CompiledInstruction,
Message,
MessageCompiledInstruction,
MessageV0,
} from '@solana/web3.js';
import { decode } from 'bs58';
import { Connection } from '@solana/web3.js';
import { RPCS_BY_CHAIN } from '@certusone/wormhole-sdk/lib/cjs/relayer';
import { CONTRACTS } from '@certusone/wormhole-sdk';

export const isLegacyMessage = (message: Message | MessageV0): message is Message => {
return message.version === 'legacy';
};

export const normalizeCompileInstruction = (
instruction: CompiledInstruction | MessageCompiledInstruction
): MessageCompiledInstruction => {
if ('accounts' in instruction) {
return {
accountKeyIndexes: instruction.accounts,
data: decode(instruction.data),
programIdIndex: instruction.programIdIndex,
};
} else {
return instruction;
}
};

export async function convertSolanaTxToAccts(txHash: string): Promise<string[]> {
const POST_MESSAGE_IX_ID = 0x01;
let accounts: string[] = [];
const connection = new Connection(RPCS_BY_CHAIN.MAINNET.solana!, 'finalized');
const txs = await connection.getTransactions([txHash], {
maxSupportedTransactionVersion: 0,
});
for (const tx of txs) {
if (!tx) {
continue;
}
const message = tx.transaction.message;
const accountKeys = isLegacyMessage(message) ? message.accountKeys : message.staticAccountKeys;
const programIdIndex = accountKeys.findIndex(
(i) => i.toBase58() === CONTRACTS.MAINNET.solana.core
);
const instructions = message.compiledInstructions;
const innerInstructions =
tx.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] !== POST_MESSAGE_IX_ID) continue;

accounts.push(accountKeys[instruction.accountKeyIndexes[1]].toBase58());
}
}
return accounts;
}
5 changes: 4 additions & 1 deletion watcher/scripts/solanaMissedMessageAccounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { Connection } from '@solana/web3.js';
import axios from 'axios';
import ora from 'ora';
import { RPCS_BY_CHAIN } from '../src/consts';
import { isLegacyMessage, normalizeCompileInstruction } from '../src/utils/solana';
import {
isLegacyMessage,
normalizeCompileInstruction,
} from '@wormhole-foundation/wormhole-monitor-common/src/solana';

// This script finds the message accounts which correspond to solana misses

Expand Down
25 changes: 0 additions & 25 deletions watcher/src/utils/solana.ts

This file was deleted.

5 changes: 4 additions & 1 deletion watcher/src/watchers/SolanaWatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import { z } from 'zod';
import { RPCS_BY_CHAIN } from '../consts';
import { VaasByBlock } from '../databases/types';
import { makeBlockKey, makeVaaKey } from '../databases/utils';
import { isLegacyMessage, normalizeCompileInstruction } from '../utils/solana';
import {
isLegacyMessage,
normalizeCompileInstruction,
} from '@wormhole-foundation/wormhole-monitor-common/src/solana';
import { Watcher } from './Watcher';
import { Environment } from '@wormhole-foundation/wormhole-monitor-common';

Expand Down

0 comments on commit 2878541

Please sign in to comment.