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(solana): rate limited solana provider #833

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

Conversation

Reinis-FRP
Copy link
Contributor

@Reinis-FRP Reinis-FRP commented Jan 22, 2025

This implements the same rate limited provider logic as src/providers/rateLimitedProvider.ts, but for Solana using V2 of @solana/web3.js library that allows creating RPC clients with composable transports.

Note that this @solana/web3.js library produces build errors and CI node version would need to be updated.

It would be quite a lift to implement mocked Solana RPC node for tests, but the expected behavior was tested with:

import { createDefaultRpcTransport, createSolanaRpcFromTransport } from "@solana/web3.js";
import winston from "winston";
import { RateLimitedSolanaRpcFactory } from "@across-protocol/sdk";

const maxConcurrency = 1;
const pctRpcCallsLogged = 10;
const logger = winston.createLogger({
  transports: [new winston.transports.Console()],
  level: "debug",
});
const chainId = 34268394551451; // Last 6 bytes of keccak256("solana-mainnet")
const url = "https://api.mainnet-beta.solana.com";
const rateLimitedRpcClient = new RateLimitedSolanaRpcFactory(maxConcurrency, pctRpcCallsLogged, logger, chainId, {
  url,
}).createRpcClient();

const rateLimitedRequests = Array.from({ length: 20 }, () => rateLimitedRpcClient.getSlot().send());
const rateLimitedResults = await Promise.all(rateLimitedRequests);
console.log("Rate limited results:", rateLimitedResults);

const regularRpc = createSolanaRpcFromTransport(createDefaultRpcTransport({ url }));
const regularRequests = Array.from({ length: 20 }, () => regularRpc.getSlot().send());
const regularResults = await Promise.all(regularRequests);
console.log("Regular results:", regularResults);

This produces following logs showing that new Solana slots get mined during rate limited requests (with concurrency set to 1) while normally these concurrent requests would resolve to the same slot:

{"at":"SolanaProviderUtils","chainId":34268394551451,"datadog":true,"level":"debug","message":"Solana provider response sample","method":"getSlot","params":[{"commitment":"confirmed"}],"provider":"https://api.mainnet-beta.solana.com","success":true,"timeElapsed":0.03452311199999895}
Rate limited results: [
  315643630n, 315643630n,
  315643630n, 315643630n,
  315643631n, 315643631n,
  315643631n, 315643631n,
  315643631n, 315643631n,
  315643631n, 315643631n,
  315643631n, 315643631n,
  315643631n, 315643631n,
  315643631n, 315643631n,
  315643631n, 315643632n
]
Regular results: [
  315643632n, 315643632n,
  315643632n, 315643632n,
  315643632n, 315643632n,
  315643632n, 315643632n,
  315643632n, 315643632n,
  315643632n, 315643632n,
  315643632n, 315643632n,
  315643632n, 315643632n,
  315643632n, 315643632n,
  315643632n, 315643632n
]

Signed-off-by: Reinis Martinsons <[email protected]>
Signed-off-by: Reinis Martinsons <[email protected]>
@Reinis-FRP Reinis-FRP requested a review from pxrl January 22, 2025 14:54
// This sets up the queue. Each task is executed by forwarding the RPC request to the underlying base transport.
// This queue sends out requests concurrently, but stops once the concurrency limit is reached. The maxConcurrency
// is configured here.
this.queue = queue(async ({ rpcArgs, resolve, reject }: SolanaRateLimitTask, callback: () => void) => {
Copy link
Member

@nicholaspai nicholaspai Jan 22, 2025

Choose a reason for hiding this comment

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

This queuing logic looks almost identical to the EVM version, is there any way to refactor and re-use code? We might be able to unit test any refactored code more easily


// In this path we log an rpc response sample.
// Note: use performance.now() to ensure a purely monotonic clock.
const startTime = performance.now();
Copy link
Member

Choose a reason for hiding this comment

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

I also think it could be useful to refactor and re-use some of this elapsedTime logic. It would also be a bit easier to unit test if we abstract the hypothetical re-used function well.

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.

This is really well written code! I think we can possibly try to reuse some code from the evm provider and that might make unit testing with mocks even easier!

message: "Solana provider response sample",
provider: getOriginFromURL(this.clusterUrl),
method: payload.method,
params: payload.params,
Copy link

Choose a reason for hiding this comment

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

Is it safe to send the params in the payload to Winston without validating or serializing them first?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it should be safe as we expect these requests to originate via Solana web3.js library. In addition, we use createDefaultRpcTransport here to create the underlying transport that internally type checks the request here.

Copy link

@md0x md0x left a comment

Choose a reason for hiding this comment

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

Looks good to me! Just added a question.

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