Skip to content

feat(svm): Initial SpokeUtils skeleton #960

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 8 commits into from
Apr 8, 2025
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@across-protocol/sdk",
"author": "UMA Team",
"version": "4.1.42",
"version": "4.1.43",
"license": "AGPL-3.0",
"homepage": "https://docs.across.to/reference/sdk",
"files": [
Expand Down
178 changes: 178 additions & 0 deletions src/arch/svm/SpokeUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { Rpc, SolanaRpcApi } from "@solana/kit";
import { Deposit, FillStatus, FillWithBlock, RelayData } from "../../interfaces";
import { BigNumber, isUnsafeDepositId } from "../../utils";

type Provider = Rpc<SolanaRpcApi>;

/**
* @param spokePool SpokePool Contract instance.
* @param deposit V3Deopsit instance.
* @param repaymentChainId Optional repaymentChainId (defaults to destinationChainId).
* @returns An Ethers UnsignedTransaction instance.
*/
export function populateV3Relay(
_spokePool: unknown,
_deposit: Omit<Deposit, "messageHash">,
_relayer: string,
_repaymentChainId = _deposit.destinationChainId
): Promise<unknown> {
throw new Error("populateV3Relay: not implemented");
}

/**
* Retrieves the time from the SpokePool contract at a particular block.
* @returns The time at the specified block tag.
*/
export function getTimeAt(_spokePool: unknown, _blockNumber: number): Promise<number> {
throw new Error("getTimeAt: not implemented");
}

/**
* Retrieves the chain time at a particular block.
* @note This should be the same as getTimeAt() but can differ in test. These two functions should be consolidated.
* @returns The chain time at the specified block tag.
*/
export async function getTimestampForBlock(provider: Provider, blockNumber: number): Promise<number> {
const block = await provider.getBlock(BigInt(blockNumber)).send();
let timestamp: number;
if (!block?.blockTime) {
console.error(`Unable to resolve svm block ${blockNumber}`);
timestamp = 0; // @todo: How to handle this?
} else {
timestamp = Number(block.blockTime); // Unix timestamps fit within number.
}

return timestamp;
}

/**
* 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
*/
export function getMaxFillDeadlineInRange(
_spokePool: unknown,
_startBlock: number,
_endBlock: number
): Promise<number> {
throw new Error("getMaxFillDeadlineInRange: not implemented");
}

/**
* Finds the deposit id at a specific block number.
* @param blockTag The block number to search for the deposit ID at.
* @returns The deposit ID.
*/
export function getDepositIdAtBlock(_contract: unknown, _blockTag: number): Promise<BigNumber> {
throw new Error("getDepositIdAtBlock: not implemented");
}

/**
* xxx todo
*/
export async function getSlotForBlock(
provider: Provider,
blockNumber: bigint,
lowSlot: bigint,
_highSlot?: bigint
): Promise<bigint | undefined> {
// @todo: Factor getBlock out to SlotFinder ??
const getBlockNumber = async (slot: bigint): Promise<bigint> => {
const block = await provider
.getBlock(slot, { transactionDetails: "none", maxSupportedTransactionVersion: 0 })
.send();
return block?.blockHeight ?? BigInt(0); // @xxx Handle undefined here!
};

let highSlot = _highSlot ?? (await provider.getSlot().send());
const [blockLow = 0, blockHigh = 1_000_000_000] = await Promise.all([
getBlockNumber(lowSlot),
getBlockNumber(highSlot),
]);

if (blockLow > blockNumber || blockHigh < blockNumber) {
return undefined; // blockNumber did not occur within the specified block range.
}

// Find the lowest slot number where blockHeight is greater than the requested blockNumber.
do {
const midSlot = (highSlot + lowSlot) / BigInt(2);
const midBlock = await getBlockNumber(midSlot);

if (midBlock < blockNumber) {
lowSlot = midSlot + BigInt(1);
} else if (midBlock > blockNumber) {
highSlot = midSlot + BigInt(1); // blockNumber occurred at or earlier than midBlock.
} else {
return midSlot;
}
} while (lowSlot <= highSlot);

return undefined;
}

export function findDepositBlock(
_spokePool: unknown,
depositId: BigNumber,
_lowBlock: number,
_highBlock?: number
): Promise<number | undefined> {
// We can only perform this search when we have a safe deposit ID.
if (isUnsafeDepositId(depositId)) {
throw new Error(`Cannot binary search for depositId ${depositId}`);
}
throw new Error("findDepositBlock: not implemented");
}

/**
* Find the amount filled for a deposit at a particular block.
* @param spokePool SpokePool contract instance.
* @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).
*/
export function relayFillStatus(
_spokePool: unknown,
_relayData: RelayData,
_blockTag?: number | "latest",
_destinationChainId?: number
): Promise<FillStatus> {
throw new Error("relayFillStatus: not implemented");
}

export function fillStatusArray(
_spokePool: unknown,
_relayData: RelayData[],
_blockTag = "processed"
): Promise<(FillStatus | undefined)[]> {
throw new Error("fillStatusArray: not implemented");
}

/**
* Find the block at which a fill was completed.
* @todo After SpokePool upgrade, this function can be simplified to use the FillStatus enum.
* @param spokePool SpokePool contract instance.
* @param relayData Deposit information that is used to complete a fill.
* @param lowBlockNumber The lower bound of the search. Must be bounded by SpokePool deployment.
* @param highBlocknumber Optional upper bound for the search.
* @returns The block number at which the relay was completed, or undefined.
*/
export function findFillBlock(
_spokePool: unknown,
_relayData: RelayData,
_lowBlockNumber: number,
_highBlockNumber?: number
): Promise<number | undefined> {
throw new Error("fillStatusArray: not implemented");
}

export function findFillEvent(
_spokePool: unknown,
_relayData: RelayData,
_lowBlockNumber: number,
_highBlockNumber?: number
): Promise<FillWithBlock | undefined> {
throw new Error("fillStatusArray: not implemented");
}
2 changes: 1 addition & 1 deletion src/arch/svm/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const platform = "svm"; // Placeholder for actual exports.
export * from "./SpokeUtils";
3 changes: 1 addition & 2 deletions src/clients/BundleDataClient/BundleDataClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,8 +728,7 @@ export class BundleDataClient {
// hasn't queried. This is because this function will usually be called
// in production with block ranges that were validated by
// DataworkerUtils.blockRangesAreInvalidForSpokeClients.
Math.min(queryBlock, spokePoolClients[deposit.destinationChainId].latestBlockSearched),
deposit.destinationChainId
Math.min(queryBlock, spokePoolClients[deposit.destinationChainId].latestBlockSearched)
);
};

Expand Down
16 changes: 10 additions & 6 deletions src/clients/SpokePoolClient/EVMSpokePoolClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Contract, EventFilter } from "ethers";
import {
fillStatusArray,
findDepositBlock,
getMaxFillDeadlineInRange as getMaxFillDeadline,
getTimeAt as _getTimeAt,
Expand Down Expand Up @@ -42,12 +43,15 @@ export class EVMSpokePoolClient extends SpokePoolClient {
super(logger, hubPoolClient, chainId, deploymentBlock, eventSearchConfig);
}

public override relayFillStatus(
relayData: RelayData,
blockTag?: number | "latest",
destinationChainId?: number
): Promise<FillStatus> {
return relayFillStatus(this.spokePool, relayData, blockTag, destinationChainId);
public override relayFillStatus(relayData: RelayData, blockTag?: number | "latest"): Promise<FillStatus> {
return relayFillStatus(this.spokePool, relayData, blockTag, this.chainId);
}

public override fillStatusArray(
relayData: RelayData[],
blockTag?: number | "latest"
): Promise<(FillStatus | undefined)[]> {
return fillStatusArray(this.spokePool, relayData, blockTag);
}

public override getMaxFillDeadlineInRange(startBlock: number, endBlock: number): Promise<number> {
Expand Down
18 changes: 13 additions & 5 deletions src/clients/SpokePoolClient/SpokePoolClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -810,11 +810,19 @@ export abstract class SpokePoolClient extends BaseAbstractClient {
/**
* Retrieves the fill status for a given relay data.
* @param relayData The relay data to retrieve the fill status for.
* @param blockTag The block at which to query the fill status.
* @returns The fill status for the given relay data.
*/
public abstract relayFillStatus(
relayData: RelayData,
blockTag?: number | "latest",
destinationChainId?: number
): Promise<FillStatus>;
public abstract relayFillStatus(relayData: RelayData, blockTag?: number | "latest"): Promise<FillStatus>;

/**
* Retrieves the fill status for an array of given relay data.
* @param relayData The array relay data to retrieve the fill status for.
* @param blockTag The block at which to query the fill status.
* @returns The fill status for each of the given relay data.
*/
public abstract fillStatusArray(
relayData: RelayData[],
blockTag?: number | "latest"
): Promise<(FillStatus | undefined)[]>;
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * as arch from "./arch";
export * as addressAggregator from "./addressAggregator";
export * as lpFeeCalculator from "./lpFeeCalculator";
export * as pool from "./pool";
Expand Down