Skip to content

Commit db73214

Browse files
committed
cloud_function: solana uses acct instead of txhash
1 parent 228154e commit db73214

File tree

10 files changed

+137
-44
lines changed

10 files changed

+137
-44
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

+56-8
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 { convertSolanaTxToAccts } 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,11 +29,20 @@ 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-
});
34-
res.status(200).send(JSON.stringify(reobs));
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+
}
43+
// Need to flatten the array of arrays before returning
44+
const retVal = reobs.flat();
45+
res.status(200).send(JSON.stringify(retVal));
3546
return;
3647
}
3748

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

4860
try {
4961
const res = await firestore.runTransaction(async (t) => {
@@ -59,18 +71,54 @@ async function getAndProcessReobsVAAs(): Promise<Map<string, ReobserveInfo>> {
5971
}
6072
vaas = data.VAAs.slice(0, MAX_VAAS_TO_REOBSERVE);
6173
console.log('number of reobserved VAAs', vaas.length);
74+
const MAX_SOLANA_VAAS_TO_REOBSERVE = 2;
75+
// Can only process 2 Solana VAAs at a time due to rpc rate limits
76+
// So we put the rest back in the collection
77+
let solanaCount = 0;
78+
for (const vaa of vaas) {
79+
if (vaa.chain === CHAIN_ID_SOLANA) {
80+
solanaCount++;
81+
if (solanaCount > MAX_SOLANA_VAAS_TO_REOBSERVE) {
82+
putBack.push(vaa);
83+
continue;
84+
}
85+
}
86+
realVaas.push(vaa);
87+
}
88+
console.log('number of real VAAs', realVaas.length);
6289
}
6390
t.update(collectionRef, { VAAs: putBack });
6491
});
6592
} catch (e) {
6693
console.error('error getting reobserved VAAs', e);
6794
return current;
6895
}
69-
for (const vaa of vaas) {
70-
if (!(await isVAASigned(vaa.vaaKey))) {
96+
for (const vaa of realVaas) {
97+
if (!(await isVAASigned(getEnvironment(), vaa.vaaKey))) {
7198
current.set(vaa.txhash, vaa);
7299
}
73100
}
74101
console.log('number of reobservable VAAs that are not signed', current.size);
75102
return current;
76103
}
104+
105+
async function processVaa(vaa: ReobserveInfo): Promise<ReobserveInfo[] | null> {
106+
let vaas: ReobserveInfo[] = [];
107+
108+
if (vaa.chain === CHAIN_ID_SOLANA) {
109+
const origTxHash = vaa.txhash;
110+
const convertedTxHash: string[] = await convertSolanaTxToAccts(origTxHash);
111+
console.log(`Converted solana txHash ${origTxHash} to account ${convertedTxHash}`);
112+
113+
if (convertedTxHash.length === 0) {
114+
console.error(`Failed to convert solana txHash ${origTxHash} to an account.`);
115+
return null; // Indicate failure to convert
116+
}
117+
for (const account of convertedTxHash) {
118+
vaas.push({ ...vaa, txhash: account }); // Return a new object with the updated txhash
119+
}
120+
} else {
121+
vaas.push(vaa); // Return the original object for non-Solana chains
122+
}
123+
return vaas;
124+
}

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/consts.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ export const CHAIN_INFO_MAP: { [key in Environment]: { [key: string]: CHAIN_INFO
191191
evm: false,
192192
chainId: CHAIN_ID_SOLANA,
193193
endpointUrl: process.env.REACT_APP_SOLANA_RPC || 'https://api.mainnet-beta.solana.com',
194-
explorerStem: `https://solscan.io`,
194+
explorerStem: `https://solana.fm`,
195195
},
196196
2: {
197197
name: 'eth',

common/src/explorer.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { CHAIN_INFO_MAP, Environment } from './consts';
3636
export const explorerBlock = (network: Environment, chainId: ChainId, block: string) =>
3737
network === 'mainnet'
3838
? chainId === CHAIN_ID_SOLANA
39-
? `https://solscan.io/block/${block}`
39+
? `https://solana.fm/block/${block}`
4040
: chainId === CHAIN_ID_ETH
4141
? `https://etherscan.io/block/${block}`
4242
: chainId === CHAIN_ID_TERRA
@@ -135,7 +135,7 @@ export const explorerBlock = (network: Environment, chainId: ChainId, block: str
135135
export const explorerTx = (network: Environment, chainId: ChainId, tx: string) =>
136136
network === 'mainnet'
137137
? chainId === CHAIN_ID_SOLANA
138-
? `https://solscan.io/account/${tx}`
138+
? `https://solana.fm/tx/${tx}`
139139
: chainId === CHAIN_ID_ETH
140140
? `https://etherscan.io/tx/${tx}`
141141
: chainId === CHAIN_ID_TERRA

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

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 convertSolanaTxToAccts(txHash: string): Promise<string[]> {
31+
const POST_MESSAGE_IX_ID = 0x01;
32+
let accounts: string[] = [];
33+
const connection = new Connection(RPCS_BY_CHAIN.MAINNET.solana!, 'finalized');
34+
const txs = await connection.getTransactions([txHash], {
35+
maxSupportedTransactionVersion: 0,
36+
});
37+
for (const tx of txs) {
38+
if (!tx) {
39+
continue;
40+
}
41+
const message = tx.transaction.message;
42+
const accountKeys = isLegacyMessage(message) ? message.accountKeys : message.staticAccountKeys;
43+
const programIdIndex = accountKeys.findIndex(
44+
(i) => i.toBase58() === CONTRACTS.MAINNET.solana.core
45+
);
46+
const instructions = message.compiledInstructions;
47+
const innerInstructions =
48+
tx.meta?.innerInstructions?.flatMap((i) => i.instructions.map(normalizeCompileInstruction)) ||
49+
[];
50+
const whInstructions = innerInstructions
51+
.concat(instructions)
52+
.filter((i) => i.programIdIndex === programIdIndex);
53+
for (const instruction of whInstructions) {
54+
// skip if not postMessage instruction
55+
const instructionId = instruction.data;
56+
if (instructionId[0] !== POST_MESSAGE_IX_ID) continue;
57+
58+
accounts.push(accountKeys[instruction.accountKeyIndexes[1]].toBase58());
59+
}
60+
}
61+
return accounts;
62+
}

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)