Skip to content

Commit 1c827a8

Browse files
committed
ft_watcher: pr comments
Signed-off-by: bingyuyap <bingyu.yap.21@gmail.com>
1 parent d6f86bd commit 1c827a8

File tree

3 files changed

+133
-56
lines changed

3 files changed

+133
-56
lines changed

watcher/src/fastTransfer/types.ts

+34
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { PublicKey } from '@solana/web3.js';
12
import BN from 'bn.js'; // Imported since FT codebase uses BN
23

34
// Type definitions are snake_case to match the database schema
@@ -108,3 +109,36 @@ export type FastTransferId = {
108109
fast_vaa_id?: string;
109110
auction_pubkey?: string;
110111
};
112+
113+
// these can be found in the matchingEngineProgram, but we are making custom snake cased
114+
// types to match the events in the logs parsed. Somehow anchor does not automatically convert
115+
// the logs to the correct types
116+
export type AuctionUpdated = {
117+
config_id: number;
118+
auction: PublicKey;
119+
vaa: PublicKey | null;
120+
source_chain: number;
121+
target_protocol: MessageProtocol;
122+
redeemer_message_len: number;
123+
end_slot: BN;
124+
best_offer_token: PublicKey;
125+
token_balance_before: BN;
126+
amount_in: BN;
127+
total_deposit: BN;
128+
max_offer_price_allowed: BN;
129+
};
130+
131+
export type MessageProtocol = {
132+
Local?: {
133+
program_id: PublicKey;
134+
};
135+
Cctp?: {
136+
domain: number;
137+
};
138+
None?: {};
139+
};
140+
141+
export type AuctionUpdatedEvent = {
142+
name: 'AuctionUpdated';
143+
data: AuctionUpdated;
144+
};

watcher/src/watchers/FTSolanaWatcher.ts

+94-53
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from '@solana/web3.js';
1717
import { findFromSignatureAndToSignature } from '../utils/solana';
1818
import { makeBlockKey } from '../databases/utils';
19-
import { BorshCoder, EventParser, Instruction } from '@coral-xyz/anchor';
19+
import { BorshCoder, Event, EventParser, Instruction } from '@coral-xyz/anchor';
2020
import { decodeTransferInstruction } from '@solana/spl-token';
2121

2222
import MATCHING_ENGINE_IDL from '../idls/matching_engine.json';
@@ -37,6 +37,8 @@ import {
3737
FastTransferStatus,
3838
ParsedLogs,
3939
isOfferArgs,
40+
AuctionUpdated,
41+
AuctionUpdatedEvent,
4042
} from '../fastTransfer/types';
4143
import knex, { Knex } from 'knex';
4244
import { assertEnvironmentVariable } from '@wormhole-foundation/wormhole-monitor-common';
@@ -79,7 +81,7 @@ export class FastTransferSolanaWatcher extends SolanaWatcher {
7981
this.MATCHING_ENGINE_PROGRAM_ID,
8082
new PublicKey(this.USDC_MINT)
8183
);
82-
this.getSignaturesLimit = 100;
84+
this.connection = new Connection(this.rpc);
8385
this.logger = getLogger(`fast_transfer_solana_${network.toLowerCase()}`);
8486
this.eventParser = new EventParser(
8587
new PublicKey(this.MATCHING_ENGINE_PROGRAM_ID),
@@ -327,20 +329,9 @@ export class FastTransferSolanaWatcher extends SolanaWatcher {
327329
);
328330
const fastVaaMessage = LiquidityLayerMessage.decode(fastVaaAccount.payload());
329331

330-
let message_protocol: FastTransferProtocol = FastTransferProtocol.NONE;
331-
let cctp_domain: number | undefined;
332-
let local_program_id: string | undefined;
333-
334-
if (res.meta?.logMessages) {
335-
const auctionUpdate = await this.fetchEventFromLogs('AuctionUpdated', res.meta.logMessages);
336-
if (auctionUpdate.target_protocol.Cctp) {
337-
message_protocol = FastTransferProtocol.CCTP;
338-
cctp_domain = auctionUpdate.target_protocol.Cctp.domain;
339-
} else if (auctionUpdate.target_protocol.Local) {
340-
message_protocol = FastTransferProtocol.LOCAL;
341-
local_program_id = auctionUpdate.target_protocol.Local.program_id.toBase58();
342-
}
343-
}
332+
const { message_protocol, cctp_domain, local_program_id } = this.checkMessageProtocols(
333+
res.meta?.logMessages || []
334+
);
344335

345336
if (!fastVaaMessage.fastMarketOrder) {
346337
throw new Error(
@@ -393,6 +384,40 @@ export class FastTransferSolanaWatcher extends SolanaWatcher {
393384
return { auction: fast_transfer, auction_offer };
394385
}
395386

387+
checkMessageProtocols(logs: string[]): {
388+
message_protocol: FastTransferProtocol;
389+
cctp_domain: number | undefined;
390+
local_program_id: string | undefined;
391+
} {
392+
const auctionUpdate = this.getAuctionUpdatedFromLogs(logs);
393+
if (!auctionUpdate) {
394+
return {
395+
message_protocol: FastTransferProtocol.NONE,
396+
cctp_domain: undefined,
397+
local_program_id: undefined,
398+
};
399+
}
400+
401+
let message_protocol: FastTransferProtocol = FastTransferProtocol.NONE;
402+
let cctp_domain: number | undefined;
403+
let local_program_id: string | undefined;
404+
405+
const { target_protocol } = auctionUpdate;
406+
if (target_protocol.Cctp) {
407+
message_protocol = FastTransferProtocol.CCTP;
408+
cctp_domain = target_protocol.Cctp.domain;
409+
} else if (target_protocol.Local) {
410+
message_protocol = FastTransferProtocol.LOCAL;
411+
local_program_id = target_protocol.Local.program_id.toBase58();
412+
}
413+
414+
return {
415+
message_protocol,
416+
cctp_domain,
417+
local_program_id,
418+
};
419+
}
420+
396421
/**
397422
* This function parses the `improve_offer` instruction
398423
* We can safely assume that the offer price here is better than the previous offer price since
@@ -648,7 +673,7 @@ export class FastTransferSolanaWatcher extends SolanaWatcher {
648673
ix: MessageCompiledInstruction
649674
) {
650675
// Slow relay is not done yet, will implement after
651-
throw new Error('[parseSettleAuctionNoneCctp] not implemented');
676+
throw new Error('[parseSettleAuctionNoneLocal] not implemented');
652677
}
653678

654679
async parseSettleAuctionNoneCctp(
@@ -661,59 +686,76 @@ export class FastTransferSolanaWatcher extends SolanaWatcher {
661686

662687
/*
663688
* `fetchAuction` fetches the auction from the chain first
664-
* if the auction is closed, `matchingEngineProgram.fetchAuction` will throw an error
665-
* we can catch this error and fetch the auction from the auction history
666-
* it's more suitable to use try-catch here because it accounts for when the auction account
667-
* is used to store other data.
689+
* if `auctionAccount` is not null, decode it using borsh program and return
690+
* otherwise, fetch the auction from the auction history
691+
* if no auction is found even from history, return null
668692
*/
669693
async fetchAuction(pubkey: string): Promise<{
670694
vaaHash: string;
671-
info: AuctionInfo;
695+
info: AuctionInfo | null;
672696
} | null> {
673-
try {
674-
const auction = await this.matchingEngineProgram.fetchAuction({
675-
address: new PublicKey(pubkey),
676-
});
677-
678-
if (!auction.info) {
679-
throw new Error('Auction info not found');
680-
}
697+
const auctionAccount = await this.connection?.getAccountInfo(new PublicKey(pubkey));
681698

699+
if (auctionAccount) {
700+
const auctionInfo = this.matchingEngineBorshCoder.accounts.decode(
701+
'Auction',
702+
auctionAccount.data
703+
);
704+
// We need to do this manually because the account info given is in snake_case
682705
return {
683-
vaaHash: Buffer.from(auction.vaaHash).toString('hex'),
684-
info: auction.info,
706+
vaaHash: Buffer.from(auctionInfo.vaa_hash).toString('hex'),
707+
info: {
708+
configId: auctionInfo.info.config_id,
709+
custodyTokenBump: auctionInfo.info.custody_token_bump,
710+
vaaSequence: auctionInfo.info.vaa_sequence,
711+
sourceChain: auctionInfo.info.source_chain,
712+
bestOfferToken: auctionInfo.info.best_offer_token,
713+
initialOfferToken: auctionInfo.info.initial_offer_token,
714+
startSlot: auctionInfo.info.start_slot,
715+
amountIn: auctionInfo.info.amount_in,
716+
securityDeposit: auctionInfo.info.security_deposit,
717+
offerPrice: auctionInfo.info.offer_price,
718+
redeemerMessageLen: auctionInfo.info.redeemer_message_len,
719+
destinationAssetInfo: auctionInfo.info.destination_asset_info,
720+
},
685721
};
686-
} catch (e) {
687-
try {
688-
const auction = await this.fetchAuctionFromHistory(pubkey);
722+
}
689723

690-
if (!auction) {
691-
throw new Error('Auction not found');
692-
}
724+
const auction = await this.fetchAuctionFromHistory(pubkey);
693725

694-
return {
695-
vaaHash: Buffer.from(auction.vaaHash).toString('hex'),
696-
info: auction.info,
697-
};
698-
} catch (e) {
699-
this.logger.error('Failed to fetch auction from history:', e);
700-
}
726+
if (!auction) {
727+
this.logger.error(`[fetchAuction] no auction found for ${pubkey}`);
728+
return null;
701729
}
702730

703-
return null;
731+
return {
732+
vaaHash: Buffer.from(auction.vaaHash).toString('hex'),
733+
info: auction.info,
734+
};
704735
}
705736

706-
async fetchEventFromLogs(name: string, logs: string[]) {
737+
/*
738+
* `getAuctionUpdatedFromLogs` fetches the auction updated event from the logs
739+
* it's used to get the auction info from the auction updated event
740+
* We only need `AuctionUpdated` event for now. If we need more events in the future, we can add them here
741+
*/
742+
getAuctionUpdatedFromLogs(logs: string[]): AuctionUpdated | null {
707743
const parsedLogs = this.eventParser.parseLogs(logs);
708744
for (let event of parsedLogs) {
709-
if (event.name === name) {
745+
if (this.isAuctionUpdatedEvent(event)) {
710746
return event.data;
711747
}
712748
}
713-
714749
return null;
715750
}
716751

752+
/*
753+
* `isAuctionUpdatedEvent` is a type guard that checks if the event is an `AuctionUpdated` event
754+
*/
755+
isAuctionUpdatedEvent(event: Event): event is AuctionUpdatedEvent {
756+
return event.name === 'AuctionUpdated' && event.data !== null && typeof event.data === 'object';
757+
}
758+
717759
/*
718760
* `fetchAuctionFromHistory` fetches the auction from the auction history
719761
* if there is a mapping in the db, we fetch the auction from the auction history using the mapping
@@ -745,7 +787,8 @@ export class FastTransferSolanaWatcher extends SolanaWatcher {
745787
let latestAuctionHistoryIndex = await this.getDbLatestAuctionHistoryIndex();
746788
const auctionHistories = [];
747789

748-
while (true) {
790+
let foundAllAuctionHistory = false;
791+
while (!foundAllAuctionHistory) {
749792
try {
750793
const auctionHistory = await this.matchingEngineProgram.fetchAuctionHistory(
751794
latestAuctionHistoryIndex
@@ -754,8 +797,7 @@ export class FastTransferSolanaWatcher extends SolanaWatcher {
754797
latestAuctionHistoryIndex++;
755798
} catch (error) {
756799
// if no more auction history records to fetch or an error occurred, break the loop
757-
this.logger.error('No more auction history records to fetch or an error occurred:', error);
758-
break;
800+
foundAllAuctionHistory = true;
759801
}
760802
}
761803

@@ -790,7 +832,6 @@ export class FastTransferSolanaWatcher extends SolanaWatcher {
790832

791833
try {
792834
const result = await this.pg('auction_history_mapping').max('index as maxIndex').first();
793-
console.log(result);
794835
return result && result.maxIndex !== null ? BigInt(result.maxIndex) : 0n;
795836
} catch (error) {
796837
this.logger.error('Failed to fetch the largest index from auction_history_mapping:', error);

watcher/src/watchers/__tests__/FTSolanaWatcher.test.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ jest.setTimeout(60_000);
55

66
// This test is working, but testing it is not very useful since the return value is just the lastBlockKey.
77
// It is just an entrypoint to test the whole thing with a local postgres database.
8+
// Skipping because it requires db
89
test.skip('getMessagesByBlock', async () => {
910
const watcher = new FastTransferSolanaWatcher('Testnet');
1011
await watcher.getMessagesByBlock(301864980, 302864980);
@@ -197,7 +198,7 @@ test.skip('should fetch closed Auction', async () => {
197198
const watcher = new FastTransferSolanaWatcher('Testnet');
198199
const auction = await watcher.fetchAuction('FS4EAzWA2WuMKyGBy2C7EBvHL9W63NDX9JR4CPveAiDK');
199200

200-
if (!auction) {
201+
if (!auction || !auction.info) {
201202
throw new Error('Auction not found');
202203
}
203204

@@ -252,7 +253,7 @@ test('should fetch auction update from logs', async () => {
252253
if (!tx.meta?.logMessages) {
253254
throw new Error('No log messages');
254255
}
255-
const auctionUpdate = await watcher.fetchEventFromLogs('AuctionUpdated', tx.meta.logMessages);
256+
const auctionUpdate = await watcher.getAuctionUpdatedFromLogs(tx.meta.logMessages);
256257

257258
if (!auctionUpdate) {
258259
throw new Error('Auction update not found');
@@ -261,7 +262,7 @@ test('should fetch auction update from logs', async () => {
261262
expect({
262263
config_id: 2,
263264
auction: auctionUpdate.auction.toString(),
264-
vaa: auctionUpdate.vaa.toString(),
265+
vaa: auctionUpdate.vaa?.toString(),
265266
source_chain: auctionUpdate.source_chain,
266267
target_protocol: {
267268
Local: {
@@ -295,6 +296,7 @@ test('should fetch auction update from logs', async () => {
295296
});
296297
});
297298

299+
// Skipped because it requires database
298300
test.skip('should index all auction history', async () => {
299301
const watcher = new FastTransferSolanaWatcher('Testnet');
300302
await watcher.indexAuctionHistory('77W4Votv6bK1tyq4xcvyo2V9gXYknXBwcZ53XErgcEs9');

0 commit comments

Comments
 (0)