Skip to content

Commit e4de6d9

Browse files
committed
cloud_function: solana uses acct instead of txhash
1 parent a2a3a1f commit e4de6d9

File tree

8 files changed

+125
-40
lines changed

8 files changed

+125
-40
lines changed

cloud_functions/src/alarmMissingVaas.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export async function alarmMissingVaas(req: any, res: any) {
117117
console.log(`skipping over ${vaaKey} because it is governed`);
118118
continue;
119119
}
120-
if (await isVAASigned(vaaKey)) {
120+
if (await isVAASigned(getEnvironment(), vaaKey)) {
121121
console.log(`skipping over ${vaaKey} because it is signed`);
122122
continue;
123123
}
@@ -249,7 +249,7 @@ async function getAndProcessReobsVAAs(): Promise<Map<string, ReobserveInfo>> {
249249
if (data) {
250250
const vaas: ReobserveInfo[] = data.VAAs;
251251
vaas.forEach(async (vaa) => {
252-
if (!(await isVAASigned(vaa.vaaKey))) {
252+
if (!(await isVAASigned(getEnvironment(), vaa.vaaKey))) {
253253
console.log('keeping reobserved VAA in firestore', vaa.vaaKey);
254254
current.set(vaa.txhash, vaa);
255255
} else {

cloud_functions/src/getReobserveVaas.ts

+48-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { assertEnvironmentVariable, isVAASigned } from './utils';
22
import { ReobserveInfo } from './types';
33
import { Firestore } from 'firebase-admin/firestore';
4-
import axios from 'axios';
4+
import { CHAIN_ID_SOLANA } from '@certusone/wormhole-sdk';
5+
import { convertSolanaTxToAcct } from '@wormhole-foundation/wormhole-monitor-common';
6+
import { getEnvironment } from '@wormhole-foundation/wormhole-monitor-common';
57

68
const MAX_VAAS_TO_REOBSERVE = 25;
79

@@ -27,10 +29,17 @@ export async function getReobserveVaas(req: any, res: any) {
2729
console.log('could not get missing VAAs', e);
2830
res.sendStatus(500);
2931
}
30-
let reobs: ReobserveInfo[] = [];
31-
reobsMap.forEach((vaa) => {
32-
reobs.push(vaa);
33-
});
32+
33+
let reobs: (ReobserveInfo | null)[] = [];
34+
try {
35+
const vaaArray = Array.from(reobsMap.values());
36+
// Process each VAA asynchronously and filter out any null results
37+
reobs = (await Promise.all(vaaArray.map(processVaa))).filter((vaa) => vaa !== null); // Remove any VAA that failed conversion
38+
} catch (e) {
39+
console.error('error processing reobservations', e);
40+
console.error('reobs', reobs);
41+
res.sendStatus(500);
42+
}
3443
res.status(200).send(JSON.stringify(reobs));
3544
return;
3645
}
@@ -44,6 +53,7 @@ async function getAndProcessReobsVAAs(): Promise<Map<string, ReobserveInfo>> {
4453
let current = new Map<string, ReobserveInfo>();
4554
let putBack: ReobserveInfo[] = [];
4655
let vaas: ReobserveInfo[] = [];
56+
let realVaas: ReobserveInfo[] = [];
4757

4858
try {
4959
const res = await firestore.runTransaction(async (t) => {
@@ -59,18 +69,49 @@ async function getAndProcessReobsVAAs(): Promise<Map<string, ReobserveInfo>> {
5969
}
6070
vaas = data.VAAs.slice(0, MAX_VAAS_TO_REOBSERVE);
6171
console.log('number of reobserved VAAs', vaas.length);
72+
const MAX_SOLANA_VAAS_TO_REOBSERVE = 2;
73+
// Can only process 2 Solana VAAs at a time due to rpc rate limits
74+
// So we put the rest back in the collection
75+
let solanaCount = 0;
76+
for (const vaa of vaas) {
77+
if (vaa.chain === CHAIN_ID_SOLANA) {
78+
solanaCount++;
79+
if (solanaCount > MAX_SOLANA_VAAS_TO_REOBSERVE) {
80+
putBack.push(vaa);
81+
continue;
82+
}
83+
}
84+
realVaas.push(vaa);
85+
}
86+
console.log('number of real VAAs', realVaas.length);
6287
}
6388
t.update(collectionRef, { VAAs: putBack });
6489
});
6590
} catch (e) {
6691
console.error('error getting reobserved VAAs', e);
6792
return current;
6893
}
69-
for (const vaa of vaas) {
70-
if (!(await isVAASigned(vaa.vaaKey))) {
94+
for (const vaa of realVaas) {
95+
if (!(await isVAASigned(getEnvironment(), vaa.vaaKey))) {
7196
current.set(vaa.txhash, vaa);
7297
}
7398
}
7499
console.log('number of reobservable VAAs that are not signed', current.size);
75100
return current;
76101
}
102+
103+
async function processVaa(vaa: ReobserveInfo) {
104+
if (vaa.chain === CHAIN_ID_SOLANA) {
105+
const origTxHash = vaa.txhash;
106+
const convertedTxHash = await convertSolanaTxToAcct(origTxHash);
107+
console.log(`Converted solana txHash ${origTxHash} to account ${convertedTxHash}`);
108+
109+
if (convertedTxHash === '') {
110+
console.error(`Failed to convert solana txHash ${origTxHash} to an account.`);
111+
return null; // Indicate failure to convert
112+
}
113+
114+
return { ...vaa, txhash: convertedTxHash }; // Return a new object with the updated txhash
115+
}
116+
return vaa; // Return the original object for non-Solana chains
117+
}

cloud_functions/src/utils.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import axios from 'axios';
22
import { PagerDutyInfo, SlackInfo } from './types';
3+
import { Environment } from '@wormhole-foundation/wormhole-monitor-common';
34

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

89-
export async function isVAASigned(vaaKey: string): Promise<boolean> {
90-
const url: string = WormholescanRPC + 'v1/signed_vaa/' + vaaKey;
90+
export async function isVAASigned(env: Environment, vaaKey: string): Promise<boolean> {
91+
const url: string = WormholescanRPC + 'v1/signed_vaa/' + vaaKey + '?network=' + env.toUpperCase();
9192
try {
9293
const response = await axios.get(url);
9394
// curl -X 'GET' \
9495
// 'https://api.wormholescan.io/v1/signed_vaa/1/ec7372995d5cc8732397fb0ad35c0121e0eaa90d26f828a534cab54391b3a4f5/319118' \
9596
// -H 'accept: application/json'
96-
// This function will return true if the get returns 200
97+
// This function will return true if the GET returns 200
9798
// Otherwise, it will return false
9899
if (response.status === 200) {
99100
return true;

common/src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './arrays';
22
export * from './consts';
3-
export * from './utils';
43
export * from './explorer';
4+
export * from './solana';
5+
export * from './utils';

common/src/solana.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {
2+
CompiledInstruction,
3+
Message,
4+
MessageCompiledInstruction,
5+
MessageV0,
6+
} from '@solana/web3.js';
7+
import { decode } from 'bs58';
8+
import { Connection } from '@solana/web3.js';
9+
import { RPCS_BY_CHAIN } from '@certusone/wormhole-sdk/lib/cjs/relayer';
10+
import { CONTRACTS } from '@certusone/wormhole-sdk';
11+
12+
export const isLegacyMessage = (message: Message | MessageV0): message is Message => {
13+
return message.version === 'legacy';
14+
};
15+
16+
export const normalizeCompileInstruction = (
17+
instruction: CompiledInstruction | MessageCompiledInstruction
18+
): MessageCompiledInstruction => {
19+
if ('accounts' in instruction) {
20+
return {
21+
accountKeyIndexes: instruction.accounts,
22+
data: decode(instruction.data),
23+
programIdIndex: instruction.programIdIndex,
24+
};
25+
} else {
26+
return instruction;
27+
}
28+
};
29+
30+
export async function convertSolanaTxToAcct(txHash: string): Promise<string> {
31+
const POST_MESSAGE_IX_ID = 0x01;
32+
const connection = new Connection(RPCS_BY_CHAIN.MAINNET.solana!, 'finalized');
33+
const txs = await connection.getTransactions([txHash], {
34+
maxSupportedTransactionVersion: 0,
35+
});
36+
for (const tx of txs) {
37+
if (!tx) {
38+
continue;
39+
}
40+
const message = tx.transaction.message;
41+
const accountKeys = isLegacyMessage(message) ? message.accountKeys : message.staticAccountKeys;
42+
const programIdIndex = accountKeys.findIndex(
43+
(i) => i.toBase58() === CONTRACTS.MAINNET.solana.core
44+
);
45+
const instructions = message.compiledInstructions;
46+
const innerInstructions =
47+
tx.meta?.innerInstructions?.flatMap((i) => i.instructions.map(normalizeCompileInstruction)) ||
48+
[];
49+
const whInstructions = innerInstructions
50+
.concat(instructions)
51+
.filter((i) => i.programIdIndex === programIdIndex);
52+
for (const instruction of whInstructions) {
53+
// skip if not postMessage instruction
54+
const instructionId = instruction.data;
55+
if (instructionId[0] !== POST_MESSAGE_IX_ID) continue;
56+
57+
return accountKeys[instruction.accountKeyIndexes[1]].toBase58();
58+
}
59+
}
60+
return '';
61+
}

watcher/scripts/solanaMissedMessageAccounts.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import { Connection } from '@solana/web3.js';
66
import axios from 'axios';
77
import ora from 'ora';
88
import { RPCS_BY_CHAIN } from '../src/consts';
9-
import { isLegacyMessage, normalizeCompileInstruction } from '../src/utils/solana';
9+
import {
10+
isLegacyMessage,
11+
normalizeCompileInstruction,
12+
} from '@wormhole-foundation/wormhole-monitor-common/src/solana';
1013

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

watcher/src/utils/solana.ts

-25
This file was deleted.

watcher/src/watchers/SolanaWatcher.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import { z } from 'zod';
1313
import { RPCS_BY_CHAIN } from '../consts';
1414
import { VaasByBlock } from '../databases/types';
1515
import { makeBlockKey, makeVaaKey } from '../databases/utils';
16-
import { isLegacyMessage, normalizeCompileInstruction } from '../utils/solana';
16+
import {
17+
isLegacyMessage,
18+
normalizeCompileInstruction,
19+
} from '@wormhole-foundation/wormhole-monitor-common/src/solana';
1720
import { Watcher } from './Watcher';
1821
import { Environment } from '@wormhole-foundation/wormhole-monitor-common';
1922

0 commit comments

Comments
 (0)