-
Notifications
You must be signed in to change notification settings - Fork 18
feat(svmSpokeUtils): svm relayFillStatus implementation #984
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
Changes from 8 commits
680b0dc
36ed97f
2c19f73
b6eb601
31c73b6
16c3098
b73a542
c3685dd
0913bf9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<SolanaRpcApi>; | ||
|
||
|
@@ -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<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; | ||
} | ||
|
||
/** | ||
|
@@ -109,7 +111,6 @@ export async function getSlotForBlock( | |
return midSlot; | ||
} | ||
} while (lowSlot <= highSlot); | ||
|
||
return undefined; | ||
} | ||
|
||
|
@@ -127,27 +128,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<FillStatus> { | ||
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OOC should we allow the user to pass this value? It's possible for small ranges (like in the indexer) we want to pass a smaller value for faster read times There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can add it as an optional param and I'll also set the default to a lower value since we'd expect these pdas to have only a few events. Good call. |
||
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, | ||
})); | ||
}) | ||
); | ||
Comment on lines
+153
to
+172
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I copied this from the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the likelihood we will be using 1000+ signatures of lookback? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call. We'd expect these pdas to have only a few events, I'll set a lower value. |
||
|
||
// 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. | ||
Comment on lines
+204
to
+206
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have corresponding tests for this? |
||
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}`); | ||
} | ||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These changes are also temporary and might not be needed with the new version of the svm events client. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 |
||
txResult: GetTransactionReturnType | ||
): { program: Address; data: EventData; name: EventName }[] { | ||
if (!txResult) return []; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Address> { | ||
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<Address> { | ||
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; | ||
} | ||
Comment on lines
+153
to
+165
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OOC is there a way we can use the function you created to derive the address? I.e. could this just be a wrapper over There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OOC - the endianness of the data feels very low level for TS. How often will endianness be a consideration and an we abstract this away from the caller? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to set the endianness to replicate the relay hash calculation doing in the Solana program itself but it is already abstracted from the caller. I mean, these helper functions are intended to be used exclusively within |
||
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. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: is latest the correct term in our case? We'd probably be looking for the most recently "processed" or "confirmed" block