Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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.7%", "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