Skip to content

feat: add request slow fill and fill functions and event tests #1028

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 6 commits into from
May 14, 2025

Conversation

md0x
Copy link
Contributor

@md0x md0x commented May 9, 2025

Changes proposed in this PR:

  • Add RequestedSlowFill and FilledRelay events integration tests.
  • Add requestSlowFill and createFill functions

md0x added 2 commits May 9, 2025 18:17
Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com>
Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com>

describe("SvmCpiEventsClient (integration)", () => {
describe.only("SvmCpiEventsClient (integration)", () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Watch out for this one 👀

Suggested change
describe.only("SvmCpiEventsClient (integration)", () => {
describe("SvmCpiEventsClient (integration)", () => {

Comment on lines 78 to 88
const uint8ArrayToBigNumber = (arr: Uint8Array): BigNumber => {
if (arr.length === 0) {
return BigNumber.from(0); // or handle this case appropriately
}

const hex = Array.from(arr)
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");

return BigNumber.from("0x" + hex);
};
Copy link
Contributor

Choose a reason for hiding this comment

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

This should fwiw be unnecessary - BigNumber seems to handle Uint8Arrays natively.

> arr = new Uint8Array([255])
Uint8Array(1) [ 255 ]
> ethers.BigNumber.from(arr).toNumber()
255

@@ -147,4 +237,16 @@ describe("SvmCpiEventsClient (integration)", () => {
expect(events).to.have.lengthOf(1);
expect(events[0].data.inputAmount).to.equal(secondDeposit.inputAmount);
});

it.only("creates and reads a single request slow fill event", async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

One more

Suggested change
it.only("creates and reads a single request slow fill event", async () => {
it("creates and reads a single request slow fill event", async () => {

Comment on lines 242 to 250
await sendRequestSlowFill();

const [requestSlowFillEvent] = await client.queryEvents("RequestedSlowFill");

const { data } = requestSlowFillEvent as { data: SvmSpokeClient.RequestedSlowFill };

expect(data.depositor).to.equal(signer.address.toString());
expect(data.recipient).to.equal(signer.address.toString());
expect(data.inputToken).to.equal(mint.address.toString());
Copy link
Contributor

@pxrl pxrl May 12, 2025

Choose a reason for hiding this comment

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

Is it worth constructing sendRequestSlowFill() here such that it accepts a RelayData input? Then we can compare the RelayData hashes on the output side to verify that the request was submitted correctly. This would also be convenient as a utility function down in the relayer, because we currently don't have any slow fill request support for SVM.

Copy link
Contributor Author

@md0x md0x May 12, 2025

Choose a reason for hiding this comment

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

Thanks @pxrl !
I added the generic deposit, requestSlowFill, and createFill functions here:

sdk/test/utils/svm/utils.ts

Lines 264 to 339 in 02128c2

// Executes a deposit into the SVM Spoke vault.
export const deposit = async (
signer: KeyPairSigner,
solanaClient: RpcClient,
depositInput: SvmSpokeClient.DepositInput,
tokenDecimals: number
) => {
const approveIx = getApproveCheckedInstruction({
source: depositInput.depositorTokenAccount,
mint: depositInput.mint,
delegate: depositInput.state,
owner: depositInput.depositor,
amount: depositInput.inputAmount,
decimals: tokenDecimals,
});
const depositIx = await SvmSpokeClient.getDepositInstruction(depositInput);
return pipe(
await createDefaultTransaction(solanaClient, signer),
(tx) => appendTransactionMessageInstruction(approveIx, tx),
(tx) => appendTransactionMessageInstruction(depositIx, tx),
(tx) => signAndSendTransaction(solanaClient, tx)
);
};
// Requests a slow fill
export const requestSlowFill = async (
signer: KeyPairSigner,
solanaClient: RpcClient,
depositInput: SvmSpokeClient.RequestSlowFillInput
) => {
const requestSlowFillIx = await SvmSpokeClient.getRequestSlowFillInstruction(depositInput);
return pipe(
await createDefaultTransaction(solanaClient, signer),
(tx) => appendTransactionMessageInstruction(requestSlowFillIx, tx),
(tx) => signAndSendTransaction(solanaClient, tx)
);
};
// Creates a fill
export const createFill = async (
signer: KeyPairSigner,
solanaClient: RpcClient,
fillInput: SvmSpokeClient.FillRelayInput,
tokenDecimals: number
) => {
const approveIx = getApproveCheckedInstruction({
source: fillInput.relayerTokenAccount,
mint: fillInput.mint,
delegate: fillInput.state,
owner: fillInput.signer,
amount: (fillInput.relayData as SvmSpokeClient.RelayDataArgs).outputAmount,
decimals: tokenDecimals,
});
const createAssociatedTokenIdempotentIx = getCreateAssociatedTokenIdempotentInstruction({
payer: signer,
owner: (fillInput.relayData as SvmSpokeClient.RelayDataArgs).recipient,
mint: fillInput.mint,
ata: fillInput.recipientTokenAccount,
systemProgram: SYSTEM_PROGRAM_ADDRESS,
tokenProgram: fillInput.tokenProgram,
});
const createFillIx = await SvmSpokeClient.getFillRelayInstruction(fillInput);
return pipe(
await createDefaultTransaction(solanaClient, signer),
(tx) => appendTransactionMessageInstruction(createAssociatedTokenIdempotentIx, tx),
(tx) => appendTransactionMessageInstruction(approveIx, tx),
(tx) => appendTransactionMessageInstruction(createFillIx, tx),
(tx) => signAndSendTransaction(solanaClient, tx)
);
};

They take SVM-typed inputs to keep things flexible.

For the event tests, I made simpler wrappers that also return the relayData object, so it’s easy to check that the events are logging correctly:

// helper to create a deposit
const sendCreateDeposit = async (payerAta: Address, inputAmount: bigint, outputAmount: bigint) => {
const latestSlot = await solanaClient.rpc.getSlot({ commitment: "confirmed" }).send();
const currentTime = await solanaClient.rpc.getBlockTime(latestSlot).send();
const depositInput: SvmSpokeClient.DepositInput = {
depositor: signer.address,
recipient: signer.address,
inputToken: mint.address,
outputToken: getRandomSvmAddress(),
inputAmount,
outputAmount,
destinationChainId: Number(mainnetId),
exclusiveRelayer: signer.address,
quoteTimestamp: Number(currentTime),
fillDeadline: Number(currentTime) + 60 * 30, // 30‑minute deadline
exclusivityParameter: 1,
message: new Uint8Array(),
state,
route,
depositorTokenAccount: payerAta,
vault,
mint: mint.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS,
program: SvmSpokeClient.SVM_SPOKE_PROGRAM_ADDRESS,
eventAuthority,
signer,
};
const signature = await deposit(signer, solanaClient, depositInput, decimals);
return { signature, depositInput };
};
// helper to send a request slow fill
const sendRequestSlowFill = async (/* add params as needed */) => {
const latestSlot = await solanaClient.rpc.getSlot({ commitment: "confirmed" }).send();
const currentTime = await getTimestampForSlot(solanaClient.rpc, Number(latestSlot));
const relayData: SvmSpokeClient.RequestSlowFillInstructionDataArgs["relayData"] = {
depositor: getRandomSvmAddress(),
recipient: getRandomSvmAddress(),
exclusiveRelayer: SVM_ZERO_ADDRESS,
inputToken: getRandomSvmAddress(),
outputToken: getRandomSvmAddress(),
inputAmount: getRandomInt(),
outputAmount: getRandomInt(),
originChainId: BigInt(mainnetId),
depositId: new Uint8Array(intToU8Array32(getRandomInt())),
fillDeadline: Number(currentTime) + 60 * 30,
exclusivityDeadline: 0,
message: new Uint8Array(),
};
const formattedRelayData = formatRelayData(relayData);
const relayDataHash = getRelayDataHash(formattedRelayData, solanaChainId);
const fillStatusPda = await getFillStatusPda(
SvmSpokeClient.SVM_SPOKE_PROGRAM_ADDRESS,
formattedRelayData,
solanaChainId
);
const requestSlowFillInput: SvmSpokeClient.RequestSlowFillInput = {
program: SvmSpokeClient.SVM_SPOKE_PROGRAM_ADDRESS,
relayHash: arrayify(relayDataHash),
relayData: relayData,
state,
fillStatus: fillStatusPda,
systemProgram: SYSTEM_PROGRAM_ADDRESS,
eventAuthority,
signer,
};
const signature = await requestSlowFill(signer, solanaClient, requestSlowFillInput);
return { signature, relayData };
};
// helper to send a fill
const sendCreateFill = async (/* add params as needed */) => {
const latestSlot = await solanaClient.rpc.getSlot({ commitment: "confirmed" }).send();
const currentTime = await getTimestampForSlot(solanaClient.rpc, Number(latestSlot));
const relayData: SvmSpokeClient.FillRelayInput["relayData"] = {
depositor: getRandomSvmAddress(),
recipient: getRandomSvmAddress(),
exclusiveRelayer: SVM_ZERO_ADDRESS,
inputToken: getRandomSvmAddress(),
outputToken: mint.address,
inputAmount: getRandomInt(),
outputAmount: tokenAmount,
originChainId: BigInt(mainnetId),
depositId: new Uint8Array(intToU8Array32(getRandomInt())),
fillDeadline: Number(currentTime) + 60 * 30,
exclusivityDeadline: Number(currentTime) + 60 * 30,
message: new Uint8Array(),
};
const formattedRelayData = formatRelayData(relayData);
const relayDataHash = getRelayDataHash(formattedRelayData, solanaChainId);
const fillStatusPda = await getFillStatusPda(
SvmSpokeClient.SVM_SPOKE_PROGRAM_ADDRESS,
formattedRelayData,
solanaChainId
);
const payerAta = await getAssociatedTokenAddress(
SvmAddress.from(signer.address),
SvmAddress.from(mint.address),
TOKEN_2022_PROGRAM_ADDRESS
);
const recipientAta = await getAssociatedTokenAddress(
SvmAddress.from(relayData.recipient),
SvmAddress.from(mint.address),
TOKEN_2022_PROGRAM_ADDRESS
);
const fillInput: SvmSpokeClient.FillRelayInput = {
signer: signer,
instructionParams: undefined,
state: state,
mint: mint.address,
relayerTokenAccount: payerAta,
recipientTokenAccount: recipientAta,
fillStatus: fillStatusPda,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ADDRESS,
systemProgram: SYSTEM_PROGRAM_ADDRESS,
eventAuthority: eventAuthority,
program: SvmSpokeClient.SVM_SPOKE_PROGRAM_ADDRESS,
relayHash: arrayify(relayDataHash),
relayData: relayData,
repaymentChainId: BigInt(mainnetId),
repaymentAddress: signer.address,
};
const signature = await createFill(signer, solanaClient, fillInput, decimals);
return { signature, relayData };
};

md0x added 2 commits May 12, 2025 13:09
Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com>
@md0x md0x marked this pull request as ready for review May 12, 2025 11:13
@md0x md0x changed the title feat: add request slow fill function wrapper and event test feat: add request slow fill and fill function wrappers and event tests May 12, 2025
@md0x md0x changed the title feat: add request slow fill and fill function wrappers and event tests feat: add request slow fill and fill functions and event tests May 12, 2025
arrayify(hexZeroPad(hexlify(relayData.depositId), 32)),
Uint8Array.from(uint32Encoder.encode(relayData.fillDeadline)),
Uint8Array.from(uint32Encoder.encode(relayData.exclusivityDeadline)),
hashNonEmptyMessage(Buffer.from(relayData.message)),
Copy link
Contributor Author

@md0x md0x May 12, 2025

Choose a reason for hiding this comment

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

messages need to use hashNonEmptyMessage

Copy link
Contributor

Choose a reason for hiding this comment

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

I tested this but it was not working. The issue is that hashNonEmptyMessage determines if a message is empty based on the length of the Buffer but we usually represent empty messages as '0x' which Buffer is not empty. I think there are two options as a work around:

  1. For SVM we only use a void string for empty messages. We'll have to change this line.

  2. We handle it in this function by doing something like this:

const messageBuffer = isMessageEmpty(relayData.message)
    ? new Uint8Array(32)
    : hashNonEmptyMessage(Buffer.from(relayData.message.slice(2), "hex"));

Not sure what would be the best approach. @james-a-morris @pxrl any thoughts? Is opt 1 feasible?

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, if 1. is feasible, I think it would be safer to do both since EVM spoke pools might still represent empty messages as '0x'.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call, @melisaguevara!

Since relay messages on Solana are passed as binary data to the program, and relayHash expects a string for EVM compatibility, we now hex-encode the Solana relay message before passing it in. We can then decode it prior to hashing.

I’ve added those changes in my last commit.

Also, note that I updated formatRelayData to hexlify the original binary message accordingly.

Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome, @md0x! It is working now! 😃

Copy link
Contributor

Choose a reason for hiding this comment

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

So @md0x @melisaguevara does this mean that empty messages on SVM chains will be a non-empty buffer? ("0x")

Copy link
Contributor

@melisaguevara melisaguevara May 14, 2025

Choose a reason for hiding this comment

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

@gsteenkamp89 onchain they will be an empty buffer (see this deposit for example). But the SvmSpokePoolClient and any other function using the svm util unwrapEventData will parse those empty buffers and set the message to '0x' (see this line). Do you think that's ok?

@melisaguevara melisaguevara self-requested a review May 12, 2025 17:49
Copy link
Contributor

@melisaguevara melisaguevara left a comment

Choose a reason for hiding this comment

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

left a comment regarding getRelayDataHash

Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com>
@md0x md0x requested review from melisaguevara and pxrl May 13, 2025 15:26
export const SVM_SPOKE_SEED = BigInt(0);
export const SVM_ZERO_ADDRESS = address("11111111111111111111111111111111");
Copy link
Member

Choose a reason for hiding this comment

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

ooc is this somewhere exportable from the SVM packages we use?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point.
I’ve updated it. Technically it’s not the 0 address, but the default address, which happens to be the same as SYSTEM_PROGRAM_ADDRESS.
I went ahead and exported it as SVM_DEFAULT_ADDRESS so it’s clearer what it’s for.

@@ -42,7 +79,7 @@ describe("SvmCpiEventsClient (integration)", () => {
outputToken: getRandomSvmAddress(),
inputAmount,
outputAmount,
destinationChainId: Number(destinationChainId),
destinationChainId: Number(mainnetId),
Copy link
Member

Choose a reason for hiding this comment

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

OOC do we want to keep this generic?

Copy link
Contributor

@melisaguevara melisaguevara left a comment

Choose a reason for hiding this comment

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

LGTM!

Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com>
@md0x md0x requested a review from james-a-morris May 14, 2025 12:52
@md0x md0x merged commit 9b3bb77 into epic/svm-client May 14, 2025
4 checks passed
@md0x md0x deleted the pablo/slow-fill branch May 14, 2025 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants