Skip to content

Commit 447d77c

Browse files
authored
feat: show how to find fill status Events and fill status by querying fill StatusPDA (#911)
* WIP Signed-off-by: Chris Maree <christopher.maree@gmail.com> * Discard changes to scripts/svm/closeRelayerPdas.ts * WIP Signed-off-by: Chris Maree <christopher.maree@gmail.com> * WIP Signed-off-by: Chris Maree <christopher.maree@gmail.com> * WIP Signed-off-by: Chris Maree <christopher.maree@gmail.com> * WIP Signed-off-by: Chris Maree <christopher.maree@gmail.com> * WIP Signed-off-by: Chris Maree <christopher.maree@gmail.com> --------- Signed-off-by: Chris Maree <christopher.maree@gmail.com>
1 parent 0991928 commit 447d77c

File tree

4 files changed

+188
-14
lines changed

4 files changed

+188
-14
lines changed

Anchor.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ generateExternalTypes = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/gener
5151
fakeFillWithRandomDistribution = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/fakeFillWithRandomDistribution.ts"
5252
addressToPublicKey = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/addressToPublicKey.ts"
5353
publicKeyToAddress = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/publicKeyToAddress.ts"
54+
findFillStatusPdaFromEvent = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/findFillStatusPdaFromEvent.ts"
55+
findFillStatusFromFillStatusPda = "NODE_NO_WARNINGS=1 yarn run ts-node ./scripts/svm/findFillStatusFromFillStatusPda.ts"
5456

5557
[test.validator]
5658
url = "https://api.mainnet-beta.solana.com"
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// This script finds the fillStatus (fillStatus + event) from a provided fillStatusPda.
2+
import * as anchor from "@coral-xyz/anchor";
3+
import { AnchorProvider, BN, Program } from "@coral-xyz/anchor";
4+
import { address, createSolanaRpc } from "@solana/web3-v2.js";
5+
import yargs from "yargs";
6+
import { hideBin } from "yargs/helpers";
7+
import { SvmSpokeIdl } from "../../src/svm";
8+
9+
import { readFillEventFromFillStatusPda } from "../../src/svm/web3-v2/solanaProgramUtils";
10+
import { program } from "@coral-xyz/anchor/dist/cjs/native/system";
11+
import { SvmSpoke } from "../../target/types/svm_spoke";
12+
13+
// Set up the provider
14+
const provider = AnchorProvider.env();
15+
anchor.setProvider(provider);
16+
17+
const argv = yargs(hideBin(process.argv))
18+
.option("fillStatusPda", { type: "string", demandOption: true, describe: "Fill Status PDA" })
19+
.option("programId", { type: "string", demandOption: true, describe: "SvmSpoke ID" }).argv;
20+
21+
async function findFillStatusFromFillStatusPda(): Promise<void> {
22+
const resolvedArgv = await argv;
23+
const fillStatusPda = address(resolvedArgv.fillStatusPda);
24+
25+
console.log(`Looking for Fill Event for Fill Status PDA: ${fillStatusPda.toString()}`);
26+
27+
const rpc = createSolanaRpc(provider.connection.rpcEndpoint);
28+
const { event, slot } = await readFillEventFromFillStatusPda(
29+
rpc,
30+
fillStatusPda,
31+
address(resolvedArgv.programId),
32+
SvmSpokeIdl
33+
);
34+
if (!event) {
35+
console.log("No fill events found");
36+
return;
37+
}
38+
39+
console.table(
40+
Object.entries(event.data).map(([key, value]) => {
41+
if (key === "relay_execution_info") {
42+
const info = value as any;
43+
return { Property: key, Value: `relayer: ${info.relayer}, executionTimestamp: ${info.executionTimestamp}` };
44+
}
45+
return { Property: key, Value: (value as any).toString() };
46+
})
47+
);
48+
49+
const program = anchor.workspace.SvmSpoke as Program<SvmSpoke>;
50+
let fillStatus;
51+
try {
52+
const fillStatusResponse = await program.account.fillStatusAccount.fetch(fillStatusPda);
53+
fillStatus = Object.keys(fillStatusResponse.status)[0];
54+
} catch (error) {
55+
fillStatus = "filled";
56+
}
57+
58+
console.log("fillStatus", fillStatus);
59+
console.log("slot", slot);
60+
}
61+
62+
findFillStatusFromFillStatusPda();
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// This script finds the associated Fill Status PDA from a fill OR deposit event by re-deriving it without doing any
2+
// // on-chain calls. Note the props required are present in both deposit and fill events.
3+
// Example usage:
4+
// anchor run findFillStatusPdaFromFill -- \
5+
// --input_token "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238" \
6+
// --output_token "wBeYLVBabtv4cyb7RyMmRxvRSkRsCP4PMBCJRw66kKC" \
7+
// --input_amount "1" \
8+
// --output_amount "1" \
9+
// --repayment_chain_id "133268194659241" \
10+
// --origin_chain_id "11155111" \
11+
// --deposit_id "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,98" \
12+
// --fill_deadline 1740569770 \
13+
// --exclusivity_deadline 1740569740 \
14+
// --exclusive_relayer "0x0000000000000000000000000000000000000000" \
15+
// --depositor "0x9a8f92a830a5cb89a3816e3d267cb7791c16b04d" \
16+
// --recipient "5HRmK3G6BzWAtF22dBgoTiPGVovSmG4rLvVQoUhum9FJ" \
17+
// --message_hash "0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0"
18+
19+
import * as anchor from "@coral-xyz/anchor";
20+
import { PublicKey } from "@solana/web3.js";
21+
import { BN } from "@coral-xyz/anchor";
22+
import yargs from "yargs";
23+
import { hideBin } from "yargs/helpers";
24+
import { calculateRelayEventHashUint8Array, getSpokePoolProgram, evmAddressToPublicKey } from "../../src/svm/web3-v1";
25+
26+
// Set up the provider
27+
const provider = anchor.AnchorProvider.env();
28+
anchor.setProvider(provider);
29+
const program = getSpokePoolProgram(provider);
30+
const programId = program.programId;
31+
32+
// Parse arguments
33+
const argv = yargs(hideBin(process.argv))
34+
.option("input_token", { type: "string", demandOption: true, describe: "Input token address" })
35+
.option("output_token", { type: "string", demandOption: true, describe: "Output token address" })
36+
.option("input_amount", { type: "string", demandOption: true, describe: "Input amount" })
37+
.option("output_amount", { type: "string", demandOption: true, describe: "Output amount" })
38+
.option("repayment_chain_id", { type: "string", demandOption: true, describe: "Repayment chain ID" })
39+
.option("origin_chain_id", { type: "string", demandOption: true, describe: "Origin chain ID" })
40+
.option("deposit_id", { type: "string", demandOption: true, describe: "Deposit ID" })
41+
.option("fill_deadline", { type: "number", demandOption: true, describe: "Fill deadline" })
42+
.option("exclusivity_deadline", { type: "number", demandOption: true, describe: "Exclusivity deadline" })
43+
.option("exclusive_relayer", { type: "string", demandOption: true, describe: "Exclusive relayer address" })
44+
.option("depositor", { type: "string", demandOption: true, describe: "Depositor address" })
45+
.option("recipient", { type: "string", demandOption: true, describe: "Recipient address" })
46+
.option("message_hash", { type: "string", demandOption: true, describe: "Message hash" }).argv;
47+
48+
async function findFillStatusPda() {
49+
const resolvedArgv = await argv;
50+
const relayEventData = {
51+
depositor: convertAddress(resolvedArgv.depositor),
52+
recipient: convertAddress(resolvedArgv.recipient),
53+
exclusiveRelayer: convertAddress(resolvedArgv.exclusive_relayer),
54+
inputToken: convertAddress(resolvedArgv.input_token),
55+
outputToken: convertAddress(resolvedArgv.output_token),
56+
inputAmount: new BN(resolvedArgv.input_amount),
57+
outputAmount: new BN(resolvedArgv.output_amount),
58+
originChainId: new BN(resolvedArgv.origin_chain_id),
59+
depositId: parseStringToUint8Array(resolvedArgv.deposit_id),
60+
fillDeadline: resolvedArgv.fill_deadline,
61+
exclusivityDeadline: resolvedArgv.exclusivity_deadline,
62+
messageHash: parseStringToUint8Array(resolvedArgv.message_hash),
63+
};
64+
65+
console.log("finding fill status pda for relay event data:");
66+
console.table(Object.entries(relayEventData).map(([key, value]) => ({ Property: key, Value: value.toString() })));
67+
68+
const chainId = new BN(resolvedArgv.repayment_chain_id);
69+
const relayHashUint8Array = calculateRelayEventHashUint8Array(relayEventData, chainId);
70+
const [fillStatusPda] = PublicKey.findProgramAddressSync([Buffer.from("fills"), relayHashUint8Array], programId);
71+
console.log("Fill Status PDA Address:", fillStatusPda.toString());
72+
}
73+
74+
findFillStatusPda().catch(console.error);
75+
76+
function convertAddress(address: string) {
77+
if (address.startsWith("0x")) return evmAddressToPublicKey(address);
78+
return new PublicKey(address);
79+
}
80+
81+
function parseStringToUint8Array(inputString: string): Uint8Array {
82+
const numberArray = inputString.split(",").map(Number);
83+
return new Uint8Array(numberArray);
84+
}

src/svm/web3-v2/solanaProgramUtils.ts

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,7 @@ export async function readProgramEvents(
2424
finality: Commitment = "confirmed",
2525
options: GetSignaturesForAddressConfig = { limit: 1000 }
2626
) {
27-
const allSignatures: GetSignaturesForAddressTransaction[] = [];
28-
29-
// Fetch all signatures in sequential batches
30-
while (true) {
31-
const signatures = await rpc.getSignaturesForAddress(program, options).send();
32-
allSignatures.push(...signatures);
33-
34-
// Update options for the next batch. Set before to the last fetched signature.
35-
if (signatures.length > 0) {
36-
options = { ...options, before: signatures[signatures.length - 1].signature };
37-
}
38-
39-
if (options.limit && signatures.length < options.limit) break; // Exit early if the number of signatures < limit
40-
}
27+
const allSignatures: GetSignaturesForAddressTransaction[] = await searchSignaturesUntilLimit(rpc, program, options);
4128

4229
// Fetch events for all signatures in parallel
4330
const eventsWithSlots = await Promise.all(
@@ -57,6 +44,27 @@ export async function readProgramEvents(
5744
return eventsWithSlots.flat();
5845
}
5946

47+
async function searchSignaturesUntilLimit(
48+
rpc: web3.Rpc<web3.SolanaRpcApiFromTransport<RpcTransport>>,
49+
program: Address,
50+
options: GetSignaturesForAddressConfig = { limit: 1000 }
51+
): Promise<GetSignaturesForAddressTransaction[]> {
52+
const allSignatures: GetSignaturesForAddressTransaction[] = [];
53+
// Fetch all signatures in sequential batches
54+
while (true) {
55+
const signatures = await rpc.getSignaturesForAddress(program, options).send();
56+
allSignatures.push(...signatures);
57+
58+
// Update options for the next batch. Set before to the last fetched signature.
59+
if (signatures.length > 0) {
60+
options = { ...options, before: signatures[signatures.length - 1].signature };
61+
}
62+
63+
if (options.limit && signatures.length < options.limit) break; // Exit early if the number of signatures < limit
64+
}
65+
return allSignatures;
66+
}
67+
6068
/**
6169
* Reads events from a transaction.
6270
*/
@@ -119,3 +127,21 @@ async function processEventFromTx(
119127

120128
return events;
121129
}
130+
131+
/**
132+
* For a given fillStatusPDa & associated spokePool ProgramID, return the fill event.
133+
*/
134+
export async function readFillEventFromFillStatusPda(
135+
rpc: web3.Rpc<web3.SolanaRpcApiFromTransport<RpcTransport>>,
136+
fillStatusPda: Address,
137+
programId: Address,
138+
programIdl: Idl
139+
): Promise<{ event: any; slot: number }> {
140+
const signatures = await searchSignaturesUntilLimit(rpc, fillStatusPda);
141+
if (signatures.length === 0) return { event: null, slot: 0 };
142+
143+
// The first signature will always be PDA creation, and therefore CPI event carrying signature. Any older signatures
144+
// will therefore be either spam or PDA closure signatures and can be ignored when looking for the fill event.
145+
const events = await readEvents(rpc, signatures[signatures.length - 1].signature, programId, programIdl);
146+
return { event: events[0], slot: Number(signatures[signatures.length - 1].slot) };
147+
}

0 commit comments

Comments
 (0)