-
Notifications
You must be signed in to change notification settings - Fork 18
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
Conversation
|
||
describe("SvmCpiEventsClient (integration)", () => { | ||
describe.only("SvmCpiEventsClient (integration)", () => { |
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.
Watch out for this one 👀
describe.only("SvmCpiEventsClient (integration)", () => { | |
describe("SvmCpiEventsClient (integration)", () => { |
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); | ||
}; |
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.
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 () => { |
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.
One more
it.only("creates and reads a single request slow fill event", async () => { | |
it("creates and reads a single request slow fill event", async () => { |
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()); |
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.
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.
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.
Thanks @pxrl !
I added the generic deposit
, requestSlowFill
, and createFill
functions here:
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:
sdk/test/Solana.SvmCpiEventsClient.Integration.test.ts
Lines 70 to 205 in 02128c2
// 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 }; | |
}; |
Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com>
src/arch/svm/SpokeUtils.ts
Outdated
arrayify(hexZeroPad(hexlify(relayData.depositId), 32)), | ||
Uint8Array.from(uint32Encoder.encode(relayData.fillDeadline)), | ||
Uint8Array.from(uint32Encoder.encode(relayData.exclusivityDeadline)), | ||
hashNonEmptyMessage(Buffer.from(relayData.message)), |
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.
messages need to use hashNonEmptyMessage
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.
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:
-
For SVM we only use a void string for empty messages. We'll have to change this line.
-
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?
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.
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'.
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.
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.
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.
Awesome, @md0x! It is working now! 😃
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.
So @md0x @melisaguevara does this mean that empty messages on SVM chains will be a non-empty buffer? ("0x")
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.
@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?
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.
left a comment regarding getRelayDataHash
Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com>
src/arch/svm/constants.ts
Outdated
export const SVM_SPOKE_SEED = BigInt(0); | ||
export const SVM_ZERO_ADDRESS = address("11111111111111111111111111111111"); |
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.
ooc is this somewhere exportable from the SVM packages we use?
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.
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), |
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.
OOC do we want to keep this generic?
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.
LGTM!
Signed-off-by: Pablo Maldonado <pablomaldonadoturci@gmail.com>
Changes proposed in this PR:
RequestedSlowFill
andFilledRelay
events integration tests.requestSlowFill
andcreateFill
functions