Skip to content

Commit 320347f

Browse files
committed
feat(SvmSpokePoolClient): relayFillStatus and fillStatusArray implementation
1 parent 3e20d7e commit 320347f

File tree

5 files changed

+177
-28
lines changed

5 files changed

+177
-28
lines changed

src/arch/svm/SpokeUtils.ts

+80-16
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
import assert from "assert";
12
import { Rpc, SolanaRpcApi, Address } from "@solana/kit";
3+
import { SvmSpokeIdl } from "@across-protocol/contracts";
4+
import { fetchState } from "@across-protocol/contracts/dist/src/svm/clients/SvmSpoke";
25

6+
import { SvmCpiEventsClient } from "./eventsClient";
37
import { Deposit, FillStatus, FillWithBlock, RelayData } from "../../interfaces";
4-
import { BigNumber, isUnsafeDepositId } from "../../utils";
5-
import { fetchState } from "@across-protocol/contracts/dist/src/svm/clients/SvmSpoke";
8+
import { BigNumber, chainIsSvm, chunk, isUnsafeDepositId } from "../../utils";
9+
import { getFillStatusPda } from "./utils";
10+
import { SVMEventNames } from "./types";
611

712
type Provider = Rpc<SolanaRpcApi>;
813

@@ -125,27 +130,86 @@ export function findDepositBlock(
125130
}
126131

127132
/**
128-
* Find the amount filled for a deposit at a particular block.
133+
* Find the fill status for a deposit at a particular block.
129134
* @param spokePool SpokePool contract instance.
130135
* @param relayData Deposit information that is used to complete a fill.
131-
* @param blockTag Block tag (numeric or "latest") to query at.
132-
* @returns The amount filled for the specified deposit at the requested block (or latest).
136+
* @param fromSlot Slot to start the search at.
137+
* @param blockTag Slot (numeric or "confirmed") to query at.
138+
* @returns The fill status for the specified deposit at the requested slot (or at the current confirmed slot).
133139
*/
134-
export function relayFillStatus(
135-
_spokePool: unknown,
136-
_relayData: RelayData,
137-
_blockTag?: number | "latest",
138-
_destinationChainId?: number
140+
export async function relayFillStatus(
141+
programId: Address,
142+
relayData: RelayData,
143+
fromSlot: number,
144+
blockTag: number | "confirmed" = "confirmed",
145+
destinationChainId: number,
146+
provider: Provider
139147
): Promise<FillStatus> {
140-
throw new Error("relayFillStatus: not implemented");
148+
assert(chainIsSvm(destinationChainId), "Destination chain must be an SVM chain");
149+
150+
// Get fill status PDA using relayData
151+
const fillStatusPda = await getFillStatusPda(programId, relayData, destinationChainId);
152+
153+
// Set search range
154+
let toSlot: bigint;
155+
if (blockTag === "confirmed") {
156+
toSlot = await provider.getSlot({ commitment: "confirmed" }).send();
157+
} else {
158+
toSlot = BigInt(blockTag);
159+
}
160+
161+
// Get fill and requested slow fill events from fillStatusPda
162+
const pdaEventsClient = await SvmCpiEventsClient.createFor(provider, programId, SvmSpokeIdl, fillStatusPda);
163+
const eventsToQuery = [SVMEventNames.FilledRelay, SVMEventNames.RequestedSlowFill];
164+
const relevantEvents = (
165+
await Promise.all(
166+
eventsToQuery.map((eventName) =>
167+
pdaEventsClient.queryDerivedAddressEvents(eventName, BigInt(fromSlot), toSlot, { limit: 50 })
168+
)
169+
)
170+
).flat();
171+
172+
if (relevantEvents.length === 0) {
173+
// No fill or requested slow fill events found for this fill status PDA
174+
return FillStatus.Unfilled;
175+
}
176+
177+
// Sort events in ascending order of slot number
178+
relevantEvents.sort((a, b) => Number(a.slot - b.slot));
179+
180+
// At this point we have an ordered array of fill and requested slow fill events and since it's not possible to
181+
// submit a slow fill request once a fill has been submitted, we can use the last event in the sorted list to
182+
// determine the fill status at the requested block.
183+
const fillStatusEvent = relevantEvents.pop();
184+
switch (fillStatusEvent!.name) {
185+
case SVMEventNames.FilledRelay:
186+
return FillStatus.Filled;
187+
case SVMEventNames.RequestedSlowFill:
188+
return FillStatus.RequestedSlowFill;
189+
default:
190+
throw new Error(`Unexpected event name: ${fillStatusEvent!.name}`);
191+
}
141192
}
142193

143-
export function fillStatusArray(
144-
_spokePool: unknown,
145-
_relayData: RelayData[],
146-
_blockTag = "processed"
194+
export async function fillStatusArray(
195+
programId: Address,
196+
relayData: RelayData[],
197+
fromSlot: number,
198+
blockTag: number | "confirmed" = "confirmed",
199+
destinationChainId: number,
200+
provider: Provider
147201
): Promise<(FillStatus | undefined)[]> {
148-
throw new Error("fillStatusArray: not implemented");
202+
assert(chainIsSvm(destinationChainId), "Destination chain must be an SVM chain");
203+
const chunkSize = 2;
204+
const chunkedRelayData = chunk(relayData, chunkSize);
205+
const results = [];
206+
for (const chunk of chunkedRelayData) {
207+
const chunkResults = await Promise.all(
208+
chunk.map((relayData) => relayFillStatus(programId, relayData, fromSlot, blockTag, destinationChainId, provider))
209+
);
210+
results.push(...chunkResults);
211+
}
212+
return results.flat();
149213
}
150214

151215
/**

src/arch/svm/utils.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { BN, BorshEventCoder, Idl } from "@coral-xyz/anchor";
22
import web3, { address, getProgramDerivedAddress, getU64Encoder, Address, RpcTransport } from "@solana/kit";
33
import { EventName, SVMEventNames } from "./types";
4-
4+
import { getRelayDataHash } from "../../utils";
5+
import { RelayData } from "../../interfaces";
56
/**
67
* Helper to determine if the current RPC network is devnet.
78
*/
@@ -79,3 +80,24 @@ export async function getStatePda(programId: Address): Promise<Address> {
7980
});
8081
return statePda;
8182
}
83+
84+
/**
85+
* Returns the fill status PDA for the given relay data.
86+
* @param programId The SpokePool program ID.
87+
* @param relayData The relay data to get the fill status PDA for.
88+
* @param destinationChainId The destination chain ID.
89+
* @returns The PDA for the fill status.
90+
*/
91+
export async function getFillStatusPda(
92+
programId: Address,
93+
relayData: RelayData,
94+
destinationChainId: number
95+
): Promise<Address> {
96+
const relayDataHash = getRelayDataHash(relayData, destinationChainId);
97+
const uint8RelayDataHash = new Uint8Array(Buffer.from(relayDataHash.slice(2), "hex"));
98+
const [fillStatusPda] = await getProgramDerivedAddress({
99+
programAddress: programId,
100+
seeds: ["fills", uint8RelayDataHash],
101+
});
102+
return fillStatusPda;
103+
}

src/clients/SpokePoolClient/SVMSpokePoolClient.ts

+24-8
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@ import winston from "winston";
22
import { Address, Rpc, SolanaRpcApiFromTransport, RpcTransport } from "@solana/kit";
33

44
import { BigNumber, DepositSearchResult, EventSearchConfig, MakeOptional } from "../../utils";
5-
import { SvmCpiEventsClient, SVMEventNames, getFillDeadline, getTimestampForBlock, getStatePda } from "../../arch/svm";
5+
import {
6+
SvmCpiEventsClient,
7+
SVMEventNames,
8+
getFillDeadline,
9+
getTimestampForBlock,
10+
getStatePda,
11+
relayFillStatus,
12+
fillStatusArray,
13+
} from "../../arch/svm";
614
import { HubPoolClient } from "../HubPoolClient";
715
import { knownEventNames, SpokePoolClient, SpokePoolUpdate } from "./SpokePoolClient";
816
import { RelayData, FillStatus } from "../../interfaces";
@@ -191,12 +199,14 @@ export class SvmSpokePoolClient extends SpokePoolClient {
191199
* Retrieves the fill status for a given relay data from the SVM chain.
192200
* TODO: Implement SVM state query for fill status.
193201
*/
194-
public relayFillStatus(
195-
_relayData: RelayData,
196-
_slot?: number | "latest", // Use slot instead of blockTag
197-
_destinationChainId?: number
202+
public override relayFillStatus(
203+
relayData: RelayData,
204+
blockTag: number | "confirmed",
205+
destinationChainId?: number
198206
): Promise<FillStatus> {
199-
throw new Error("relayFillStatus not implemented for SVM");
207+
destinationChainId ??= this.chainId;
208+
// @note: deploymentBlock actually refers to the deployment slot. Also, blockTag should be a slot number.
209+
return relayFillStatus(this.programId, relayData, this.deploymentBlock, blockTag, destinationChainId, this.rpc);
200210
}
201211

202212
/**
@@ -205,7 +215,13 @@ export class SvmSpokePoolClient extends SpokePoolClient {
205215
* @param blockTag The block at which to query the fill status.
206216
* @returns The fill status for each of the given relay data.
207217
*/
208-
public fillStatusArray(_relayData: RelayData[], _blockTag?: number | "latest"): Promise<(FillStatus | undefined)[]> {
209-
throw new Error("fillStatusArray not implemented for SVM");
218+
public fillStatusArray(
219+
relayData: RelayData[],
220+
blockTag?: number | "confirmed",
221+
destinationChainId?: number
222+
): Promise<(FillStatus | undefined)[]> {
223+
// @note: deploymentBlock actually refers to the deployment slot. Also, blockTag should be a slot number.
224+
destinationChainId ??= this.chainId;
225+
return fillStatusArray(this.programId, relayData, this.deploymentBlock, blockTag, destinationChainId, this.rpc);
210226
}
211227
}

src/clients/SpokePoolClient/SpokePoolClient.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -813,7 +813,10 @@ export abstract class SpokePoolClient extends BaseAbstractClient {
813813
* @param blockTag The block at which to query the fill status.
814814
* @returns The fill status for the given relay data.
815815
*/
816-
public abstract relayFillStatus(relayData: RelayData, blockTag?: number | "latest"): Promise<FillStatus>;
816+
public abstract relayFillStatus(
817+
relayData: RelayData,
818+
blockTag?: number | "latest" | "confirmed"
819+
): Promise<FillStatus>;
817820

818821
/**
819822
* Retrieves the fill status for an array of given relay data.
@@ -823,6 +826,6 @@ export abstract class SpokePoolClient extends BaseAbstractClient {
823826
*/
824827
public abstract fillStatusArray(
825828
relayData: RelayData[],
826-
blockTag?: number | "latest"
829+
blockTag?: number | "latest" | "confirmed"
827830
): Promise<(FillStatus | undefined)[]>;
828831
}

src/utils/SpokeUtils.ts

+45-1
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { MAX_SAFE_DEPOSIT_ID, ZERO_ADDRESS, ZERO_BYTES } from "../constants";
33
import { Deposit, RelayData } from "../interfaces";
44
import { toBytes32 } from "./AddressUtils";
55
import { keccak256 } from "./common";
6-
import { BigNumber } from "./BigNumberUtils";
6+
import { BigNumber, toBN } from "./BigNumberUtils";
77
import { isMessageEmpty } from "./DepositUtils";
8+
import { chainIsSvm } from "./NetworkUtils";
89

910
/**
1011
* Produce the RelayData for a Deposit.
@@ -43,6 +44,9 @@ export function getRelayDataHash(relayData: RelayData, destinationChainId: numbe
4344
outputToken: toBytes32(relayData.outputToken),
4445
exclusiveRelayer: toBytes32(relayData.exclusiveRelayer),
4546
};
47+
if (chainIsSvm(destinationChainId)) {
48+
return _getRelayDataHashSvm(_relayData, destinationChainId);
49+
}
4650
return keccak256(
4751
ethersUtils.defaultAbiCoder.encode(
4852
[
@@ -71,6 +75,46 @@ export function getRelayHashFromEvent(e: RelayData & { destinationChainId: numbe
7175
return getRelayDataHash(e, e.destinationChainId);
7276
}
7377

78+
function _getRelayDataHashSvm(relayData: RelayData, destinationChainId: number): string {
79+
const uint8ArrayFromHexString = (hex: string, littleEndian: boolean = false): Uint8Array => {
80+
const buffer = Buffer.from(hex.slice(2), "hex");
81+
if (buffer.length < 32) {
82+
const zeroPad = new Uint8Array(32);
83+
buffer.copy(zeroPad, 32 - buffer.length);
84+
return littleEndian ? zeroPad.reverse() : zeroPad;
85+
}
86+
const result = new Uint8Array(buffer.slice(0, 32));
87+
return littleEndian ? result.reverse() : result;
88+
};
89+
const uint8ArrayFromInt = (num: BigNumber, byteLength: number, littleEndian: boolean = true): Uint8Array => {
90+
const buffer = Buffer.from(num.toHexString().slice(2), "hex");
91+
if (buffer.length < byteLength) {
92+
const zeroPad = new Uint8Array(byteLength);
93+
buffer.copy(zeroPad, byteLength - buffer.length);
94+
return littleEndian ? zeroPad.reverse() : zeroPad;
95+
}
96+
const result = new Uint8Array(buffer.slice(0, byteLength));
97+
return littleEndian ? result.reverse() : result;
98+
};
99+
const contentToHash = Buffer.concat([
100+
uint8ArrayFromHexString(relayData.depositor),
101+
uint8ArrayFromHexString(relayData.recipient),
102+
uint8ArrayFromHexString(relayData.exclusiveRelayer),
103+
uint8ArrayFromHexString(relayData.inputToken),
104+
uint8ArrayFromHexString(relayData.outputToken),
105+
uint8ArrayFromInt(relayData.inputAmount, 8),
106+
uint8ArrayFromInt(relayData.outputAmount, 8),
107+
uint8ArrayFromInt(toBN(relayData.originChainId), 8),
108+
uint8ArrayFromInt(relayData.depositId, 32, false),
109+
uint8ArrayFromInt(toBN(relayData.fillDeadline), 4),
110+
uint8ArrayFromInt(toBN(relayData.exclusivityDeadline), 4),
111+
uint8ArrayFromHexString(getMessageHash(relayData.message)),
112+
uint8ArrayFromInt(toBN(destinationChainId), 8),
113+
]);
114+
const returnHash = keccak256(contentToHash);
115+
return returnHash;
116+
}
117+
74118
export function isUnsafeDepositId(depositId: BigNumber): boolean {
75119
// SpokePool.unsafeDepositV3() produces a uint256 depositId by hashing the msg.sender, depositor and input
76120
// uint256 depositNonce. There is a possibility that this resultant uint256 is less than the maxSafeDepositId (i.e.

0 commit comments

Comments
 (0)