Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions src/utils/cctp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
import {
DepositData,
DepositForBurnEvent,
MintAndWithdrawEvent,
FillMetadata,
} from "views/DepositStatus/hooks/useDepositTracking/types";
import {
TokenMessengerMinterV2Client,
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -320,3 +336,39 @@ export async function getDepositForBurnBySignatureSVM({
return convertedLog;
}
}

export async function getMintAndBurnBySignatureSVM({
signature,
chainId,
}: {
signature: Signature;
chainId: number;
}): Promise<FillMetadata | undefined> {
// 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;
}
}
32 changes: 32 additions & 0 deletions src/utils/fills.ts
Original file line number Diff line number Diff line change
@@ -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<FillMetadata | undefined> {
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,
};
}
5 changes: 4 additions & 1 deletion src/utils/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
DepositStatusResponse,
FillInfo,
IChainStrategy,
FillMetadata,
} from "../types";
import {
findFillEvent,
Expand All @@ -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
Expand Down Expand Up @@ -84,21 +89,33 @@ export class SVMStrategy implements IChainStrategy {
* @param depositInfo Deposit information
* @returns Fill information
*/
async getFill(depositInfo: DepositedInfo): Promise<FillInfo> {
async getFill(
depositInfo: DepositedInfo,
bridgeProvider: BridgeProvider = "across"
): Promise<FillInfo> {
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,
Expand Down Expand Up @@ -153,7 +170,10 @@ export class SVMStrategy implements IChainStrategy {
* @param depositInfo Deposit information
* @returns Fill transaction hash (signature)
*/
async getFillFromRpc(depositInfo: DepositedInfo): Promise<string> {
async getFillFromRpc(
depositInfo: DepositedInfo,
bridgeProvider: BridgeProvider
): Promise<string> {
const { depositId } = depositInfo.depositLog;

try {
Expand All @@ -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;
Expand All @@ -191,34 +219,34 @@ 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<FillMetadata | undefined> {
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}`
);
}

return {
fillTxHash,
fillTxTimestamp: Number(fillTxDetails.fillTimestamp),
outputAmount: BigNumber.from(fillTxDetails.outputAmount),
fillTxTimestamp: Number(fillTx.fillTxTimestamp),
outputAmount: BigNumber.from(fillTx.outputAmount),
};
} catch (error) {
console.error(
Expand Down
17 changes: 16 additions & 1 deletion src/views/DepositStatus/hooks/useDepositTracking/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -121,7 +133,10 @@ export interface IChainStrategy {
* @param bridgeProvider Bridge provider
* @returns Normalized fill information
*/
getFillFromRpc(depositInfo: DepositedInfo): Promise<string>;
getFillFromRpc(
depositInfo: DepositedInfo,
bridgeProvider: BridgeProvider
): Promise<string>;

/**
* Get fill information for a deposit
Expand Down
Loading