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": "85.8%", "color": "green"}
13 changes: 0 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ FROM rust:1.80 as builder

WORKDIR /usr/src/app
COPY . .
RUN cargo build --release --bin kora-rpc
RUN cargo build --release --bin kora

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/src/app/target/release/kora-rpc /usr/local/bin/
COPY --from=builder /usr/src/app/target/release/kora /usr/local/bin/

EXPOSE 8080
CMD ["kora-rpc"]
CMD ["kora"]
1 change: 0 additions & 1 deletion crates/lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,3 @@ tempfile = "3.2"
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
mockito = "1.2.0"
serial_test = "3.2.0"
redis-test = "0.12.0"
4 changes: 2 additions & 2 deletions crates/lib/src/admin/token_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ use {crate::cache::CacheUtil, crate::state::get_config};

#[cfg(test)]
use {
crate::config::SplTokenConfig, crate::tests::config_mock::mock_state::get_config,
crate::tests::redis_cache_mock::MockCacheUtil as CacheUtil,
crate::config::SplTokenConfig, crate::tests::cache_mock::MockCacheUtil as CacheUtil,
crate::tests::config_mock::mock_state::get_config,
};

/*
Expand Down
64 changes: 64 additions & 0 deletions crates/lib/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::{
DEFAULT_CACHE_ACCOUNT_TTL, DEFAULT_CACHE_DEFAULT_TTL,
DEFAULT_FEE_PAYER_BALANCE_METRICS_EXPIRY_SECONDS, DEFAULT_MAX_TIMESTAMP_AGE,
DEFAULT_METRICS_ENDPOINT, DEFAULT_METRICS_PORT, DEFAULT_METRICS_SCRAPE_INTERVAL,
DEFAULT_USAGE_LIMIT_FALLBACK_IF_UNAVAILABLE, DEFAULT_USAGE_LIMIT_MAX_TRANSACTIONS,
},
error::KoraError,
fee::price::{PriceConfig, PriceModel},
Expand Down Expand Up @@ -349,6 +350,8 @@ pub struct KoraConfig {
pub payment_address: Option<String>,
#[serde(default)]
pub cache: CacheConfig,
#[serde(default)]
pub usage_limit: UsageLimitConfig,
}

impl Default for KoraConfig {
Expand All @@ -359,6 +362,30 @@ impl Default for KoraConfig {
auth: AuthConfig::default(),
payment_address: None,
cache: CacheConfig::default(),
usage_limit: UsageLimitConfig::default(),
}
}
}

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct UsageLimitConfig {
/// Enable per-wallet usage limiting
pub enabled: bool,
/// Cache URL for shared usage limiting across multiple Kora instances
pub cache_url: Option<String>,
/// Default maximum transactions per wallet (0 = unlimited)
pub max_transactions: u64,
/// Fallback behavior when cache is unavailable
pub fallback_if_unavailable: bool,
}

impl Default for UsageLimitConfig {
fn default() -> Self {
Self {
enabled: false,
cache_url: None,
max_transactions: DEFAULT_USAGE_LIMIT_MAX_TRANSACTIONS,
fallback_if_unavailable: DEFAULT_USAGE_LIMIT_FALLBACK_IF_UNAVAILABLE,
}
}
}
Expand Down Expand Up @@ -671,4 +698,41 @@ mod tests {
assert_eq!(config.kora.cache.default_ttl, 300);
assert_eq!(config.kora.cache.account_ttl, 60);
}

#[test]
fn test_usage_limit_config_parsing() {
let config = ConfigBuilder::new()
.with_usage_limit_config(true, Some("redis://localhost:6379"), 10, false)
.build_config()
.unwrap();

assert!(config.kora.usage_limit.enabled);
assert_eq!(config.kora.usage_limit.cache_url, Some("redis://localhost:6379".to_string()));
assert_eq!(config.kora.usage_limit.max_transactions, 10);
assert!(!config.kora.usage_limit.fallback_if_unavailable);
}

#[test]
fn test_usage_limit_config_default() {
let config = ConfigBuilder::new().build_config().unwrap();

assert!(!config.kora.usage_limit.enabled);
assert_eq!(config.kora.usage_limit.cache_url, None);
assert_eq!(config.kora.usage_limit.max_transactions, DEFAULT_USAGE_LIMIT_MAX_TRANSACTIONS);
assert_eq!(
config.kora.usage_limit.fallback_if_unavailable,
DEFAULT_USAGE_LIMIT_FALLBACK_IF_UNAVAILABLE
);
}

#[test]
fn test_usage_limit_config_unlimited() {
let config = ConfigBuilder::new()
.with_usage_limit_config(true, None, 0, true)
.build_config()
.unwrap();

assert!(config.kora.usage_limit.enabled);
assert_eq!(config.kora.usage_limit.max_transactions, 0); // 0 = unlimited
}
}
3 changes: 3 additions & 0 deletions crates/lib/src/constant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ pub const DEFAULT_CACHE_DEFAULT_TTL: u64 = 300; // 5 minutes
pub const DEFAULT_CACHE_ACCOUNT_TTL: u64 = 60; // 1 minute for account data
pub const DEFAULT_FEE_PAYER_BALANCE_METRICS_EXPIRY_SECONDS: u64 = 30; // 30 seconds

pub const DEFAULT_USAGE_LIMIT_MAX_TRANSACTIONS: u64 = 0; // 0 = unlimited
pub const DEFAULT_USAGE_LIMIT_FALLBACK_IF_UNAVAILABLE: bool = false;

// Account Indexes within instructions
// Instruction indexes for the instructions that we support to parse from the transaction
pub mod instruction_indexes {
Expand Down
3 changes: 3 additions & 0 deletions crates/lib/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ pub enum KoraError {

#[error("Rate limit exceeded")]
RateLimitExceeded,

#[error("Usage limit exceeded: {0}")]
UsageLimitExceeded(String),
}

impl From<ClientError> for KoraError {
Expand Down
4 changes: 1 addition & 3 deletions crates/lib/src/fee/fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ use solana_message::Message;
use {crate::cache::CacheUtil, crate::state::get_config};

#[cfg(test)]
use crate::tests::{
config_mock::mock_state::get_config, redis_cache_mock::MockCacheUtil as CacheUtil,
};
use crate::tests::{cache_mock::MockCacheUtil as CacheUtil, config_mock::mock_state::get_config};
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_message::VersionedMessage;
use solana_program::program_pack::Pack;
Expand Down
1 change: 1 addition & 0 deletions crates/lib/src/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod signer;
pub mod state;
pub mod token;
pub mod transaction;
pub mod usage_limit;
pub mod validator;
pub use cache::CacheUtil;
pub use config::Config;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ use crate::{
error::KoraError,
fee::fee::FeeConfigUtil,
rpc_server::middleware_utils::default_sig_verify,
state::{get_config, get_request_signer_with_signer_key},
state::get_request_signer_with_signer_key,
transaction::{TransactionUtil, VersionedTransactionResolved},
};

use serde::{Deserialize, Serialize};
use solana_client::nonblocking::rpc_client::RpcClient;

#[cfg(not(test))]
use crate::state::get_config;

#[cfg(test)]
use crate::tests::config_mock::mock_state::get_config;

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct EstimateTransactionFeeRequest {
pub transaction: String, // Base64 encoded serialized transaction
Expand Down
12 changes: 10 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,4 +1,4 @@
use crate::rpc_server::middleware_utils::default_sig_verify;
use crate::{rpc_server::middleware_utils::default_sig_verify, usage_limit::UsageTracker};
use serde::{Deserialize, Serialize};
use solana_client::nonblocking::rpc_client::RpcClient;
use std::sync::Arc;
Expand Down Expand Up @@ -34,6 +34,10 @@ pub async fn sign_and_send_transaction(
request: SignAndSendTransactionRequest,
) -> Result<SignAndSendTransactionResponse, KoraError> {
let transaction = TransactionUtil::decode_b64_transaction(&request.transaction)?;

// Check usage limit for transaction sender
UsageTracker::check_transaction_usage_limit(&transaction).await?;

let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;

let mut resolved_transaction = VersionedTransactionResolved::from_transaction(
Expand All @@ -57,7 +61,7 @@ pub async fn sign_and_send_transaction(
mod tests {
use super::*;
use crate::tests::{
common::{setup_or_get_test_signer, RpcMockBuilder},
common::{setup_or_get_test_signer, setup_or_get_test_usage_limiter, RpcMockBuilder},
config_mock::ConfigMockBuilder,
transaction_mock::create_mock_encoded_transaction,
};
Expand All @@ -67,6 +71,8 @@ mod tests {
let _m = ConfigMockBuilder::new().build_and_setup();
let _ = setup_or_get_test_signer();

let _ = setup_or_get_test_usage_limiter().await;

let rpc_client = Arc::new(RpcMockBuilder::new().build());

let request = SignAndSendTransactionRequest {
Expand All @@ -85,6 +91,8 @@ mod tests {
let _m = ConfigMockBuilder::new().build_and_setup();
let _ = setup_or_get_test_signer();

let _ = setup_or_get_test_usage_limiter().await;

let rpc_client = Arc::new(RpcMockBuilder::new().build());

let request = SignAndSendTransactionRequest {
Expand Down
11 changes: 10 additions & 1 deletion crates/lib/src/rpc_server/method/sign_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::{
rpc_server::middleware_utils::default_sig_verify,
state::get_request_signer_with_signer_key,
transaction::{TransactionUtil, VersionedTransactionOps, VersionedTransactionResolved},
usage_limit::UsageTracker,
KoraError,
};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -33,6 +34,10 @@ pub async fn sign_transaction(
request: SignTransactionRequest,
) -> Result<SignTransactionResponse, KoraError> {
let transaction = TransactionUtil::decode_b64_transaction(&request.transaction)?;

// Check usage limit for transaction sender
UsageTracker::check_transaction_usage_limit(&transaction).await?;

let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;

let mut resolved_transaction = VersionedTransactionResolved::from_transaction(
Expand All @@ -58,7 +63,7 @@ pub async fn sign_transaction(
mod tests {
use super::*;
use crate::tests::{
common::{setup_or_get_test_signer, RpcMockBuilder},
common::{setup_or_get_test_signer, setup_or_get_test_usage_limiter, RpcMockBuilder},
config_mock::ConfigMockBuilder,
transaction_mock::create_mock_encoded_transaction,
};
Expand All @@ -68,6 +73,8 @@ mod tests {
let _m = ConfigMockBuilder::new().build_and_setup();
let _ = setup_or_get_test_signer();

let _ = setup_or_get_test_usage_limiter().await;

let rpc_client = Arc::new(RpcMockBuilder::new().build());

let request = SignTransactionRequest {
Expand All @@ -86,6 +93,8 @@ mod tests {
let _m = ConfigMockBuilder::new().build_and_setup();
let _ = setup_or_get_test_signer();

let _ = setup_or_get_test_usage_limiter().await;

let rpc_client = Arc::new(RpcMockBuilder::new().build());

let request = SignTransactionRequest {
Expand Down
11 changes: 10 additions & 1 deletion crates/lib/src/rpc_server/method/sign_transaction_if_paid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::{
rpc_server::middleware_utils::default_sig_verify,
state::get_request_signer_with_signer_key,
transaction::{TransactionUtil, VersionedTransactionOps, VersionedTransactionResolved},
usage_limit::UsageTracker,
KoraError,
};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -33,6 +34,10 @@ pub async fn sign_transaction_if_paid(
request: SignTransactionIfPaidRequest,
) -> Result<SignTransactionIfPaidResponse, KoraError> {
let transaction_requested = TransactionUtil::decode_b64_transaction(&request.transaction)?;

// Check usage limit for transaction sender
UsageTracker::check_transaction_usage_limit(&transaction_requested).await?;

let signer = get_request_signer_with_signer_key(request.signer_key.as_deref())?;

let mut resolved_transaction = VersionedTransactionResolved::from_transaction(
Expand All @@ -58,7 +63,7 @@ pub async fn sign_transaction_if_paid(
mod tests {
use super::*;
use crate::tests::{
common::{setup_or_get_test_signer, RpcMockBuilder},
common::{setup_or_get_test_signer, setup_or_get_test_usage_limiter, RpcMockBuilder},
config_mock::ConfigMockBuilder,
transaction_mock::create_mock_encoded_transaction,
};
Expand All @@ -68,6 +73,8 @@ mod tests {
let _m = ConfigMockBuilder::new().build_and_setup();
let _ = setup_or_get_test_signer();

let _ = setup_or_get_test_usage_limiter().await;

let rpc_client = Arc::new(RpcMockBuilder::new().build());

let request = SignTransactionIfPaidRequest {
Expand All @@ -86,6 +93,8 @@ mod tests {
let _m = ConfigMockBuilder::new().build_and_setup();
let _ = setup_or_get_test_signer();

let _ = setup_or_get_test_usage_limiter().await;

let rpc_client = Arc::new(RpcMockBuilder::new().build());

let request = SignTransactionIfPaidRequest {
Expand Down
17 changes: 12 additions & 5 deletions crates/lib/src/rpc_server/method/transfer_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,18 @@ pub async fn transfer_transaction(
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::common::{
setup_or_get_test_config, setup_or_get_test_signer, RpcMockBuilder,
use crate::{
state::update_config,
tests::{
common::{setup_or_get_test_signer, RpcMockBuilder},
config_mock::ConfigMockBuilder,
},
};

#[tokio::test]
async fn test_transfer_transaction_invalid_source() {
let _ = setup_or_get_test_config();
let config = ConfigMockBuilder::new().build();
let _ = update_config(config);
let _ = setup_or_get_test_signer();

let rpc_client = Arc::new(RpcMockBuilder::new().build());
Expand Down Expand Up @@ -182,7 +187,8 @@ mod tests {

#[tokio::test]
async fn test_transfer_transaction_invalid_destination() {
let _ = setup_or_get_test_config();
let config = ConfigMockBuilder::new().build();
let _ = update_config(config);
let _ = setup_or_get_test_signer();

let rpc_client = Arc::new(RpcMockBuilder::new().build());
Expand All @@ -209,7 +215,8 @@ mod tests {

#[tokio::test]
async fn test_transfer_transaction_invalid_token() {
let _ = setup_or_get_test_config();
let config = ConfigMockBuilder::new().build();
let _ = update_config(config);
let _ = setup_or_get_test_signer();

let rpc_client = Arc::new(RpcMockBuilder::new().build());
Expand Down
Loading