Skip to content
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

feat(svm): svm spoke events client #899

Open
wants to merge 18 commits into
base: master
Choose a base branch
from

Conversation

md0x
Copy link

@md0x md0x commented Feb 25, 2025

Changes proposed in this PR:

  • Add SvmSpokeEventsClient, an events client that fetches CPI events from SvmSpoke by event type, with optional filtering by fromSlot and/or toSlot.

How to use it:

import { SvmSpokeClient } from "@across-protocol/contracts";
import { createSolanaRpc } from "@solana/web3-v2.js";
import { SvmSpokeEventsClient } from "./src/svm";

async function main() {
    const rpc = createSolanaRpc("YOUR_RPC_URL");
    const eventsClient = await SvmSpokeEventsClient.create(rpc);
    const fromSlot = BigInt(362424536);
    const toSlot = BigInt(363326129);
    const events = await eventsClient.queryEvents<SvmSpokeClient.FilledRelay>("FilledRelay", fromSlot, toSlot);
    console.log(events);
}

main();

md0x added 5 commits February 24, 2025 11:37
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Copy link

linear bot commented Feb 25, 2025

Signed-off-by: Pablo Maldonado <[email protected]>
@@ -12,6 +12,8 @@
"node": ">=20.18.0"
},
"scripts": {
"build-bigint-buffer": "[ -d node_modules/bigint-buffer ] && command -v node-gyp > /dev/null && cd node_modules/bigint-buffer && node-gyp configure && node-gyp build || echo 'Skipping bigint-buffer build: folder or node-gyp not found'",
Copy link
Author

Choose a reason for hiding this comment

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

Same as in relayer to get rid of the bigint-buffer warning

@@ -105,7 +107,9 @@
"@eth-optimism/sdk": "^3.3.1",
"@ethersproject/bignumber": "^5.7.0",
"@pinata/sdk": "^2.1.0",
"@solana/web3.js": "^2.0.0",
"@solana/web3-v2.js": "npm:@solana/web3.js@2",
Copy link
Author

Choose a reason for hiding this comment

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

We need to have both versions because the BorshEncoder uses v1 internally

md0x added 3 commits February 25, 2025 19:00
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
* Gets the event name from a raw name.
*/
export function getEventName(rawName?: string): EventName {
if (!rawName) throw new Error("Raw name is undefined");
Copy link
Author

@md0x md0x Feb 25, 2025

Choose a reason for hiding this comment

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

This should never happen so we throw an error

Copy link
Contributor

Choose a reason for hiding this comment

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

why accept optional param in the first place?

case "TransferredOwnership":
return eventData as TransferredOwnershipEvent;
default:
throw new Error(`Unknown event name: ${name}`);
Copy link
Author

Choose a reason for hiding this comment

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

Same here

md0x added 2 commits February 25, 2025 19:37
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
@md0x md0x marked this pull request as ready for review February 25, 2025 19:04
import { getEventName, parseEventData } from "./utils/events";
import { isDevnet } from "./utils/helpers";

type GetTransactionReturnType = ReturnType<GetTransactionApi["getTransaction"]>;
Copy link
Contributor

Choose a reason for hiding this comment

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

This might be bit risky as this infers the union of all overloads. E.g. if the caller passed { encoding: "base58" } then returned transaction property would be of type Base58EncodedDataResponse and the event client would error when trying to access something like txResult.transaction.message.accountKeys.

I think the above does not exactly apply to us as we are calling getTransaction without setting encoding, so it matches the optional json overload which has the props we are trying to access. But maybe its worth adding a comment on this for safer code maintenance.

Copy link
Author

Choose a reason for hiding this comment

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

Good call. I've added a type wrapper to extract only the "json" encoded return type.

): Promise<{ program: Address; data: EventData; name: EventName }[]> {
if (!txResult) return [];

const eventAuthorities: Map<string, Address> = new Map();
Copy link
Contributor

Choose a reason for hiding this comment

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

is mapping necessary? we only have single program in this method

Comment on lines 195 to 196
programId == ixProgramId &&
eventAuthorities.get(ixProgramId.toString()) == singleIxAccount
Copy link
Contributor

Choose a reason for hiding this comment

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

should these use strict evaluation ===?

Comment on lines 202 to 207
const name = getEventName(event?.name);
events.push({
program: programId,
data: parseEventData(event?.data),
name,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe parsing should be in separate function that caller can try to parse. getEventName and parseEventData would not work if the caller passed other program than spoke pool

Copy link
Contributor

Choose a reason for hiding this comment

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

alternatively, we could omit passing program from all private methods and use only the spoke pool

fromSlot?: bigint,
toSlot?: bigint,
options: GetSignaturesForAddressConfig = { limit: 1000 },
finality: Commitment = "confirmed"
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can we better use commitment for the parameter name.

Comment on lines 79 to 80
program: Address,
anchorIdl: Idl,
Copy link
Contributor

Choose a reason for hiding this comment

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

these might be not necessary as we use them in private methods and it should work only for the spoke pool program. Except, maybe keep program address, but that is useful only if the public create method allowed passing a custom spoke address.

Comment on lines 60 to 61
options: GetSignaturesForAddressConfig = { limit: 1000 },
finality: Commitment = "confirmed"
Copy link
Contributor

Choose a reason for hiding this comment

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

does it make sense to pass separate commitment in the finality parameter (used in getTransaction calls) from options.commitment that gets used in getSignaturesForAddress calls?

const events = await this.readEventsFromSignature(signatureTransaction.signature, program, anchorIdl, finality);
return events.map((event) => ({
...event,
confirmationStatus: signatureTransaction.confirmationStatus || "Unknown",
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe pass the Commitment | null type as is?

return events.map((event) => ({
...event,
confirmationStatus: signatureTransaction.confirmationStatus || "Unknown",
blockTime: signatureTransaction.blockTime || unixTimestamp(BigInt(0)),
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe pass UnixTimestamp | null type as is?

md0x added 4 commits February 28, 2025 16:40
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
Signed-off-by: Pablo Maldonado <[email protected]>
@md0x md0x requested a review from Reinis-FRP February 28, 2025 16:11

while (hasMoreSignatures) {
const signatures: GetSignaturesForAddressApiResponse = await this.rpc
.getSignaturesForAddress(this.svmSpokeAddress, currentOptions)
Copy link
Member

Choose a reason for hiding this comment

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

Is a signature any transaction sent to the svmSpokeAddress?

options: GetSignaturesForAddressConfig = { limit: 1000, commitment: "confirmed" }
): Promise<EventWithData<T>[]> {
const events = await this.queryAllEvents(fromSlot, toSlot, options);
return events.filter((event) => event.name === eventName) as EventWithData<T>[];
Copy link
Member

Choose a reason for hiding this comment

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

So there's no way to query the rpc for just the eventName? You always need to query all events and then filter from there?

Seems expensive but if we're smart about setting from/toSlot we can cache the results

Copy link
Member

@nicholaspai nicholaspai left a comment

Choose a reason for hiding this comment

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

i just left some questions for my own knowledge

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.

3 participants