diff --git a/src/utils/cctp.ts b/src/utils/cctp.ts index 9c03fb086..5e9542bbe 100644 --- a/src/utils/cctp.ts +++ b/src/utils/cctp.ts @@ -9,6 +9,8 @@ import { import { DepositData, DepositForBurnEvent, + MintAndWithdrawEvent, + FillMetadata, } from "views/DepositStatus/hooks/useDepositTracking/types"; import { TokenMessengerMinterV2Client, @@ -257,6 +259,20 @@ export class SvmCctpEventsClient extends SvmCpiEventsClient { const events = await this.readEventsFromSignature(signature); return events.filter((event) => event.name === "DepositForBurn"); } + + async queryMintAndWithdrawEvents(fromSlot?: bigint, toSlot?: bigint) { + return await this.queryEvents( + "MintAndWithdraw" as any, // Maybe override queryEvents method + fromSlot, + toSlot, + { limit: 1000, commitment: "confirmed" } + ); + } + + async getMintAndWithdrawEventsFromSignature(signature: Signature) { + const events = await this.readEventsFromSignature(signature); + return events.filter((event) => event.name === "MintAndWithdraw"); + } } export async function getDepositForBurnBySignatureSVM({ @@ -320,3 +336,39 @@ export async function getDepositForBurnBySignatureSVM({ return convertedLog; } } + +export async function getMintAndBurnBySignatureSVM({ + signature, + chainId, +}: { + signature: Signature; + chainId: number; +}): Promise { + // init events client + const eventsClient = await SvmCctpEventsClient.create(); + const rpc = getSVMRpc(chainId); + const [mintAndWithdrawEvents, fillTx] = await Promise.all([ + eventsClient.getMintAndWithdrawEventsFromSignature(signature), + rpc + .getTransaction(signature, { + commitment: "confirmed", + maxSupportedTransactionVersion: 0, + }) + .send(), + ]); + + if (mintAndWithdrawEvents?.length) { + const event = mintAndWithdrawEvents[0]; + const data = event.data as MintAndWithdrawEvent; + + const blockTimestamp = Number(fillTx?.blockTime); + + const metadata: FillMetadata = { + fillTxHash: signature, + fillTxTimestamp: blockTimestamp, + outputAmount: BigNumber.from(data.amount?.toString?.() ?? "0"), + }; + + return metadata; + } +} diff --git a/src/utils/fills.ts b/src/utils/fills.ts new file mode 100644 index 000000000..e66683dd4 --- /dev/null +++ b/src/utils/fills.ts @@ -0,0 +1,32 @@ +import { Signature } from "@solana/kit"; +import { getSVMRpc } from "./providers"; +import { SvmCpiEventsClient } from "./sdk"; +import { FillMetadata } from "views/DepositStatus/hooks/useDepositTracking/types"; + +// ====================================================== // +// ========================= SVM ======================== // +// ====================================================== // + +// get FilledRelay event from SVM Spoke by transaction signature +export async function getFillTxBySignature({ + signature, + chainId, +}: { + signature: Signature; + chainId: number; +}): Promise { + const rpc = getSVMRpc(chainId); + const eventsClient = await SvmCpiEventsClient.create(rpc); + const fillTx = await eventsClient.getFillEventsFromSignature( + chainId, + signature + ); + + const data = fillTx?.[0]; + + return { + fillTxHash: signature, + fillTxTimestamp: Number(data?.fillTimestamp), + outputAmount: data?.outputAmount, + }; +} diff --git a/src/utils/sdk.ts b/src/utils/sdk.ts index 65622bd7b..3db556429 100644 --- a/src/utils/sdk.ts +++ b/src/utils/sdk.ts @@ -34,7 +34,10 @@ export { export { getMessageHash } from "@across-protocol/sdk/dist/esm/utils/SpokeUtils"; export { SvmCpiEventsClient } from "@across-protocol/sdk/dist/esm/arch/svm/eventsClient"; export { findFillEvent } from "@across-protocol/sdk/dist/esm/arch/svm/SpokeUtils"; -export { bigToU8a32 } from "@across-protocol/sdk/dist/esm/arch/svm/utils"; +export { + bigToU8a32, + getNearestSlotTime, +} from "@across-protocol/sdk/dist/esm/arch/svm/utils"; export { paginatedEventQuery } from "@across-protocol/sdk/dist/esm/utils/EventUtils"; export { getCctpDestinationChainFromDomain } from "@across-protocol/sdk/dist/esm/utils/CCTPUtils"; export type { SVMProvider } from "@across-protocol/sdk/dist/esm/arch/svm/types"; diff --git a/src/views/DepositStatus/hooks/useDepositTracking/strategies/svm.ts b/src/views/DepositStatus/hooks/useDepositTracking/strategies/svm.ts index cf2c7a506..a4aa952d7 100644 --- a/src/views/DepositStatus/hooks/useDepositTracking/strategies/svm.ts +++ b/src/views/DepositStatus/hooks/useDepositTracking/strategies/svm.ts @@ -8,6 +8,7 @@ import { DepositStatusResponse, FillInfo, IChainStrategy, + FillMetadata, } from "../types"; import { findFillEvent, @@ -26,7 +27,11 @@ import { BigNumber } from "ethers"; import { SvmSpokeClient } from "@across-protocol/contracts"; import { hexlify } from "ethers/lib/utils"; import { isHex } from "viem"; -import { getDepositForBurnBySignatureSVM } from "utils/cctp"; +import { + getDepositForBurnBySignatureSVM, + getMintAndBurnBySignatureSVM, +} from "utils/cctp"; +import { getFillTxBySignature } from "utils/fills"; /** * Strategy for handling Solana (SVM) chain operations @@ -84,21 +89,33 @@ export class SVMStrategy implements IChainStrategy { * @param depositInfo Deposit information * @returns Fill information */ - async getFill(depositInfo: DepositedInfo): Promise { + async getFill( + depositInfo: DepositedInfo, + bridgeProvider: BridgeProvider = "across" + ): Promise { const { depositId } = depositInfo.depositLog; const fillChainId = this.chainId; try { const fillTxSignature = await Promise.any([ this.getFillFromIndexer(depositInfo), - this.getFillFromRpc(depositInfo), + this.getFillFromRpc(depositInfo, bridgeProvider), ]); if (!fillTxSignature) { throw new NoFilledRelayLogError(Number(depositId), fillChainId); } - const metadata = await this.getFillMetadata(fillTxSignature); + const metadata = await this.getFillMetadata( + fillTxSignature, + bridgeProvider + ); + + if (!metadata) { + throw new Error( + `Unable to parse fill tx logs for signature: ${fillTxSignature}` + ); + } return { fillTxHash: metadata.fillTxHash, @@ -153,7 +170,10 @@ export class SVMStrategy implements IChainStrategy { * @param depositInfo Deposit information * @returns Fill transaction hash (signature) */ - async getFillFromRpc(depositInfo: DepositedInfo): Promise { + async getFillFromRpc( + depositInfo: DepositedInfo, + bridgeProvider: BridgeProvider + ): Promise { const { depositId } = depositInfo.depositLog; try { @@ -166,20 +186,28 @@ export class SVMStrategy implements IChainStrategy { ) )?.number; - const formattedRelayData = this.formatRelayData(depositInfo.depositLog); - - const fillEvent = await findFillEvent( - formattedRelayData, - this.chainId, - eventsClient, - fromSlot - ); + let fillEventTxRef = ""; - if (!fillEvent) { + if (bridgeProvider === "cctp") { + // todo throw new NoFilledRelayLogError(Number(depositId), this.chainId); + } else { + const formattedRelayData = this.formatRelayData(depositInfo.depositLog); + + const fillEvent = await findFillEvent( + formattedRelayData, + this.chainId, + eventsClient, + fromSlot + ); + + if (!fillEvent) { + throw new NoFilledRelayLogError(Number(depositId), this.chainId); + } + fillEventTxRef = fillEvent.txnRef; } - return fillEvent.txnRef; + return fillEventTxRef; } catch (error) { console.error("Error fetching Solana fill from RPC:", error); throw error; @@ -191,25 +219,25 @@ export class SVMStrategy implements IChainStrategy { * @param fillTxHash Fill transaction hash (signature) * @returns Fill metadata */ - async getFillMetadata(fillTxHash: string): Promise<{ - fillTxHash: string; - fillTxTimestamp: number; - outputAmount: BigNumber | undefined; - }> { + async getFillMetadata( + fillTxHash: string, + bridgeProvider: BridgeProvider + ): Promise { try { if (!isSignature(fillTxHash)) { throw new Error(`Invalid tx signature: ${fillTxHash}`); } - const rpc = getSVMRpc(this.chainId); - const eventsClient = await SvmCpiEventsClient.create(rpc); - // We skip fetching swap metadata on Solana, since we don't support destination chain swaps yet - const fillTx = await eventsClient.getFillEventsFromSignature( - this.chainId, - fillTxHash - ); + const fillTx = ["cctp", "sponsored-cctp"].includes(bridgeProvider) + ? await getMintAndBurnBySignatureSVM({ + signature: fillTxHash, + chainId: this.chainId, + }) + : await getFillTxBySignature({ + signature: fillTxHash, + chainId: this.chainId, + }); - const fillTxDetails = fillTx?.[0]; - if (!fillTxDetails) { + if (!fillTx) { throw new Error( `Unable to find fill with signature ${fillTxHash} on chain ${this.chainId}` ); @@ -217,8 +245,8 @@ export class SVMStrategy implements IChainStrategy { return { fillTxHash, - fillTxTimestamp: Number(fillTxDetails.fillTimestamp), - outputAmount: BigNumber.from(fillTxDetails.outputAmount), + fillTxTimestamp: Number(fillTx.fillTxTimestamp), + outputAmount: BigNumber.from(fillTx.outputAmount), }; } catch (error) { console.error( diff --git a/src/views/DepositStatus/hooks/useDepositTracking/types.ts b/src/views/DepositStatus/hooks/useDepositTracking/types.ts index 03365cf94..8a87118ea 100644 --- a/src/views/DepositStatus/hooks/useDepositTracking/types.ts +++ b/src/views/DepositStatus/hooks/useDepositTracking/types.ts @@ -94,6 +94,18 @@ export type DepositForBurnEvent = { mintRecipient: string; // base58 account (20 byte evm address) }; +export type MintAndWithdrawEvent = { + mintRecipient: string; + amount: bigint; + mintToken: string; +}; + +export type FillMetadata = { + fillTxHash: string; + fillTxTimestamp: number; + outputAmount: BigNumber | undefined; +}; + /** * Common chain strategy interface * Each chain implementation adapts its native types to these normalized interfaces @@ -121,7 +133,10 @@ export interface IChainStrategy { * @param bridgeProvider Bridge provider * @returns Normalized fill information */ - getFillFromRpc(depositInfo: DepositedInfo): Promise; + getFillFromRpc( + depositInfo: DepositedInfo, + bridgeProvider: BridgeProvider + ): Promise; /** * Get fill information for a deposit