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

DX-272: Rust tx-sender crate #820

Merged
merged 23 commits into from
Mar 25, 2025
Merged

DX-272: Rust tx-sender crate #820

merged 23 commits into from
Mar 25, 2025

Conversation

parhim
Copy link
Contributor

@parhim parhim commented Mar 12, 2025

https://linear.app/orca-so/issue/DX-272/build-and-test-crate-with-same-specs-as-ts-package

this still needs some thoughtful review but at least it works with and without fees
jito
exact + dynamic

not as clean as typescript but thats a rust limitation
tests are doing small transfers between orca wallets (can use whatever on devnet)
but you need a locally stored mainnet-test-keypair.json (ping me on slack if you need the lightly funded orca pk)

Cargo.toml Outdated
@@ -1,7 +1,8 @@
[workspace]
resolver = "2"
members = [
"programs/*"
"programs/*",
"rust-sdk/tx-sender",
Copy link
Member

Choose a reason for hiding this comment

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

We only added programs/* to the workspace to avoid version conflicts. Each SDK has their own cargo.lock file and can thus have an independent version of shared dependencies (e.g. solana-program). Can you remove?

@@ -0,0 +1,5 @@
[workspace]
Copy link
Member

Choose a reason for hiding this comment

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

Same goes for this one ^

edition = "2021"
description = "Solana Transaction Sender for building and sending transactions with priority fees"
repository = "https://github.com/orca-so/whirlpools"
license = "Orca"
Copy link
Member

Choose a reason for hiding this comment

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

Think this should match what is in the other cargo.toml files. I think we use license-path instead

description = "Solana Transaction Sender for building and sending transactions with priority fees"
repository = "https://github.com/orca-so/whirlpools"
license = "Orca"
rust-version = "1.70"
Copy link
Member

Choose a reason for hiding this comment

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

Unless this is a limitation I wouldn't add this one

rust-version = "1.70"

[dependencies]
# Use consistent versions to avoid conflicts
Copy link
Member

Choose a reason for hiding this comment

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

Cargo is kinda annoying when it comes to dependencies. Specifying "1.17.22" here will most likely pull in v1.18.x.

On the other hand we want this lib to work with as many versions of solana-program as possible. Specifying "1.17.22" in cargo means >= 1.17.22 and <2.0.0. We might want this lib to also work with >2.0 like the other libs in the repo.


/// Chain identifier based on genesis hash
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChainId(Hash);
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to have a enum instead and implement From<Hash> for it?

}

/// RPC configuration for connecting to Solana nodes
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
Copy link
Member

Choose a reason for hiding this comment

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

Does this need Serialize and Deserialize?

}

/// Async constructor with chain ID detection
pub async fn with_chain_detection(url: impl Into<String>) -> Result<Self> {
Copy link
Member

Choose a reason for hiding this comment

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

I would just only do the async constructor. Is there a scenario where we would want this without a chain_id?

@@ -0,0 +1,70 @@
#[cfg(test)]
mod tests {
Copy link
Member

Choose a reason for hiding this comment

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

Why is this a separate file and not in-file?

use std::time::Duration;

/// Transaction configuration for sending transactions
#[derive(Debug, Clone, Serialize, Deserialize)]
Copy link
Member

Choose a reason for hiding this comment

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

Does this need Serialize and Deserialize?

};

// Create sender instance
let sender = TransactionSender::new(rpc_config, fee_config);
Copy link
Member

Choose a reason for hiding this comment

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

Is this still accurate?


let transaction = Transaction::new_unsigned(message);
// Simulate transaction
let simulation_result = rpc_client.simulate_transaction(&transaction)
Copy link
Member

Choose a reason for hiding this comment

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

Does this work with an unsigned tx? I thought you had to add a param to disable sig_verify and replace_blockhash. Also can we use VersionedTransaction here?



/// Calculate and return compute budget instructions for a transaction
pub async fn add_compute_budget_instructions(
Copy link
Member

Choose a reason for hiding this comment

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

nit: maybe get_compute_budget_instruction is more appropriate here

/// - `Option<RpcConfig>` - Must be explicitly set with `set_rpc()` before sending transactions
/// - `Option<Arc<RpcClient>>` - Created when RPC is set, for reuse across transactions
/// - `FeeConfig` - Configured with defaults but can be customized
static GLOBAL_CONFIG: OnceLock<RwLock<GlobalConfig>> = OnceLock::new();
Copy link
Member

Choose a reason for hiding this comment

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

Do we need a OnceLock here?

);

// Return unsigned transaction (signing happens at the higher level)
Ok(Transaction::new_unsigned(message))
Copy link
Member

Choose a reason for hiding this comment

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

Can we do a versioned TX?

let expiry_time = Instant::now() + Duration::from_millis(options.timeout_ms);
let mut retries = 0;

while Instant::now() < expiry_time && retries <= options.max_retries {
Copy link
Member

Choose a reason for hiding this comment

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

I am still kinda concerned about the max_retries.

  • I think a default of 3 is too low
  • After the last retry there is still a fairly decent chance that the transaction lands (but we won't pick picked up potentially because of the timeout_ms).

I think it would be safest to add the same logic here as in the TS sdk.

loop until expiry_time:
  send TX
  see if TX is confirmed
  Wait 1s


while Instant::now() < expiry_time && retries <= options.max_retries {
let send_config = RpcSendTransactionConfig {
skip_preflight: options.skip_preflight,
Copy link
Member

Choose a reason for hiding this comment

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

With client retries skip_preflight needs to be always true. Don't think it makes sense to make this configurable by the user.

impl From<Hash> for ChainId {
fn from(hash: Hash) -> Self {
// Mainnet genesis hash
let mainnet_hash = Hash::from_str(MAINNET_HASH).unwrap_or_default();
Copy link
Member

Choose a reason for hiding this comment

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

do we need to do the from_str here every time? Can we instead do hash.to_string()?

Copy link
Member

@wjthieme wjthieme left a comment

Choose a reason for hiding this comment

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

One comment about timeout and retries but other than that lgtm 🚀

Comment on lines 26 to 29
pub const DEFAULT_TRANSACTION_TIMEOUT_MS: u64 = 30_000;

/// Default number of retries
pub const DEFAULT_RETRIES: usize = 3;
Copy link
Member

Choose a reason for hiding this comment

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

Are these still being used?

return Err(format!("Transaction simulation failed: {}", err));
}

let expiry_time = Instant::now() + Duration::from_millis(options.timeout_ms);
Copy link
Member

Choose a reason for hiding this comment

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

Think the default for this should be 90 seconds instead of 30 (or just make it not configurable like on TS). The issue with lowering the timeout is that the transaction is valid for the full 90 seconds. This means that if it ends up landing on the final send this function never picks up on it. If you set the timeout to 30 seconds and it 'times-out' you can't definitively say that the transaction did not land

@parhim parhim merged commit 9ced62d into main Mar 25, 2025
6 checks passed
@parhim parhim deleted the sam/DX-272 branch March 25, 2025 16:18
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.

2 participants