@@ -18,16 +18,37 @@ import {
18
18
universalAddress_stripped ,
19
19
} from '@wormhole-foundation/wormhole-monitor-common' ;
20
20
import { Watcher } from './Watcher' ;
21
- import { Network , contracts } from '@wormhole-foundation/sdk-base' ;
21
+ import { Network , contracts , encoding } from '@wormhole-foundation/sdk-base' ;
22
22
import { deserializePostMessage } from '@wormhole-foundation/sdk-solana-core' ;
23
23
import { getAllKeys } from '../utils/solana' ;
24
+ import { UniversalAddress } from '@wormhole-foundation/sdk-definitions' ;
25
+ import { DeriveType , deserialize , Layout } from 'binary-layout' ;
24
26
25
27
const COMMITMENT : Commitment = 'finalized' ;
26
28
const GET_SIGNATURES_LIMIT = 1000 ;
27
29
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
+
28
48
export class SolanaWatcher extends Watcher {
29
49
readonly rpc : string ;
30
50
readonly programId : string ;
51
+ readonly shimProgramId : string ;
31
52
// this is set as a class field so we can modify it in tests
32
53
getSignaturesLimit = GET_SIGNATURES_LIMIT ;
33
54
// The Solana watcher uses the `getSignaturesForAddress` RPC endpoint to fetch all transactions
@@ -43,6 +64,7 @@ export class SolanaWatcher extends Watcher {
43
64
super ( network , 'Solana' , mode ) ;
44
65
this . rpc = RPCS_BY_CHAIN [ this . network ] . Solana ! ;
45
66
this . programId = contracts . coreBridge ( this . network , 'Solana' ) ;
67
+ this . shimProgramId = ShimContracts [ this . network ] ;
46
68
}
47
69
48
70
getConnection ( ) : Connection {
@@ -162,39 +184,75 @@ export class SolanaWatcher extends Watcher {
162
184
163
185
const accountKeys = await getAllKeys ( this . getConnection ( ) , res ) ;
164
186
const programIdIndex = accountKeys . findIndex ( ( i ) => i . toBase58 ( ) === this . programId ) ;
187
+ const shimProgramIdIndex = accountKeys . findIndex (
188
+ ( i ) => i . toBase58 ( ) === this . shimProgramId
189
+ ) ;
165
190
const message : VersionedMessage = res . transaction . message ;
166
191
const instructions = message . compiledInstructions ;
167
192
const innerInstructions =
168
193
res . meta ?. innerInstructions ?. flatMap ( ( i ) =>
169
194
i . instructions . map ( normalizeCompileInstruction )
170
195
) || [ ] ;
171
196
197
+ // Need to look for Wormhole instructions and shim instructions
172
198
const whInstructions = innerInstructions
173
199
. concat ( instructions )
174
- . filter ( ( i ) => i . programIdIndex === programIdIndex ) ;
200
+ . filter (
201
+ ( i ) => i . programIdIndex === programIdIndex || i . programIdIndex === shimProgramIdIndex
202
+ ) ;
175
203
176
204
const blockKey = makeBlockKey (
177
205
res . slot . toString ( ) ,
178
206
new Date ( res . blockTime * 1000 ) . toISOString ( )
179
207
) ;
180
208
209
+ let needShim = false ;
210
+ let gotShim = false ;
181
211
const vaaKeys : string [ ] = [ ] ;
182
212
for ( const instruction of whInstructions ) {
183
- // skip if not postMessage instruction
184
213
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
+ }
186
223
187
- const accountId = accountKeys [ instruction . accountKeyIndexes [ 1 ] ] ;
224
+ let emitterAddress : UniversalAddress ;
225
+ let sequence : bigint ;
188
226
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?
194
252
195
253
vaaKeys . push (
196
254
makeVaaKey (
197
- res . transaction . signatures [ 0 ] ,
255
+ res . transaction . signatures [ 0 ] , // This is the tx hash
198
256
this . chain ,
199
257
universalAddress_stripped ( emitterAddress ) ,
200
258
sequence . toString ( )
@@ -217,6 +275,28 @@ export class SolanaWatcher extends Watcher {
217
275
return { vaasByBlock : { [ lastBlockKey ] : [ ] , ...vaasByBlock } } ;
218
276
}
219
277
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
+
220
300
isValidVaaKey ( key : string ) {
221
301
try {
222
302
const [ txHash , vaaKey ] = key . split ( ':' ) ;
0 commit comments