Skip to content

Commit 5908cfd

Browse files
authored
feat: improve event utils (#789)
1 parent 550ca5e commit 5908cfd

12 files changed

+162
-130
lines changed

src/SvmUtils.ts

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
AddressLookupTableProgram,
2424
VersionedTransaction,
2525
TransactionMessage,
26+
ConfirmedSignatureInfo,
2627
} from "@solana/web3.js";
2728

2829
export function findProgramAddress(label: string, program: PublicKey, extraSeeds?: string[]) {
@@ -50,13 +51,14 @@ export async function readEvents<IDL extends Idl = Idl>(
5051
programs: Program<IDL>[],
5152
commitment: Finality = "confirmed"
5253
) {
53-
const txResult = await connection.getTransaction(txSignature, {
54-
commitment,
55-
maxSupportedTransactionVersion: 0,
56-
});
54+
const txResult = await connection.getTransaction(txSignature, { commitment, maxSupportedTransactionVersion: 0 });
5755

5856
if (txResult === null) return [];
5957

58+
return processEventFromTx(txResult, programs);
59+
}
60+
61+
function processEventFromTx(txResult: web3.VersionedTransactionResponse, programs: Program<any>[]) {
6062
const eventAuthorities: Map<string, PublicKey> = new Map();
6163
for (const program of programs) {
6264
eventAuthorities.set(
@@ -95,10 +97,32 @@ export async function readEvents<IDL extends Idl = Idl>(
9597
}
9698
}
9799
}
98-
99100
return events;
100101
}
101102

103+
// Helper function to wait for an event to be emitted. Should only be used in tests where txSignature is known to emit.
104+
export async function readEventsUntilFound<IDL extends Idl = Idl>(
105+
connection: Connection,
106+
txSignature: string,
107+
programs: Program<IDL>[]
108+
) {
109+
const startTime = Date.now();
110+
let txResult = null;
111+
112+
while (Date.now() - startTime < 5000) {
113+
// 5 seconds timeout to wait to find the event.
114+
txResult = await connection.getTransaction(txSignature, {
115+
commitment: "confirmed",
116+
maxSupportedTransactionVersion: 0,
117+
});
118+
if (txResult !== null) return processEventFromTx(txResult, programs);
119+
120+
await new Promise((resolve) => setTimeout(resolve, 50)); // 50 ms delay between retries.
121+
}
122+
123+
throw new Error("No event found within 5 seconds");
124+
}
125+
102126
export function getEvent(events: any[], program: PublicKey, eventName: string) {
103127
for (const event of events) {
104128
if (event.name === eventName && program.toString() === event.program.toString()) {
@@ -108,19 +132,53 @@ export function getEvent(events: any[], program: PublicKey, eventName: string) {
108132
throw new Error("Event " + eventName + " not found");
109133
}
110134

135+
export interface EventType {
136+
program: PublicKey;
137+
data: any;
138+
name: string;
139+
slot: number;
140+
confirmationStatus: string;
141+
blockTime: number;
142+
signature: string;
143+
}
144+
111145
export async function readProgramEvents(
112146
connection: Connection,
113147
program: Program<any>,
114-
options?: SignaturesForAddressOptions,
115-
finality: Finality = "confirmed"
116-
) {
117-
const events = [];
118-
const pastSignatures = await connection.getSignaturesForAddress(program.programId, options, finality);
148+
finality: Finality = "confirmed",
149+
options: SignaturesForAddressOptions = { limit: 1000 }
150+
): Promise<EventType[]> {
151+
const allSignatures: ConfirmedSignatureInfo[] = [];
152+
153+
// Fetch all signatures in sequential batches
154+
while (true) {
155+
const signatures = await connection.getSignaturesForAddress(program.programId, options, finality);
156+
allSignatures.push(...signatures);
157+
158+
// Update options for the next batch. Set before to the last fetched signature.
159+
if (signatures.length > 0) {
160+
options = { ...options, before: signatures[signatures.length - 1].signature };
161+
}
119162

120-
for (const signature of pastSignatures) {
121-
events.push(...(await readEvents(connection, signature.signature, [program], finality)));
163+
if (options.limit && signatures.length < options.limit) break; // Exit early if the number of signatures < limit
122164
}
123-
return events;
165+
166+
// Fetch events for all signatures in parallel
167+
const eventsWithSlots = await Promise.all(
168+
allSignatures.map(async (signature) => {
169+
const events = await readEvents(connection, signature.signature, [program], finality);
170+
return events.map((event) => ({
171+
...event,
172+
confirmationStatus: signature.confirmationStatus || "Unknown",
173+
blockTime: signature.blockTime || 0,
174+
signature: signature.signature,
175+
slot: signature.slot,
176+
name: event.name || "Unknown",
177+
}));
178+
})
179+
);
180+
181+
return eventsWithSlots.flat(); // Flatten the array of events & return.
124182
}
125183

126184
export async function subscribeToCpiEventsForProgram(

test/svm/SvmSpoke.Bundle.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ import {
2222
buildRelayerRefundMerkleTree,
2323
loadExecuteRelayerRefundLeafParams,
2424
randomBigInt,
25-
readEvents,
26-
readProgramEvents,
25+
readEventsUntilFound,
2726
relayerRefundHashFn,
2827
RelayerRefundLeafSolana,
2928
RelayerRefundLeafType,
@@ -186,11 +185,8 @@ describe("svm_spoke.bundle", () => {
186185
.accounts(relayRootBundleAccounts)
187186
.rpc();
188187

189-
// Wait for event processing
190-
await new Promise((resolve) => setTimeout(resolve, 1000));
191-
192188
// Check for the emitted event
193-
const events = await readEvents(connection, tx, [program]);
189+
let events = await readEventsUntilFound(connection, tx, [program]);
194190
const event = events.find((event) => event.name === "relayedRootBundle")?.data;
195191
assert.isTrue(event.rootBundleId.toString() === rootBundleId.toString(), "Root bundle ID should match");
196192
assert.isTrue(
@@ -255,17 +251,15 @@ describe("svm_spoke.bundle", () => {
255251
};
256252
const proofAsNumbers = proof.map((p) => Array.from(p));
257253
await loadExecuteRelayerRefundLeafParams(program, owner, stateAccountData.rootBundleId, leaf, proofAsNumbers);
258-
await program.methods
254+
const tx = await program.methods
259255
.executeRelayerRefundLeaf()
260256
.accounts(executeRelayerRefundLeafAccounts)
261257
.remainingAccounts(remainingAccounts)
262258
.rpc();
263259

264260
// Verify the ExecutedRelayerRefundRoot event
265-
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for event processing
266-
let events = await readProgramEvents(connection, program);
261+
let events = await readEventsUntilFound(connection, tx, [program]);
267262
let event = events.find((event) => event.name === "executedRelayerRefundRoot")?.data;
268-
269263
// Remove the expectedValues object and use direct assertions
270264
assertSE(event.amountToReturn, relayerRefundLeaves[0].amountToReturn, "amountToReturn should match");
271265
assertSE(event.chainId, chainId, "chainId should match");
@@ -280,7 +274,6 @@ describe("svm_spoke.bundle", () => {
280274
assertSE(event.caller, owner, "caller should match");
281275

282276
event = events.find((event) => event.name === "tokensBridged")?.data;
283-
284277
assertSE(event.amountToReturn, relayerRefundLeaves[0].amountToReturn, "amountToReturn should match");
285278
assertSE(event.chainId, chainId, "chainId should match");
286279
assertSE(event.leafId, leaf.leafId, "leafId should match");
@@ -1486,17 +1479,14 @@ describe("svm_spoke.bundle", () => {
14861479
it("No deferred refunds in all Token Accounts", async () => {
14871480
const tx = await executeRelayerRefundLeaf({ deferredRefunds: false });
14881481

1489-
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for event processing
1490-
const events = await readEvents(connection, tx, [program]);
1482+
const events = await readEventsUntilFound(connection, tx, [program]);
14911483
const event = events.find((event) => event.name === "executedRelayerRefundRoot")?.data;
14921484
assert.isFalse(event.deferredRefunds, "deferredRefunds should be false");
14931485
});
14941486

14951487
it("Deferred refunds in all Claim Accounts", async () => {
14961488
const tx = await executeRelayerRefundLeaf({ deferredRefunds: true });
1497-
1498-
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for event processing
1499-
const events = await readEvents(connection, tx, [program]);
1489+
const events = await readEventsUntilFound(connection, tx, [program]);
15001490
const event = events.find((event) => event.name === "executedRelayerRefundRoot")?.data;
15011491
assert.isTrue(event.deferredRefunds, "deferredRefunds should be true");
15021492
});
@@ -1657,7 +1647,6 @@ describe("svm_spoke.bundle", () => {
16571647
.rpc();
16581648
assert.fail("Leaf verification should fail without leading 64 bytes");
16591649
} catch (err: any) {
1660-
console.log("err", err);
16611650
assert.include(err.toString(), "Invalid Merkle proof", "Expected merkle verification to fail");
16621651
}
16631652
});
@@ -1749,7 +1738,6 @@ describe("svm_spoke.bundle", () => {
17491738
.rpc();
17501739
assert.fail("Leaf verification should fail without leading 64 bytes");
17511740
} catch (err: any) {
1752-
console.log("err", err);
17531741
assert.include(err.toString(), "Invalid Merkle proof", "Expected merkle verification to fail");
17541742
}
17551743
});

test/svm/SvmSpoke.Deposit.ts

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from "@solana/spl-token";
1717
import { PublicKey, Keypair, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
1818
import { common, DepositDataValues } from "./SvmSpoke.common";
19-
import { readProgramEvents, intToU8Array32, u8Array32ToInt } from "./utils";
19+
import { readEventsUntilFound, intToU8Array32 } from "./utils";
2020
const { provider, connection, program, owner, seedBalance, initializeState, depositData } = common;
2121
const { createRoutePda, getVaultAta, assertSE, assert, getCurrentTime, depositQuoteTimeBuffer, fillDeadlineBuffer } =
2222
common;
@@ -121,7 +121,8 @@ describe("svm_spoke.deposit", () => {
121121
.accounts(calledDepositAccounts)
122122
.instruction();
123123
const depositTx = new Transaction().add(approveIx, depositIx);
124-
await sendAndConfirmTransaction(connection, depositTx, [payer, depositor]);
124+
const tx = await sendAndConfirmTransaction(connection, depositTx, [payer, depositor]);
125+
return tx;
125126
};
126127

127128
beforeEach(async () => {
@@ -183,10 +184,9 @@ describe("svm_spoke.deposit", () => {
183184

184185
// Execute the first deposit_v3 call
185186
let depositDataValues = Object.values(depositData) as DepositDataValues;
186-
await approvedDepositV3(depositDataValues);
187-
await new Promise((resolve) => setTimeout(resolve, 500));
187+
const tx = await approvedDepositV3(depositDataValues);
188188

189-
let events = await readProgramEvents(connection, program);
189+
let events = await readEventsUntilFound(connection, tx, [program]);
190190
let event = events[0].data; // 0th event is the latest event
191191
const expectedValues1 = { ...depositData, depositId: intToU8Array32(1) }; // Verify the event props emitted match the depositData.
192192
for (let [key, value] of Object.entries(expectedValues1)) {
@@ -195,9 +195,8 @@ describe("svm_spoke.deposit", () => {
195195
}
196196

197197
// Execute the second deposit_v3 call
198-
await approvedDepositV3(depositDataValues);
199-
await new Promise((resolve) => setTimeout(resolve, 500));
200-
events = await readProgramEvents(connection, program);
198+
const tx2 = await approvedDepositV3(depositDataValues);
199+
events = await readEventsUntilFound(connection, tx2, [program]);
201200
event = events[0].data; // 0th event is the latest event.
202201

203202
const expectedValues2 = { ...expectedValues1, depositId: intToU8Array32(2) }; // Verify the event props emitted match the depositData.
@@ -364,8 +363,6 @@ describe("svm_spoke.deposit", () => {
364363

365364
await program.methods.setEnableRoute(inputToken, fakeRouteChainId, true).accounts(fakeSetEnableRouteAccounts).rpc();
366365

367-
await new Promise((resolve) => setTimeout(resolve, 2000));
368-
369366
const fakeDepositAccounts = {
370367
state: fakeState.state,
371368
route: fakeRoutePda,
@@ -382,11 +379,9 @@ describe("svm_spoke.deposit", () => {
382379
...depositData,
383380
destinationChainId: fakeRouteChainId,
384381
}) as DepositDataValues;
385-
await approvedDepositV3(depositDataValues, fakeDepositAccounts);
386-
387-
await new Promise((resolve) => setTimeout(resolve, 500));
382+
const tx = await approvedDepositV3(depositDataValues, fakeDepositAccounts);
388383

389-
let events = await readProgramEvents(connection, program);
384+
let events = await readEventsUntilFound(connection, tx, [program]);
390385
let event = events[0].data; // 0th event is the latest event.
391386
const expectedValues = {
392387
...{ ...depositData, destinationChainId: fakeRouteChainId },
@@ -457,11 +452,9 @@ describe("svm_spoke.deposit", () => {
457452
.accounts(depositAccounts)
458453
.instruction();
459454
const depositTx = new Transaction().add(approveIx, depositIx);
460-
await sendAndConfirmTransaction(connection, depositTx, [payer, depositor]);
455+
const tx = await sendAndConfirmTransaction(connection, depositTx, [payer, depositor]);
461456

462-
await new Promise((resolve) => setTimeout(resolve, 500));
463-
464-
const events = await readProgramEvents(connection, program);
457+
const events = await readEventsUntilFound(connection, tx, [program]);
465458
const event = events[0].data; // 0th event is the latest event.
466459

467460
// Verify the event props emitted match the expected values
@@ -526,10 +519,9 @@ describe("svm_spoke.deposit", () => {
526519
depositData.exclusivityParameter = maxExclusivityOffsetSeconds;
527520

528521
const depositDataValues = Object.values(depositData) as DepositDataValues;
529-
await approvedDepositV3(depositDataValues);
522+
const tx = await approvedDepositV3(depositDataValues);
530523

531-
await new Promise((resolve) => setTimeout(resolve, 500));
532-
const events = await readProgramEvents(connection, program);
524+
const events = await readEventsUntilFound(connection, tx, [program]);
533525
const event = events[0].data; // 0th event is the latest event
534526
assertSE(
535527
event.exclusivityDeadline,
@@ -547,10 +539,9 @@ describe("svm_spoke.deposit", () => {
547539
depositData.exclusivityParameter = exclusivityDeadlineTimestamp;
548540

549541
const depositDataValues = Object.values(depositData) as DepositDataValues;
550-
await approvedDepositV3(depositDataValues);
542+
const tx = await approvedDepositV3(depositDataValues);
551543

552-
await new Promise((resolve) => setTimeout(resolve, 500));
553-
const events = await readProgramEvents(connection, program);
544+
const events = await readEventsUntilFound(connection, tx, [program]);
554545
const event = events[0].data; // 0th event is the latest event;
555546

556547
assertSE(event.exclusivityDeadline, exclusivityDeadlineTimestamp, "exclusivityDeadline should be passed in time");
@@ -565,10 +556,9 @@ describe("svm_spoke.deposit", () => {
565556
depositData.exclusivityParameter = zeroExclusivity;
566557

567558
const depositDataValues = Object.values(depositData) as DepositDataValues;
568-
await approvedDepositV3(depositDataValues);
559+
const tx = await approvedDepositV3(depositDataValues);
569560

570-
await new Promise((resolve) => setTimeout(resolve, 500));
571-
const events = await readProgramEvents(connection, program);
561+
const events = await readEventsUntilFound(connection, tx, [program]);
572562
const event = events[0].data; // 0th event is the latest event;
573563

574564
assertSE(event.exclusivityDeadline, zeroExclusivity, "Exclusivity deadline should always be 0");
@@ -629,13 +619,12 @@ describe("svm_spoke.deposit", () => {
629619
.instruction();
630620

631621
const unsafeDepositTx = new Transaction().add(approveIx, unsafeDepositIx);
632-
await sendAndConfirmTransaction(connection, unsafeDepositTx, [payer, depositor]);
622+
const tx = await sendAndConfirmTransaction(connection, unsafeDepositTx, [payer, depositor]);
633623

634624
// Wait for a short period to ensure the event is emitted
635-
await new Promise((resolve) => setTimeout(resolve, 500));
636625

637626
// Read and verify the event
638-
const events = await readProgramEvents(connection, program);
627+
const events = await readEventsUntilFound(connection, tx, [program]);
639628
const event = events[0].data; // Assuming the latest event is the one we want
640629

641630
const expectedValues = { ...depositData, depositId: expectedDepositIdArray };

test/svm/SvmSpoke.Fill.AcrossPlus.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ describe("svm_spoke.fill.across_plus", () => {
255255
await sendTransactionWithLookupTable(connection, [computeBudgetIx, approveIx, fillIx], relayer);
256256

257257
// Verify relayer's balance after the fill
258-
await new Promise((resolve) => setTimeout(resolve, 1000)); // Make sure token transfers get processed.
258+
await new Promise((resolve) => setTimeout(resolve, 500)); // Make sure token transfers get processed.
259259
const fRelayerBal = (await getAccount(connection, relayerATA)).amount;
260260
assertSE(
261261
fRelayerBal,

0 commit comments

Comments
 (0)