Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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/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
82 changes: 71 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,13 @@ 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 { sig_verify, ..Default::default() },
)
.await
.map_err(|e| KoraError::RpcError(format!("Failed to simulate transaction: {e}")))?;

Expand Down Expand Up @@ -669,9 +674,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 +776,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 +822,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 +884,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
Loading