Skip to content

feat(svmSpokeUtils): get fill deadline buffer implementation #978

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 23, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
22 changes: 10 additions & 12 deletions src/arch/svm/SpokeUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Rpc, SolanaRpcApi } from "@solana/kit";
import { Rpc, SolanaRpcApi, Address } from "@solana/kit";

import { Deposit, FillStatus, FillWithBlock, RelayData } from "../../interfaces";
import { BigNumber, isUnsafeDepositId } from "../../utils";
import { fetchState } from "@across-protocol/contracts/dist/src/svm/clients/SvmSpoke";

type Provider = Rpc<SolanaRpcApi>;

Expand Down Expand Up @@ -46,18 +48,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<number> {
throw new Error("getMaxFillDeadlineInRange: not implemented");
export async function getFillDeadline(provider: Provider, statePda: Address): Promise<number> {
const state = await fetchState(provider, statePda);
return state.data.fillDeadlineBuffer;
}

/**
Expand Down
19 changes: 18 additions & 1 deletion src/arch/svm/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BN, BorshEventCoder, Idl } from "@coral-xyz/anchor";
import web3, { address, RpcTransport } from "@solana/kit";
import web3, { address, getProgramDerivedAddress, getU64Encoder, Address, RpcTransport } from "@solana/kit";

import { EventName, EventData, SVMEventNames } from "./types";

/**
Expand Down Expand Up @@ -64,3 +65,19 @@ export function getEventName(rawName: string): EventName {
if (Object.values(SVMEventNames).some((name) => rawName.includes(name))) return rawName as EventName;
throw new Error(`Unknown event name: ${rawName}`);
}

/**
* 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: string, extraSeed = 0): Promise<Address> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency should programId also be Address?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const seedEncoder = getU64Encoder();
const encodedExtraSeed = seedEncoder.encode(extraSeed);
const [statePda] = await getProgramDerivedAddress({
programAddress: address(programId),
seeds: ["state", encodedExtraSeed],
});
return statePda;
}
30 changes: 22 additions & 8 deletions src/clients/SpokePoolClient/SVMSpokePoolClient.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import winston from "winston";
import { Rpc, SolanaRpcApiFromTransport, RpcTransport } from "@solana/kit";
import { Address, Rpc, SolanaRpcApiFromTransport, RpcTransport } from "@solana/kit";

import { BigNumber, DepositSearchResult, EventSearchConfig, MakeOptional } from "../../utils";
import { SvmSpokeEventsClient, SVMEventNames } from "../../arch/svm";
import {
SvmSpokeEventsClient,
SVMEventNames,
getFillDeadline,
getTimestampForBlock,
getStatePda,
} from "../../arch/svm";
import { HubPoolClient } from "../HubPoolClient";
import { knownEventNames, SpokePoolClient, SpokePoolUpdate } from "./SpokePoolClient";
import { RelayData, FillStatus } from "../../interfaces";

/**
* SvmSpokePoolClient is a client for the SVM SpokePool program. It extends the base SpokePoolClient
* and implements the abstract methods required for interacting with an SVM Spoke Pool.
Expand All @@ -19,6 +27,8 @@ export class SvmSpokePoolClient extends SpokePoolClient {
chainId: number,
deploymentSlot: bigint, // Using slot instead of block number for SVM
eventSearchConfig: MakeOptional<EventSearchConfig, "toBlock">,
protected programId: string,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added programId here because I think it's useful to have direct access to it. That said, I'm also not sure this is the best approach since svmSpokeEventsClient already resolves programId internally, and that value might differ from the one passed in here. To address that I think we can either (1) use the resolved value from svmEventsClient or (2) let the callers provide programId and pass that to svmSpokeEventsClient.

For option 1, we only need to make svmSpokeAddress a public property (here).

For option 2 we'll have to remove the resolution logic from svmSpokeEventsClient entirely and turn it into a constructor param instead.

I'm curious to hear other's thoughts about this. Personally, I'm leaning toward option 2. I think it's closer to what we have for EVM and it also improves flexibility. As an example, I had to override the program signature to test some Solana features while working in the Indexer and there was other way to do so than changing the svmSpokeEventsClient.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@melisaguevara Is it necessary for the eventsClient to be instantiated outside of the SVMSpokePoolClient? Could we instead instantiate it entirely within the .create() static method?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fwiw I think it makes sense to also expose the svmSpokeAddress - it's not required to be private, and could theoretically be encapsulated in a getter fn

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it necessary for the eventsClient to be instantiated outside of the SVMSpokePoolClient? Could we instead instantiate it entirely within the .create() static method?

@pxrl I don't think so, I think we'll always use it through the clients. This could be a follow up if we all agree to do so.

Copy link
Contributor Author

@melisaguevara melisaguevara Apr 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fwiw I think it makes sense to also expose the svmSpokeAddress - it's not required to be private, and could theoretically be encapsulated in a getter fn

@james-a-morris Yes I saw you did that in your PR. I think that's also an option, though as an end user you would have to do svmSpokeClient.svmEventsClient.getSvmSpokeAddress(). I think it would be nice to have that directly as property of SvmSpokeClient. For now I'll follow your approach but I'm also including it as a spoke client property, we can revisit this later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how it looks for now: d7e3dd6

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pxrl I don't think so, I think we'll always use it through the clients. This could be a follow up if we all agree to do so.

Yeah; I think it's probably cleaner to incorporate it directly into the SpokePoolClient, rather than requiring it to be supplied as an external dependency. This should make it easier to instantiate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can modify this after this PR goes in: #982

protected statePda: Address,
protected svmEventsClient: SvmSpokeEventsClient,
protected rpc: Rpc<SolanaRpcApiFromTransport<RpcTransport>>
) {
Expand All @@ -35,15 +45,19 @@ export class SvmSpokePoolClient extends SpokePoolClient {
chainId: number,
deploymentSlot: bigint,
eventSearchConfig: MakeOptional<EventSearchConfig, "toBlock"> = { fromBlock: 0, maxBlockLookBack: 0 }, // Provide default
programId: string,
rpc: Rpc<SolanaRpcApiFromTransport<RpcTransport>>
): Promise<SvmSpokePoolClient> {
const statePda = await getStatePda(programId);
const svmEventsClient = await SvmSpokeEventsClient.create(rpc);
return new SvmSpokePoolClient(
logger,
hubPoolClient,
chainId,
deploymentSlot,
eventSearchConfig,
programId,
statePda,
svmEventsClient,
rpc
);
Expand Down Expand Up @@ -150,18 +164,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<number> {
throw new Error("getMaxFillDeadlineInRange not implemented for SVM");
public override getMaxFillDeadlineInRange(_startSlot: number, _endSlot: number): Promise<number> {
return getFillDeadline(this.rpc, this.statePda);
}

/**
* Retrieves the timestamp for a given SVM slot number.
*/
public getTimestampForBlock(_blockNumber: number): Promise<number> {
throw new Error("getTimestampForBlock not implemented for SVM");
public override getTimestampForBlock(blockNumber: number): Promise<number> {
return getTimestampForBlock(this.rpc, blockNumber);
}

/**
Expand Down