diff --git a/src/arch/svm/SpokeUtils.ts b/src/arch/svm/SpokeUtils.ts index 8eb4bee31..4d7bfd585 100644 --- a/src/arch/svm/SpokeUtils.ts +++ b/src/arch/svm/SpokeUtils.ts @@ -1,6 +1,12 @@ -import { Rpc, SolanaRpcApi } from "@solana/kit"; +import { Rpc, SolanaRpcApi, Address } from "@solana/kit"; +import { getDeployedBlockNumber } from "@across-protocol/contracts"; +import { fetchState } from "@across-protocol/contracts/dist/src/svm/clients/SvmSpoke"; + import { Deposit, FillStatus, FillWithBlock, RelayData } from "../../interfaces"; -import { BigNumber, isUnsafeDepositId } from "../../utils"; +import { isUnsafeDepositId, BigNumber } from "../../utils"; +import { getFillStatusPda } from "./utils"; +import { SvmSpokeEventsClient } from "./eventsClient"; +import { SVMEventNames } from "./types"; type Provider = Rpc; @@ -46,18 +52,14 @@ export async function getTimestampForBlock(provider: Provider, blockNumber: numb } /** - * Return maximum of fill deadline buffer at start and end of block range. - * @param spokePool SpokePool contract instance - * @param startBlock start block - * @param endBlock end block - * @returns maximum of fill deadline buffer at start and end block + * Returns the current fill deadline buffer. + * @param provider SVM Provider instance + * @param statePda Spoke Pool's State PDA + * @returns fill deadline buffer */ -export function getMaxFillDeadlineInRange( - _spokePool: unknown, - _startBlock: number, - _endBlock: number -): Promise { - throw new Error("getMaxFillDeadlineInRange: not implemented"); +export async function getFillDeadline(provider: Provider, statePda: Address): Promise { + const state = await fetchState(provider, statePda); + return state.data.fillDeadlineBuffer; } /** @@ -127,27 +129,90 @@ export function findDepositBlock( } /** - * Find the amount filled for a deposit at a particular block. - * @param spokePool SpokePool contract instance. + * Find the fill status for a deposit at a particular block. + * @param programId SpokePool program address. * @param relayData Deposit information that is used to complete a fill. * @param blockTag Block tag (numeric or "latest") to query at. - * @returns The amount filled for the specified deposit at the requested block (or latest). + * @param destinationChainId Destination chain ID. + * @param provider Solana RPC provider instance. + * @param svmEventsClient SvmSpokeEventsClient instance. + * @returns The fill status for the specified deposit at the requested block (or latest). */ -export function relayFillStatus( - _spokePool: unknown, - _relayData: RelayData, - _blockTag?: number | "latest", - _destinationChainId?: number +export async function relayFillStatus( + programId: Address, + relayData: RelayData, + blockTag: number | "latest", + destinationChainId: number, + provider: Provider, + svmEventsClient: SvmSpokeEventsClient ): Promise { - throw new Error("relayFillStatus: not implemented"); -} + // Get fill status PDA using relayData + const fillStatusPda = await getFillStatusPda(programId, relayData, destinationChainId); + + // Get fill and requested slow fill events from fillStatusPda + // TODO: modify this to use svmEventsClient once we can instantiate it with dynamic addresses. + const fillPdaSignatures = await provider + .getSignaturesForAddress(fillStatusPda, { + limit: 1000, + commitment: "confirmed", + }) + .send(); + + const eventsWithSlots = await Promise.all( + fillPdaSignatures.map(async (signatureTransaction) => { + const events = await svmEventsClient.readEventsFromSignature(signatureTransaction.signature); + return events.map((event) => ({ + ...event, + confirmationStatus: signatureTransaction.confirmationStatus, + blockTime: signatureTransaction.blockTime, + signature: signatureTransaction.signature, + slot: signatureTransaction.slot, + })); + }) + ); + + // Translate blockTag from block to slot to use for filtering events + // This should also be temporary, it can be removed once we can use svmEventsClient with dynamic addresses. + let toSlot: bigint | undefined; + if (blockTag === "latest") { + toSlot = await provider.getSlot().send(); + } else { + const lowSlot = getDeployedBlockNumber("SvmSpoke", destinationChainId); + toSlot = await getSlotForBlock(provider, BigInt(blockTag), BigInt(lowSlot)); + if (!toSlot) { + throw new Error(`Unable to resolve slot for block ${blockTag}`); + } + } -export function fillStatusArray( - _spokePool: unknown, - _relayData: RelayData[], - _blockTag = "processed" -): Promise<(FillStatus | undefined)[]> { - throw new Error("fillStatusArray: not implemented"); + // Filter events by slot and event name + const relevantEvents = eventsWithSlots + .flat() + .filter( + (event) => + event.slot <= toSlot! && + [SVMEventNames.FilledRelay, SVMEventNames.RequestedSlowFill].includes(event.name as SVMEventNames) + ); + + if (relevantEvents.length === 0) { + // No fill or requested slow fill events found for this fill status PDA + return FillStatus.Unfilled; + } + + // Sort events in ascending order of slot number + relevantEvents.sort((a, b) => Number(a.slot - b.slot)); + + // At this point we have only fill and requested slow fill events and since it's not possible to submit + // a slow fill request once a fill has been submitted, we can use the last event in the sorted list to + // determine the fill status at the requested block. + const fillStatusEvent = relevantEvents.pop(); + switch (fillStatusEvent!.name) { + case SVMEventNames.FilledRelay: + return FillStatus.Filled; + case SVMEventNames.RequestedSlowFill: + return FillStatus.RequestedSlowFill; + default: + throw new Error(`Unexpected event name: ${fillStatusEvent!.name}`); + } } /** diff --git a/src/arch/svm/eventsClient.ts b/src/arch/svm/eventsClient.ts index 42d51ece8..a1ce00a20 100644 --- a/src/arch/svm/eventsClient.ts +++ b/src/arch/svm/eventsClient.ts @@ -160,7 +160,7 @@ export class SvmSpokeEventsClient { * @param commitment - Commitment level. * @returns A promise that resolves to an array of events. */ - private async readEventsFromSignature(txSignature: Signature, commitment: Commitment = "confirmed") { + public async readEventsFromSignature(txSignature: Signature, commitment: Commitment = "confirmed") { const txResult = await this.rpc .getTransaction(txSignature, { commitment, maxSupportedTransactionVersion: 0 }) .send(); @@ -175,7 +175,7 @@ export class SvmSpokeEventsClient { * @param txResult - The transaction result. * @returns A promise that resolves to an array of events with their data and name. */ - private processEventFromTx( + public processEventFromTx( txResult: GetTransactionReturnType ): { program: Address; data: EventData; name: EventName }[] { if (!txResult) return []; diff --git a/src/arch/svm/utils.ts b/src/arch/svm/utils.ts index b13319318..d5b5b67d6 100644 --- a/src/arch/svm/utils.ts +++ b/src/arch/svm/utils.ts @@ -1,8 +1,9 @@ import { BN, BorshEventCoder, Idl } from "@coral-xyz/anchor"; -import web3, { address, isAddress, RpcTransport } from "@solana/kit"; -import { BigNumber, isUint8Array, SvmAddress } from "../../utils"; +import web3, { address, getProgramDerivedAddress, getU64Encoder, Address, isAddress, RpcTransport } from "@solana/kit"; +import { BigNumber, getRelayDataHash, isUint8Array, SvmAddress } from "../../utils"; + import { EventName, EventData, SVMEventNames } from "./types"; -import { FillType } from "../../interfaces"; +import { FillType, RelayData } from "../../interfaces"; /** * Helper to determine if the current RPC network is devnet. @@ -132,3 +133,33 @@ export function unwrapEventData( // Return primitives as is return data; } + +/** + * Returns the PDA for the State account. + * @param programId The SpokePool program ID. + * @param extraSeed An optional extra seed. Defaults to 0. + * @returns The PDA for the State account. + */ +export async function getStatePda(programId: Address, extraSeed = 0): Promise
{ + const seedEncoder = getU64Encoder(); + const encodedExtraSeed = seedEncoder.encode(extraSeed); + const [statePda] = await getProgramDerivedAddress({ + programAddress: programId, + seeds: ["state", encodedExtraSeed], + }); + return statePda; +} + +export async function getFillStatusPda( + programId: Address, + relayData: RelayData, + destinationChainId: number +): Promise
{ + const relayDataHash = getRelayDataHash(relayData, destinationChainId); + const uint8RelayDataHash = new Uint8Array(Buffer.from(relayDataHash.slice(2), "hex")); + const [fillStatusPda] = await getProgramDerivedAddress({ + programAddress: programId, + seeds: ["fills", uint8RelayDataHash], + }); + return fillStatusPda; +} diff --git a/src/clients/SpokePoolClient/SVMSpokePoolClient.ts b/src/clients/SpokePoolClient/SVMSpokePoolClient.ts index 7d31add4e..c25e61c3e 100644 --- a/src/clients/SpokePoolClient/SVMSpokePoolClient.ts +++ b/src/clients/SpokePoolClient/SVMSpokePoolClient.ts @@ -1,6 +1,14 @@ -import { Rpc, RpcTransport, SolanaRpcApiFromTransport } from "@solana/kit"; import winston from "winston"; -import { SVMEventNames, SvmSpokeEventsClient, unwrapEventData } from "../../arch/svm"; +import { Address, Rpc, SolanaRpcApiFromTransport, RpcTransport } from "@solana/kit"; +import { + SvmSpokeEventsClient, + SVMEventNames, + getFillDeadline, + getTimestampForBlock, + getStatePda, + unwrapEventData, + relayFillStatus, +} from "../../arch/svm"; import { FillStatus, RelayData, SortableEvent } from "../../interfaces"; import { BigNumber, @@ -29,8 +37,10 @@ export class SvmSpokePoolClient extends SpokePoolClient { chainId: number, deploymentSlot: bigint, // Using slot instead of block number for SVM eventSearchConfig: MakeOptional, - protected svmEventsClient: SvmSpokeEventsClient, - protected rpc: Rpc> + public programId: Address, + protected statePda: Address, + public svmEventsClient: SvmSpokeEventsClient, + public rpc: Rpc> ) { // Convert deploymentSlot to number for base class, might need refinement super(logger, hubPoolClient, chainId, Number(deploymentSlot), eventSearchConfig); @@ -48,12 +58,16 @@ export class SvmSpokePoolClient extends SpokePoolClient { rpc: Rpc> ): Promise { const svmEventsClient = await SvmSpokeEventsClient.create(rpc); + const programId = svmEventsClient.getSvmSpokeAddress(); + const statePda = await getStatePda(programId); return new SvmSpokePoolClient( logger, hubPoolClient, chainId, deploymentSlot, eventSearchConfig, + programId, + statePda, svmEventsClient, rpc ); @@ -154,18 +168,18 @@ export class SvmSpokePoolClient extends SpokePoolClient { } /** - * Retrieves the maximum fill deadline buffer. - * TODO: Implement SVM equivalent, perhaps reading from a config account. + * Retrieves the fill deadline buffer fetched from the State PDA. + * @note This function assumes that fill deadline buffer is a constant value in svm environments. */ - public getMaxFillDeadlineInRange(_startSlot: number, _endSlot: number): Promise { - throw new Error("getMaxFillDeadlineInRange not implemented for SVM"); + public override getMaxFillDeadlineInRange(_startSlot: number, _endSlot: number): Promise { + return getFillDeadline(this.rpc, this.statePda); } /** * Retrieves the timestamp for a given SVM slot number. */ - public getTimestampForBlock(_blockNumber: number): Promise { - throw new Error("getTimestampForBlock not implemented for SVM"); + public override getTimestampForBlock(blockNumber: number): Promise { + return getTimestampForBlock(this.rpc, blockNumber); } /** @@ -187,12 +201,13 @@ export class SvmSpokePoolClient extends SpokePoolClient { * Retrieves the fill status for a given relay data from the SVM chain. * TODO: Implement SVM state query for fill status. */ - public relayFillStatus( - _relayData: RelayData, - _slot?: number | "latest", // Use slot instead of blockTag - _destinationChainId?: number + public override relayFillStatus( + relayData: RelayData, + blockTag: number | "latest", + destinationChainId?: number ): Promise { - throw new Error("relayFillStatus not implemented for SVM"); + destinationChainId ??= this.chainId; + return relayFillStatus(this.programId, relayData, blockTag, destinationChainId, this.rpc, this.svmEventsClient); } /** diff --git a/src/utils/SpokeUtils.ts b/src/utils/SpokeUtils.ts index 696bd7c69..c8e0c320c 100644 --- a/src/utils/SpokeUtils.ts +++ b/src/utils/SpokeUtils.ts @@ -3,8 +3,9 @@ import { MAX_SAFE_DEPOSIT_ID, ZERO_ADDRESS, ZERO_BYTES } from "../constants"; import { Deposit, RelayData } from "../interfaces"; import { toBytes32 } from "./AddressUtils"; import { keccak256 } from "./common"; -import { BigNumber } from "./BigNumberUtils"; +import { BigNumber, toBN } from "./BigNumberUtils"; import { isMessageEmpty } from "./DepositUtils"; +import { chainIsSvm } from "./NetworkUtils"; /** * Produce the RelayData for a Deposit. @@ -43,6 +44,9 @@ export function getRelayDataHash(relayData: RelayData, destinationChainId: numbe outputToken: toBytes32(relayData.outputToken), exclusiveRelayer: toBytes32(relayData.exclusiveRelayer), }; + if (chainIsSvm(destinationChainId)) { + return _getRelayDataHashSvm(_relayData, destinationChainId); + } return keccak256( ethersUtils.defaultAbiCoder.encode( [ @@ -71,6 +75,46 @@ export function getRelayHashFromEvent(e: RelayData & { destinationChainId: numbe return getRelayDataHash(e, e.destinationChainId); } +function _getRelayDataHashSvm(relayData: RelayData, destinationChainId: number): string { + const uint8ArrayFromHexString = (hex: string, littleEndian: boolean = false): Uint8Array => { + const buffer = Buffer.from(hex.slice(2), "hex"); + if (buffer.length < 32) { + const zeroPad = new Uint8Array(32); + buffer.copy(zeroPad, 32 - buffer.length); + return littleEndian ? zeroPad.reverse() : zeroPad; + } + const result = new Uint8Array(buffer.slice(0, 32)); + return littleEndian ? result.reverse() : result; + }; + const uint8ArrayFromInt = (num: BigNumber, byteLength: number, littleEndian: boolean = true): Uint8Array => { + const buffer = Buffer.from(num.toHexString().slice(2), "hex"); + if (buffer.length < byteLength) { + const zeroPad = new Uint8Array(byteLength); + buffer.copy(zeroPad, byteLength - buffer.length); + return littleEndian ? zeroPad.reverse() : zeroPad; + } + const result = new Uint8Array(buffer.slice(0, byteLength)); + return littleEndian ? result.reverse() : result; + }; + const contentToHash = Buffer.concat([ + uint8ArrayFromHexString(relayData.depositor), + uint8ArrayFromHexString(relayData.recipient), + uint8ArrayFromHexString(relayData.exclusiveRelayer), + uint8ArrayFromHexString(relayData.inputToken), + uint8ArrayFromHexString(relayData.outputToken), + uint8ArrayFromInt(relayData.inputAmount, 8), + uint8ArrayFromInt(relayData.outputAmount, 8), + uint8ArrayFromInt(toBN(relayData.originChainId), 8), + uint8ArrayFromInt(relayData.depositId, 32, false), + uint8ArrayFromInt(toBN(relayData.fillDeadline), 4), + uint8ArrayFromInt(toBN(relayData.exclusivityDeadline), 4), + uint8ArrayFromHexString(getMessageHash(relayData.message)), + uint8ArrayFromInt(toBN(destinationChainId), 8), + ]); + const returnHash = keccak256(contentToHash); + return returnHash; +} + export function isUnsafeDepositId(depositId: BigNumber): boolean { // SpokePool.unsafeDepositV3() produces a uint256 depositId by hashing the msg.sender, depositor and input // uint256 depositNonce. There is a possibility that this resultant uint256 is less than the maxSafeDepositId (i.e.