Skip to content

Commit b0566eb

Browse files
committed
watcher: solana shim support
1 parent 45ebe2e commit b0566eb

File tree

3 files changed

+97
-14
lines changed

3 files changed

+97
-14
lines changed

package-lock.json

+5-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

watcher/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"algosdk": "^2.4.0",
4646
"anchor-0.29.0": "npm:@coral-xyz/anchor@^0.29.0",
4747
"aptos": "^1.4.0",
48+
"binary-layout": "^1.2.0",
4849
"bs58": "^5.0.0",
4950
"dotenv": "^16.0.3",
5051
"firebase-admin": "^11.4.0",

watcher/src/watchers/SolanaWatcher.ts

+91-11
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,37 @@ import {
1818
universalAddress_stripped,
1919
} from '@wormhole-foundation/wormhole-monitor-common';
2020
import { Watcher } from './Watcher';
21-
import { Network, contracts } from '@wormhole-foundation/sdk-base';
21+
import { Network, contracts, encoding } from '@wormhole-foundation/sdk-base';
2222
import { deserializePostMessage } from '@wormhole-foundation/sdk-solana-core';
2323
import { getAllKeys } from '../utils/solana';
24+
import { UniversalAddress } from '@wormhole-foundation/sdk-definitions';
25+
import { DeriveType, deserialize, Layout } from 'binary-layout';
2426

2527
const COMMITMENT: Commitment = 'finalized';
2628
const GET_SIGNATURES_LIMIT = 1000;
2729

30+
const ShimContracts: { [key in Network]: string } = {
31+
Mainnet: '',
32+
Testnet: 'EtZMZM22ViKMo4r5y4Anovs3wKQ2owUmDpjygnMMcdEX',
33+
Devnet: '',
34+
};
35+
36+
const POST_MESSAGE_INSTRUCTION_ID = 0x01;
37+
const POST_MESSAGE_UNRELIABLE_INSTRUCTION_ID = 0x08;
38+
const shimMessageEventDiscriminator = 'e445a52e51cb9a1d441b8f004d4c8970';
39+
40+
const shimMessageEventLayout = [
41+
{ name: 'discriminator', binary: 'bytes', size: 16 },
42+
{ name: 'emitterAddress', binary: 'bytes', size: 32 },
43+
{ name: 'sequence', binary: 'uint', size: 8, endianness: 'little' },
44+
{ name: 'timestamp', binary: 'uint', size: 4, endianness: 'little' },
45+
] as const satisfies Layout;
46+
export type ShimMessageEvent = DeriveType<typeof shimMessageEventLayout>;
47+
2848
export class SolanaWatcher extends Watcher {
2949
readonly rpc: string;
3050
readonly programId: string;
51+
readonly shimProgramId: string;
3152
// this is set as a class field so we can modify it in tests
3253
getSignaturesLimit = GET_SIGNATURES_LIMIT;
3354
// The Solana watcher uses the `getSignaturesForAddress` RPC endpoint to fetch all transactions
@@ -43,6 +64,7 @@ export class SolanaWatcher extends Watcher {
4364
super(network, 'Solana', mode);
4465
this.rpc = RPCS_BY_CHAIN[this.network].Solana!;
4566
this.programId = contracts.coreBridge(this.network, 'Solana');
67+
this.shimProgramId = ShimContracts[this.network];
4668
}
4769

4870
getConnection(): Connection {
@@ -162,39 +184,75 @@ export class SolanaWatcher extends Watcher {
162184

163185
const accountKeys = await getAllKeys(this.getConnection(), res);
164186
const programIdIndex = accountKeys.findIndex((i) => i.toBase58() === this.programId);
187+
const shimProgramIdIndex = accountKeys.findIndex(
188+
(i) => i.toBase58() === this.shimProgramId
189+
);
165190
const message: VersionedMessage = res.transaction.message;
166191
const instructions = message.compiledInstructions;
167192
const innerInstructions =
168193
res.meta?.innerInstructions?.flatMap((i) =>
169194
i.instructions.map(normalizeCompileInstruction)
170195
) || [];
171196

197+
// Need to look for Wormhole instructions and shim instructions
172198
const whInstructions = innerInstructions
173199
.concat(instructions)
174-
.filter((i) => i.programIdIndex === programIdIndex);
200+
.filter(
201+
(i) => i.programIdIndex === programIdIndex || i.programIdIndex === shimProgramIdIndex
202+
);
175203

176204
const blockKey = makeBlockKey(
177205
res.slot.toString(),
178206
new Date(res.blockTime * 1000).toISOString()
179207
);
180208

209+
let needShim = false;
210+
let gotShim = false;
181211
const vaaKeys: string[] = [];
182212
for (const instruction of whInstructions) {
183-
// skip if not postMessage instruction
184213
const instructionId = instruction.data;
185-
if (instructionId[0] !== 0x08 && instructionId[0] !== 0x01) continue;
214+
if (
215+
instruction.programIdIndex === programIdIndex &&
216+
instructionId[0] === POST_MESSAGE_UNRELIABLE_INSTRUCTION_ID
217+
) {
218+
// TODO: Do I need to verify that this message has no data?
219+
// This is an unreliable wormhole message. It is only used in conjunction with a shim message.
220+
needShim = true;
221+
continue;
222+
}
186223

187-
const accountId = accountKeys[instruction.accountKeyIndexes[1]];
224+
let emitterAddress: UniversalAddress;
225+
let sequence: bigint;
188226

189-
const acctInfo = await this.getConnection().getAccountInfo(accountId, COMMITMENT);
190-
if (!acctInfo?.data) throw new Error('No data found in message account');
191-
const { emitterAddress, sequence } = deserializePostMessage(
192-
new Uint8Array(acctInfo.data)
193-
);
227+
if (instruction.programIdIndex === programIdIndex) {
228+
if (instructionId[0] !== POST_MESSAGE_INSTRUCTION_ID) {
229+
console.log('Got non-post message instruction');
230+
continue;
231+
}
232+
const accountId = accountKeys[instruction.accountKeyIndexes[1]];
233+
234+
const acctInfo = await this.getConnection().getAccountInfo(accountId, COMMITMENT);
235+
if (!acctInfo?.data) throw new Error('No data found in message account');
236+
const deserializedMsg = deserializePostMessage(new Uint8Array(acctInfo.data));
237+
emitterAddress = deserializedMsg.emitterAddress;
238+
sequence = deserializedMsg.sequence;
239+
} else {
240+
// instruction.programIdIndex === shimProgramIdIndex
241+
console.log('Got shim instruction');
242+
gotShim = true;
243+
const parsedMsg = this.parseShimMessage(instruction.data);
244+
if (!parsedMsg) {
245+
console.log('Failed to parse shim message');
246+
continue;
247+
}
248+
emitterAddress = parsedMsg.emitterAddress;
249+
sequence = parsedMsg.sequence;
250+
}
251+
// TODO: should I check if needShim === gotShim?
194252

195253
vaaKeys.push(
196254
makeVaaKey(
197-
res.transaction.signatures[0],
255+
res.transaction.signatures[0], // This is the tx hash
198256
this.chain,
199257
universalAddress_stripped(emitterAddress),
200258
sequence.toString()
@@ -217,6 +275,28 @@ export class SolanaWatcher extends Watcher {
217275
return { vaasByBlock: { [lastBlockKey]: [], ...vaasByBlock } };
218276
}
219277

278+
parseShimMessage(data: Uint8Array): {
279+
emitterAddress: UniversalAddress;
280+
sequence: bigint;
281+
} | null {
282+
// First step is to convert the data into a hex string
283+
const hexData = encoding.hex.encode(data);
284+
285+
// Next, we need to check which discriminator is present in the data
286+
if (hexData.startsWith(shimMessageEventDiscriminator)) {
287+
// The data is in the format of the discriminator followed by the emitter address, sequence number, and timestamp.
288+
// The emitter address is 32 bytes, the sequence number is 8 bytes, and the timestamp is 4 bytes.
289+
290+
// Try using the layout instead
291+
const decoded = deserialize(shimMessageEventLayout, data);
292+
const emitterAddress = new UniversalAddress(decoded.emitterAddress);
293+
const sequence = BigInt(decoded.sequence);
294+
295+
return { emitterAddress, sequence };
296+
}
297+
return null;
298+
}
299+
220300
isValidVaaKey(key: string) {
221301
try {
222302
const [txHash, vaaKey] = key.split(':');

0 commit comments

Comments
 (0)