Skip to content

Commit 9d93433

Browse files
authored
feat(svm-eventsClient): add methods to find deposit and fills from signature (#1033)
Signed-off-by: Gerhard Steenkamp <gerhard@umaproject.org>
1 parent 9b3bb77 commit 9d93433

File tree

4 files changed

+179
-6
lines changed

4 files changed

+179
-6
lines changed

src/arch/svm/eventsClient.ts

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@ import { Idl } from "@coral-xyz/anchor";
22
import { getDeployedAddress, SvmSpokeIdl } from "@across-protocol/contracts";
33
import { getSolanaChainId } from "@across-protocol/contracts/dist/src/svm/web3-v1";
44
import web3, { Address, Commitment, GetSignaturesForAddressApi, GetTransactionApi, Signature } from "@solana/kit";
5-
import { bs58 } from "../../utils";
5+
import { bs58, chainIsSvm, getMessageHash } from "../../utils";
66
import { EventName, EventWithData, SVMProvider } from "./types";
77
import { decodeEvent, isDevnet } from "./utils";
8+
import { DepositWithTime, FillWithTime } from "../../interfaces";
9+
import { unwrapEventData } from "./";
10+
import assert from "assert";
11+
import {
12+
FundsDepositedEventObject,
13+
FilledRelayEventObject,
14+
} from "@across-protocol/contracts/dist/typechain/contracts/SpokePool";
815

916
// Utility type to extract the return type for the JSON encoding overload. We only care about the overload where the
1017
// configuration parameter (C) has the optional property 'encoding' set to 'json'.
@@ -19,6 +26,9 @@ type GetSignaturesForAddressConfig = Parameters<GetSignaturesForAddressApi["getS
1926
type GetSignaturesForAddressTransaction = ReturnType<GetSignaturesForAddressApi["getSignaturesForAddress"]>[number];
2027
type GetSignaturesForAddressApiResponse = readonly GetSignaturesForAddressTransaction[];
2128

29+
export type DepositEventFromSignature = Omit<DepositWithTime, "fromLiteChain" | "toLiteChain">;
30+
export type FillEventFromSignature = FillWithTime;
31+
2232
export class SvmCpiEventsClient {
2333
private rpc: SVMProvider;
2434
private programAddress: Address;
@@ -211,6 +221,109 @@ export class SvmCpiEventsClient {
211221
return events;
212222
}
213223

224+
/**
225+
* Finds all FundsDeposited events for a given transaction signature.
226+
*
227+
* @param originChainId - The chain ID where the deposit originated.
228+
* @param txSignature - The transaction signature to search for events.
229+
* @param commitment - Optional commitment level for the transaction query.
230+
* @returns A promise that resolves to an array of deposit events for the transaction, or undefined if none found.
231+
*/
232+
public async getDepositEventsFromSignature(
233+
originChainId: number,
234+
txSignature: Signature,
235+
commitment: Commitment = "confirmed"
236+
): Promise<DepositEventFromSignature[] | undefined> {
237+
assert(chainIsSvm(originChainId), `Origin chain ${originChainId} is not an SVM chain`);
238+
239+
const [events, txDetails] = await Promise.all([
240+
this.readEventsFromSignature(txSignature, commitment),
241+
this.rpc
242+
.getTransaction(txSignature, {
243+
commitment,
244+
maxSupportedTransactionVersion: 0,
245+
})
246+
.send(),
247+
]);
248+
249+
// Filter for FundsDeposited events only
250+
const depositEvents = events?.filter((event) => event?.name === "FundsDeposited");
251+
252+
if (!txDetails || !depositEvents?.length) {
253+
return;
254+
}
255+
256+
return events.map((event) => {
257+
const unwrappedEventArgs = unwrapEventData(event as Record<string, unknown>, ["depositId"]) as Record<
258+
"data",
259+
FundsDepositedEventObject
260+
>;
261+
262+
return {
263+
...unwrappedEventArgs.data,
264+
depositTimestamp: Number(txDetails.blockTime),
265+
originChainId,
266+
messageHash: getMessageHash(unwrappedEventArgs.data.message),
267+
blockNumber: Number(txDetails.slot),
268+
txnIndex: 0,
269+
txnRef: txSignature,
270+
logIndex: 0,
271+
destinationChainId: unwrappedEventArgs.data.destinationChainId.toNumber(),
272+
} satisfies DepositEventFromSignature;
273+
});
274+
}
275+
276+
/**
277+
* Finds all FilledRelay events for a given transaction signature.
278+
*
279+
* @param destinationChainId - The destination chain ID (must be an SVM chain).
280+
* @param txSignature - The transaction signature to search for events.
281+
* @returns A promise that resolves to an array of fill events for the transaction, or undefined if none found.
282+
*/
283+
public async getFillEventsFromSignature(
284+
destinationChainId: number,
285+
txSignature: Signature,
286+
commitment: Commitment = "confirmed"
287+
): Promise<FillEventFromSignature[] | undefined> {
288+
assert(chainIsSvm(destinationChainId), `Destination chain ${destinationChainId} is not an SVM chain`);
289+
290+
// Find all events from the transaction signature and get transaction details
291+
const [events, txDetails] = await Promise.all([
292+
this.readEventsFromSignature(txSignature, commitment),
293+
this.rpc
294+
.getTransaction(txSignature, {
295+
commitment,
296+
maxSupportedTransactionVersion: 0,
297+
})
298+
.send(),
299+
]);
300+
301+
// Filter for FilledRelay events only
302+
const fillEvents = events?.filter((event) => event?.name === "FilledRelay");
303+
304+
if (!txDetails || !fillEvents?.length) {
305+
return;
306+
}
307+
308+
return fillEvents.map((event) => {
309+
const unwrappedEventData = unwrapEventData(event as Record<string, unknown>) as Record<
310+
"data",
311+
FilledRelayEventObject
312+
>;
313+
return {
314+
...unwrappedEventData.data,
315+
fillTimestamp: Number(txDetails.blockTime),
316+
blockNumber: Number(txDetails.slot),
317+
txnRef: txSignature,
318+
txnIndex: 0,
319+
logIndex: 0,
320+
destinationChainId,
321+
repaymentChainId: unwrappedEventData.data.repaymentChainId.toNumber(),
322+
originChainId: unwrappedEventData.data.originChainId.toNumber(),
323+
} satisfies FillEventFromSignature;
324+
});
325+
}
326+
214327
public getProgramAddress(): Address {
215328
return this.programAddress;
216329
}

src/interfaces/SpokePool.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ export interface DepositWithBlock extends Deposit, SortableEvent {
3737
quoteBlockNumber: number;
3838
}
3939

40+
export interface DepositWithTime extends Deposit, SortableEvent {
41+
depositTimestamp: number;
42+
}
43+
4044
export enum FillStatus {
4145
Unfilled = 0,
4246
RequestedSlowFill,
@@ -66,6 +70,9 @@ export interface Fill extends Omit<RelayData, "message"> {
6670
}
6771

6872
export interface FillWithBlock extends Fill, SortableEvent {}
73+
export interface FillWithTime extends Fill, SortableEvent {
74+
fillTimestamp: number;
75+
}
6976

7077
export interface EnabledDepositRoute {
7178
originToken: string;

src/utils/SpokeUtils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { encodeAbiParameters, keccak256 } from "viem";
1+
import { encodeAbiParameters, Hex, keccak256 } from "viem";
22
import { MAX_SAFE_DEPOSIT_ID, ZERO_ADDRESS, ZERO_BYTES } from "../constants";
33
import { Deposit, RelayData } from "../interfaces";
44
import { toBytes32 } from "./AddressUtils";
@@ -91,5 +91,5 @@ export function isZeroAddress(address: string): boolean {
9191
}
9292

9393
export function getMessageHash(message: string): string {
94-
return isMessageEmpty(message) ? ZERO_BYTES : keccak256(message as "0x{string}");
94+
return isMessageEmpty(message) ? ZERO_BYTES : keccak256(message as Hex);
9595
}

test/Solana.SvmCpiEventsClient.Integration.test.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { SvmSpokeClient } from "@across-protocol/contracts";
2-
import { getSolanaChainId, intToU8Array32 } from "@across-protocol/contracts/dist/src/svm/web3-v1";
2+
import { getSolanaChainId, intToU8Array32, u8Array32ToInt } from "@across-protocol/contracts/dist/src/svm/web3-v1";
33
import { SYSTEM_PROGRAM_ADDRESS } from "@solana-program/system";
44
import { ASSOCIATED_TOKEN_PROGRAM_ADDRESS, TOKEN_2022_PROGRAM_ADDRESS } from "@solana-program/token-2022";
5-
import { Address, KeyPairSigner } from "@solana/kit";
5+
import { address, Address, KeyPairSigner } from "@solana/kit";
66
import { expect } from "chai";
77
import { BigNumber } from "ethers";
88
import { arrayify, hexlify } from "ethers/lib/utils";
@@ -29,6 +29,12 @@ import {
2929
requestSlowFill,
3030
} from "./utils/svm/utils";
3131
import { validatorSetup, validatorTeardown } from "./utils/svm/validator.setup";
32+
import { CHAIN_IDs } from "@across-protocol/constants";
33+
34+
// Define an extended interface for our Solana client with chainId
35+
interface ExtendedSolanaClient extends ReturnType<typeof createDefaultSolanaClient> {
36+
chainId: number;
37+
}
3238

3339
const formatRelayData = (relayData: SvmSpokeClient.RelayDataArgs): RelayData => {
3440
return {
@@ -52,7 +58,9 @@ const getRandomInt = (min: number = 0, max: number = 1000000) => {
5258
};
5359

5460
describe("SvmCpiEventsClient (integration)", () => {
55-
const solanaClient = createDefaultSolanaClient();
61+
const solanaClient = createDefaultSolanaClient() as ExtendedSolanaClient;
62+
// Add chainId property for tests
63+
solanaClient.chainId = 7777; // Use a test value for Solana testnet
5664
let client: SvmCpiEventsClient;
5765

5866
let signer: KeyPairSigner;
@@ -328,4 +336,49 @@ describe("SvmCpiEventsClient (integration)", () => {
328336
expect(data.fillDeadline).to.equal(relayData.fillDeadline);
329337
expect(data.exclusivityDeadline).to.equal(relayData.exclusivityDeadline);
330338
});
339+
340+
it("gets deposit events from transaction signature", async () => {
341+
// deposit from solana
342+
solanaClient.chainId = CHAIN_IDs.SOLANA;
343+
const payerAta = await mintTokens(signer, solanaClient, address(mint.address), tokenAmount);
344+
const { depositInput, signature } = await sendCreateDeposit(payerAta, tokenAmount, tokenAmount);
345+
346+
const depositEvents = await client.getDepositEventsFromSignature(solanaClient.chainId, signature);
347+
348+
expect(depositEvents).to.have.lengthOf(1);
349+
const depositEvent = depositEvents![0];
350+
expect(SvmAddress.from(depositEvent.depositor, "base16").toBase58()).to.equal(depositInput.depositor.toString());
351+
expect(SvmAddress.from(depositEvent.recipient, "base16").toBase58()).to.equal(depositInput.recipient.toString());
352+
expect(SvmAddress.from(depositEvent.inputToken, "base16").toBase58()).to.equal(depositInput.inputToken.toString());
353+
expect(SvmAddress.from(depositEvent.outputToken, "base16").toBase58()).to.equal(
354+
depositInput.outputToken.toString()
355+
);
356+
expect(depositEvent.inputAmount.toString()).to.equal(depositInput.inputAmount.toString());
357+
expect(depositEvent.outputAmount.toString()).to.equal(depositInput.outputAmount.toString());
358+
expect(depositEvent.destinationChainId).to.equal(depositInput.destinationChainId);
359+
});
360+
361+
it("gets fill events from transaction signature", async () => {
362+
solanaClient.chainId = CHAIN_IDs.SOLANA;
363+
364+
await mintTokens(signer, solanaClient, address(mint.address), tokenAmount);
365+
366+
const { relayData, signature } = await sendCreateFill();
367+
368+
const fillEvents = await client.getFillEventsFromSignature(solanaClient.chainId, signature);
369+
370+
expect(fillEvents).to.have.lengthOf(1);
371+
const fillEvent = fillEvents![0];
372+
373+
expect(SvmAddress.from(fillEvent.depositor, "base16").toBase58()).to.equal(relayData.depositor.toString());
374+
expect(SvmAddress.from(fillEvent.recipient, "base16").toBase58()).to.equal(relayData.recipient.toString());
375+
expect(SvmAddress.from(fillEvent.inputToken, "base16").toBase58()).to.equal(relayData.inputToken.toString());
376+
expect(SvmAddress.from(fillEvent.outputToken, "base16").toBase58()).to.equal(relayData.outputToken.toString());
377+
expect(fillEvent.inputAmount.toString()).to.equal(BigInt(relayData.inputAmount).toString());
378+
expect(fillEvent.outputAmount.toString()).to.equal(BigInt(relayData.outputAmount).toString());
379+
expect(fillEvent.originChainId).to.equal(Number(relayData.originChainId));
380+
expect(fillEvent.depositId).to.equal(u8Array32ToInt(relayData.depositId));
381+
expect(fillEvent.fillDeadline).to.equal(Number(relayData.fillDeadline));
382+
expect(fillEvent.exclusivityDeadline).to.equal(Number(relayData.exclusivityDeadline));
383+
});
331384
});

0 commit comments

Comments
 (0)