Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/badges/coverage.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"schemaVersion": 1, "label": "coverage", "message": "86.3%", "color": "green"}
{"schemaVersion": 1, "label": "coverage", "message": "86.2%", "color": "green"}
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ jobs:
retention-days: 30

- name: Update PR description with coverage badge
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
uses: actions/github-script@v7
with:
script: |
Expand Down
15 changes: 13 additions & 2 deletions crates/lib/src/rpc_server/method/estimate_transaction_fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use utoipa::ToSchema;
use crate::{
error::KoraError,
fee::fee::FeeConfigUtil,
rpc_server::middleware_utils::default_sig_verify,
state::{get_config, get_request_signer_with_signer_key},
token::token::TokenUtil,
transaction::{TransactionUtil, VersionedTransactionResolved},
Expand All @@ -22,6 +23,9 @@ pub struct EstimateTransactionFeeRequest {
/// Optional signer signer_key to ensure consistency across related RPC calls
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signer_key: Option<String>,
/// Whether to verify signatures during simulation (defaults to true)
#[serde(default = "default_sig_verify")]
pub sig_verify: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
Expand All @@ -47,8 +51,12 @@ pub async fn estimate_transaction_fee(
let validation_config = &config.validation;
let fee_payer = signer.solana_pubkey();

let mut resolved_transaction =
VersionedTransactionResolved::from_transaction(&transaction, rpc_client).await?;
let mut resolved_transaction = VersionedTransactionResolved::from_transaction(
&transaction,
rpc_client,
request.sig_verify,
)
.await?;

let fee_in_lamports = FeeConfigUtil::estimate_transaction_fee(
rpc_client,
Expand Down Expand Up @@ -108,6 +116,7 @@ mod tests {
transaction: "invalid_base64!@#$".to_string(),
fee_token: None,
signer_key: None,
sig_verify: true,
};

let result = estimate_transaction_fee(&rpc_client, request).await;
Expand All @@ -126,6 +135,7 @@ mod tests {
transaction: create_mock_encoded_transaction(),
fee_token: None,
signer_key: Some("invalid_pubkey".to_string()),
sig_verify: true,
};

let result = estimate_transaction_fee(&rpc_client, request).await;
Expand All @@ -146,6 +156,7 @@ mod tests {
transaction: create_mock_encoded_transaction(),
fee_token: Some("invalid_mint_address".to_string()),
signer_key: None,
sig_verify: true,
};

let result = estimate_transaction_fee(&rpc_client, request).await;
Expand Down
14 changes: 12 additions & 2 deletions crates/lib/src/rpc_server/method/sign_and_send_transaction.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::rpc_server::middleware_utils::default_sig_verify;
use serde::{Deserialize, Serialize};
use solana_client::nonblocking::rpc_client::RpcClient;
use std::sync::Arc;
Expand All @@ -15,6 +16,9 @@ pub struct SignAndSendTransactionRequest {
/// Optional signer signer_key to ensure consistency across related RPC calls
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signer_key: Option<String>,
/// Whether to verify signatures during simulation (defaults to true)
#[serde(default = "default_sig_verify")]
pub sig_verify: bool,
}

#[derive(Debug, Serialize, ToSchema)]
Expand All @@ -32,8 +36,12 @@ pub async fn sign_and_send_transaction(
let transaction = TransactionUtil::decode_b64_transaction(&request.transaction)?;
let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;

let mut resolved_transaction =
VersionedTransactionResolved::from_transaction(&transaction, rpc_client).await?;
let mut resolved_transaction = VersionedTransactionResolved::from_transaction(
&transaction,
rpc_client,
request.sig_verify,
)
.await?;

let (signature, signed_transaction) =
resolved_transaction.sign_and_send_transaction(&signer, rpc_client).await?;
Expand Down Expand Up @@ -64,6 +72,7 @@ mod tests {
let request = SignAndSendTransactionRequest {
transaction: "invalid_base64!@#$".to_string(),
signer_key: None,
sig_verify: true,
};

let result = sign_and_send_transaction(&rpc_client, request).await;
Expand All @@ -81,6 +90,7 @@ mod tests {
let request = SignAndSendTransactionRequest {
transaction: create_mock_encoded_transaction(),
signer_key: Some("invalid_pubkey".to_string()),
sig_verify: true,
};

let result = sign_and_send_transaction(&rpc_client, request).await;
Expand Down
14 changes: 12 additions & 2 deletions crates/lib/src/rpc_server/method/sign_transaction.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::{
rpc_server::middleware_utils::default_sig_verify,
state::get_request_signer_with_signer_key,
transaction::{TransactionUtil, VersionedTransactionOps, VersionedTransactionResolved},
KoraError,
Expand All @@ -14,6 +15,9 @@ pub struct SignTransactionRequest {
/// Optional signer signer_key to ensure consistency across related RPC calls
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signer_key: Option<String>,
/// Whether to verify signatures during simulation (defaults to true)
#[serde(default = "default_sig_verify")]
pub sig_verify: bool,
}

#[derive(Debug, Serialize, ToSchema)]
Expand All @@ -31,8 +35,12 @@ pub async fn sign_transaction(
let transaction = TransactionUtil::decode_b64_transaction(&request.transaction)?;
let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;

let mut resolved_transaction =
VersionedTransactionResolved::from_transaction(&transaction, rpc_client).await?;
let mut resolved_transaction = VersionedTransactionResolved::from_transaction(
&transaction,
rpc_client,
request.sig_verify,
)
.await?;

let (signed_transaction, _) =
resolved_transaction.sign_transaction(&signer, rpc_client).await?;
Expand Down Expand Up @@ -65,6 +73,7 @@ mod tests {
let request = SignTransactionRequest {
transaction: "invalid_base64!@#$".to_string(),
signer_key: None,
sig_verify: true,
};

let result = sign_transaction(&rpc_client, request).await;
Expand All @@ -82,6 +91,7 @@ mod tests {
let request = SignTransactionRequest {
transaction: create_mock_encoded_transaction(),
signer_key: Some("invalid_pubkey".to_string()),
sig_verify: true,
};

let result = sign_transaction(&rpc_client, request).await;
Expand Down
14 changes: 12 additions & 2 deletions crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::{
rpc_server::middleware_utils::default_sig_verify,
state::get_request_signer_with_signer_key,
transaction::{TransactionUtil, VersionedTransactionOps, VersionedTransactionResolved},
KoraError,
Expand All @@ -14,6 +15,9 @@ pub struct SignTransactionIfPaidRequest {
/// Optional signer signer_key to ensure consistency across related RPC calls
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signer_key: Option<String>,
/// Whether to verify signatures during simulation (defaults to true)
#[serde(default = "default_sig_verify")]
pub sig_verify: bool,
}

#[derive(Debug, Serialize, ToSchema)]
Expand All @@ -31,8 +35,12 @@ pub async fn sign_transaction_if_paid(
let transaction_requested = TransactionUtil::decode_b64_transaction(&request.transaction)?;
let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;

let mut resolved_transaction =
VersionedTransactionResolved::from_transaction(&transaction_requested, rpc_client).await?;
let mut resolved_transaction = VersionedTransactionResolved::from_transaction(
&transaction_requested,
rpc_client,
request.sig_verify,
)
.await?;

let (transaction, signed_transaction) = resolved_transaction
.sign_transaction_if_paid(&signer, rpc_client)
Expand Down Expand Up @@ -65,6 +73,7 @@ mod tests {
let request = SignTransactionIfPaidRequest {
transaction: "invalid_base64!@#$".to_string(),
signer_key: None,
sig_verify: true,
};

let result = sign_transaction_if_paid(&rpc_client, request).await;
Expand All @@ -82,6 +91,7 @@ mod tests {
let request = SignTransactionIfPaidRequest {
transaction: create_mock_encoded_transaction(),
signer_key: Some("invalid_pubkey".to_string()),
sig_verify: true,
};

let result = sign_transaction_if_paid(&rpc_client, request).await;
Expand Down
4 changes: 4 additions & 0 deletions crates/lib/src/rpc_server/middleware_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ use futures_util::TryStreamExt;
use http::Request;
use jsonrpsee::server::logger::Body;

pub fn default_sig_verify() -> bool {
false
}

pub async fn extract_parts_and_body_bytes(
request: Request<Body>,
) -> (http::request::Parts, Vec<u8>) {
Expand Down
86 changes: 75 additions & 11 deletions crates/lib/src/transaction/versioned_transaction.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use async_trait::async_trait;
use base64::{engine::general_purpose::STANDARD, Engine as _};
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_client::{nonblocking::rpc_client::RpcClient, rpc_config::RpcSimulateTransactionConfig};
use solana_commitment_config::CommitmentConfig;
use solana_message::{v0::MessageAddressTableLookup, VersionedMessage};
use solana_sdk::{
Expand Down Expand Up @@ -79,6 +79,7 @@ impl VersionedTransactionResolved {
pub async fn from_transaction(
transaction: &VersionedTransaction,
rpc_client: &RpcClient,
sig_verify: bool,
) -> Result<Self, KoraError> {
let mut resolved = Self {
transaction: transaction.clone(),
Expand Down Expand Up @@ -113,7 +114,7 @@ impl VersionedTransactionResolved {
let outer_instructions =
IxUtils::uncompile_instructions(transaction.message.instructions(), &all_account_keys);

let inner_instructions = resolved.fetch_inner_instructions(rpc_client).await?;
let inner_instructions = resolved.fetch_inner_instructions(rpc_client, sig_verify).await?;

resolved.all_instructions.extend(outer_instructions);
resolved.all_instructions.extend(inner_instructions);
Expand All @@ -139,9 +140,17 @@ impl VersionedTransactionResolved {
async fn fetch_inner_instructions(
&mut self,
rpc_client: &RpcClient,
sig_verify: bool,
) -> Result<Vec<Instruction>, KoraError> {
let simulation_result = rpc_client
.simulate_transaction(&self.transaction)
.simulate_transaction_with_config(
&self.transaction,
RpcSimulateTransactionConfig {
commitment: Some(rpc_client.commitment()),
sig_verify,
..Default::default()
},
)
.await
.map_err(|e| KoraError::RpcError(format!("Failed to simulate transaction: {e}")))?;

Expand Down Expand Up @@ -669,9 +678,10 @@ mod tests {
);
let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();

let resolved = VersionedTransactionResolved::from_transaction(&transaction, &rpc_client)
.await
.unwrap();
let resolved =
VersionedTransactionResolved::from_transaction(&transaction, &rpc_client, true)
.await
.unwrap();

assert_eq!(resolved.transaction, transaction);
assert_eq!(resolved.all_account_keys, transaction.message.static_account_keys());
Expand Down Expand Up @@ -770,9 +780,10 @@ mod tests {

let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();

let resolved = VersionedTransactionResolved::from_transaction(&transaction, &rpc_client)
.await
.unwrap();
let resolved =
VersionedTransactionResolved::from_transaction(&transaction, &rpc_client, true)
.await
.unwrap();

assert_eq!(resolved.transaction, transaction);

Expand Down Expand Up @@ -815,7 +826,7 @@ mod tests {
let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();

let result =
VersionedTransactionResolved::from_transaction(&transaction, &rpc_client).await;
VersionedTransactionResolved::from_transaction(&transaction, &rpc_client, true).await;

// The simulation should fail, but the exact error type depends on mock implementation
// We expect either an RpcError (from mock deserialization) or InvalidTransaction (from simulation logic)
Expand Down Expand Up @@ -877,7 +888,60 @@ mod tests {
let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();

let mut resolved = VersionedTransactionResolved::from_kora_built_transaction(&transaction);
let inner_instructions = resolved.fetch_inner_instructions(&rpc_client).await.unwrap();
let inner_instructions =
resolved.fetch_inner_instructions(&rpc_client, true).await.unwrap();

assert_eq!(inner_instructions.len(), 1);
assert_eq!(inner_instructions[0].data, vec![10, 20, 30]);
}

#[tokio::test]
async fn test_fetch_inner_instructions_with_sig_verify_false() {
let config = setup_test_config();
let _m = setup_config_mock(config);

let keypair = Keypair::new();
let instruction = Instruction::new_with_bytes(
Pubkey::new_unique(),
&[1, 2, 3],
vec![AccountMeta::new(keypair.pubkey(), true)],
);
let message =
VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
let transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();

// Mock RPC client with inner instructions
let inner_instruction_data = bs58::encode(&[10, 20, 30]).into_string();
let mut mocks = HashMap::new();
mocks.insert(
RpcRequest::SimulateTransaction,
json!({
"context": { "slot": 1 },
"value": {
"err": null,
"logs": [],
"accounts": null,
"unitsConsumed": 1000,
"innerInstructions": [
{
"index": 0,
"instructions": [
{
"programIdIndex": 1,
"accounts": [0],
"data": inner_instruction_data
}
]
}
]
}
}),
);
let rpc_client = RpcMockBuilder::new().with_custom_mocks(mocks).build();

let mut resolved = VersionedTransactionResolved::from_kora_built_transaction(&transaction);
let inner_instructions =
resolved.fetch_inner_instructions(&rpc_client, false).await.unwrap();

assert_eq!(inner_instructions.len(), 1);
assert_eq!(inner_instructions[0].data, vec![10, 20, 30]);
Expand Down
7 changes: 5 additions & 2 deletions tests/src/common/assertions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ impl RpcAssertions for Value {

/// Assertions for transaction responses
pub trait TransactionAssertions {
/// Assert the transaction blockhash is valid (44 chars base58)
/// Assert the transaction blockhash is valid (43-44 chars base58)
fn assert_valid_blockhash(&self);
}

Expand All @@ -95,7 +95,10 @@ impl TransactionAssertions for Value {
.expect("Response missing blockhash field");

// Solana blockhashes are typically 44 chars in base58
assert_eq!(blockhash.len(), 44, "Invalid blockhash format: {blockhash}");
assert!(
blockhash.len() >= 43 && blockhash.len() <= 44,
"Invalid blockhash format: {blockhash}"
);
}
}

Expand Down