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
1 change: 1 addition & 0 deletions crates/cli/src/commands/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ async fn handle_add(project_path: &ProjectLocation) -> Result<(), NetworkError>
enable_sending_blobs: Some(true),
gas_bump_blocks_every: Default::default(),
max_gas_price_multiplier: 4,
allowed_random_relayers: None,
});

project_path.overwrite_setup_config(setup_config)?;
Expand Down
1 change: 1 addition & 0 deletions crates/cli/src/commands/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ pub async fn handle_init(path: &Path) -> Result<(), InitError> {
enable_sending_blobs: Some(true),
gas_bump_blocks_every: Default::default(),
max_gas_price_multiplier: 4,
allowed_random_relayers: None,
}],
gas_providers: None,
api_config: ApiConfig {
Expand Down
23 changes: 23 additions & 0 deletions crates/core/src/app_state.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::sync::Arc;

use tokio::sync::Mutex;
Expand Down Expand Up @@ -34,6 +35,26 @@ impl RelayersInternalOnly {
}
}

pub struct RelayersAllowedForRandom {
values: HashMap<ChainId, Vec<EvmAddress>>,
}

impl RelayersAllowedForRandom {
pub fn new(values: HashMap<ChainId, Vec<EvmAddress>>) -> Self {
Self { values }
}

pub fn is_allowed(&self, relayer: &EvmAddress, chain_id: &ChainId) -> bool {
if let Some(allowed_relayers) = self.values.get(chain_id) {
// If the list is empty, it means all relayers are allowed (equivalent to "*")
allowed_relayers.is_empty() || allowed_relayers.contains(relayer)
} else {
// If no configuration exists for this chain, the feature is disabled
false
}
}
}

pub struct AppState {
/// Database client with connection pooling
pub db: Arc<PostgresClient>,
Expand All @@ -59,6 +80,8 @@ pub struct AppState {
pub safe_proxy_manager: Arc<SafeProxyManager>,
/// Any relayers which can only be called by internal logic
pub relayer_internal_only: Arc<RelayersInternalOnly>,
/// Relayers allowed for random selection per network
pub relayers_allowed_for_random: Arc<RelayersAllowedForRandom>,
/// Hold all networks permissions
pub network_permissions: Arc<Vec<(ChainId, Vec<NetworkPermissionsConfig>)>>,
/// The API keys mapped to be able to be used
Expand Down
25 changes: 23 additions & 2 deletions crates/core/src/startup.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use crate::app_state::RelayersInternalOnly;
use crate::app_state::{RelayersAllowedForRandom, RelayersInternalOnly};
use crate::authentication::{create_basic_auth_routes, inject_basic_auth_status};
use crate::background_tasks::run_background_tasks;
use crate::common_types::EvmAddress;
use crate::gas::{BlobGasOracleCache, GasOracleCache};
use crate::network::{create_network_routes, ChainId};
use crate::shared::HttpError;
use crate::webhooks::WebhookManager;
use crate::yaml::{ApiKey, NetworkPermissionsConfig, ReadYamlError};
use crate::yaml::{AllOrOneOrManyAddresses, ApiKey, NetworkPermissionsConfig, ReadYamlError};
use crate::{
app_state::AppState,
postgres::{PostgresClient, PostgresConnectionError, PostgresError},
Expand Down Expand Up @@ -37,6 +37,7 @@ use axum::{
Json, Router,
};
use dotenv::dotenv;
use std::collections::HashMap;
use std::path::Path;
use std::{net::SocketAddr, sync::Arc, time::Instant};
use thiserror::Error;
Expand Down Expand Up @@ -165,6 +166,7 @@ async fn start_api(
db: Arc<PostgresClient>,
safe_proxy_manager: Arc<SafeProxyManager>,
relayer_internal_only: RelayersInternalOnly,
relayers_allowed_for_random: RelayersAllowedForRandom,
config: &SetupConfig,
) -> Result<(), StartApiError> {
// Calculate which networks are configured with only private keys
Expand Down Expand Up @@ -212,6 +214,7 @@ async fn start_api(
relayer_creation_mutex: Arc::new(Mutex::new(())),
safe_proxy_manager,
relayer_internal_only: Arc::new(relayer_internal_only),
relayers_allowed_for_random: Arc::new(relayers_allowed_for_random),
network_permissions: Arc::new(network_permissions),
api_keys: Arc::new(api_keys),
network_configs: Arc::new(config.networks.clone()),
Expand Down Expand Up @@ -346,11 +349,27 @@ pub async fn start(project_path: &Path) -> Result<(), StartError> {

let mut safe_configs: Vec<SafeProxyConfig> = vec![];
let mut relayer_internal_only: Vec<(ChainId, EvmAddress)> = vec![];
let mut relayers_allowed_for_random: HashMap<ChainId, Vec<EvmAddress>> = HashMap::new();
let mut network_permissions: Vec<(ChainId, Vec<NetworkPermissionsConfig>)> = vec![];
let mut api_keys: Vec<(ChainId, Vec<ApiKey>)> = vec![];
for network_config in &config.networks {
api_keys
.push((network_config.chain_id, network_config.api_keys.clone().unwrap_or_default()));

if let Some(allowed_random) = &network_config.allowed_random_relayers {
let allowed_addresses = match allowed_random {
AllOrOneOrManyAddresses::All => {
// Empty vector means all relayers are allowed
vec![]
}
AllOrOneOrManyAddresses::One(address) => {
vec![*address]
}
AllOrOneOrManyAddresses::Many(addresses) => addresses.clone(),
};
relayers_allowed_for_random.insert(network_config.chain_id, allowed_addresses);
}

if let Some(automatic_top_up_configs) = &network_config.automatic_top_up {
for automatic_top_up in automatic_top_up_configs {
if let Some(safe_address) = &automatic_top_up.from.safe {
Expand All @@ -374,6 +393,7 @@ pub async fn start(project_path: &Path) -> Result<(), StartError> {

let safe_proxy_manager = Arc::new(SafeProxyManager::new(safe_configs));
let relayer_internal_only = RelayersInternalOnly::new(relayer_internal_only);
let relayers_allowed_for_random = RelayersAllowedForRandom::new(relayers_allowed_for_random);

let transaction_queue = startup_transactions_queues(
gas_oracle_cache.clone(),
Expand Down Expand Up @@ -425,6 +445,7 @@ pub async fn start(project_path: &Path) -> Result<(), StartError> {
postgres_client,
safe_proxy_manager,
relayer_internal_only,
relayers_allowed_for_random,
&config,
)
.await?;
Expand Down
7 changes: 6 additions & 1 deletion crates/core/src/transaction/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod get_transactions_pending_count;
mod replace_transaction;
mod send_transaction;
pub use send_transaction::{RelayTransactionRequest, SendTransactionResult};
mod send_random_transaction;
mod types;
pub use types::TransactionSpeed;

Expand All @@ -26,7 +27,11 @@ pub fn create_transactions_routes() -> Router<Arc<AppState>> {
Router::new()
.route("/:id", get(get_transaction_by_id::get_transaction_by_id_api))
.route("/status/:id", get(get_transaction_status::get_transaction_status))
.route("/relayers/:relayer_id/send", post(send_transaction::send_transaction))
.route("/relayers/:relayer_id/send", post(send_transaction::handle_send_transaction))
.route(
"/relayers/:chain_id/send-random",
post(send_random_transaction::send_transaction_random),
)
.route("/replace/:transaction_id", put(replace_transaction::replace_transaction))
.route("/cancel/:transaction_id", put(cancel_transaction::cancel_transaction))
.route("/relayers/:relayer_id", get(get_relayer_transactions::get_relayer_transactions))
Expand Down
63 changes: 63 additions & 0 deletions crates/core/src/transaction/api/send_random_transaction.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use crate::app_state::AppState;
use crate::network::ChainId;
use crate::relayer::Relayer;
use crate::shared::{bad_request, not_found, HttpError};
use crate::transaction::api::send_transaction::send_transaction;
use crate::transaction::api::{RelayTransactionRequest, SendTransactionResult};
use axum::{
extract::{Path, State},
http::HeaderMap,
Json,
};
use rand::seq::SliceRandom;
use std::sync::Arc;

/// Handles random relayer selection for transaction requests
/// across multiple relayers on the same chain.
///
/// This endpoint selects a random available (non-paused, non-internal) relayer
/// and forwards the transaction request to it.
pub async fn send_transaction_random(
State(state): State<Arc<AppState>>,
Path(chain_id): Path<ChainId>,
headers: HeaderMap,
Json(transaction): Json<RelayTransactionRequest>,
) -> Result<Json<SendTransactionResult>, HttpError> {
state.validate_allowed_passed_basic_auth(&headers)?;
let relayer = select_random_relayer(&state, &chain_id).await?;
let result = send_transaction(relayer, transaction, &state, &headers).await?;
Ok(Json(result))
}

/// Selects a random available relayer for the specified chain.
///
/// Filters out paused, internal-only, and relayers only allowed for random selection.
/// Note: The random relayer feature must be explicitly enabled via `allowed_random_relayers`
/// config for the network, otherwise all relayers will be filtered out.
async fn select_random_relayer(
state: &Arc<AppState>,
chain_id: &ChainId,
) -> Result<Relayer, HttpError> {
let relayers = state.db.get_all_relayers_for_chain(chain_id).await?;

if relayers.is_empty() {
return Err(not_found(format!("No relayers found for chain {}", chain_id)));
}

let mut rng = rand::thread_rng();
// TODO: it should be smart enough to also only pick the one with enough native funds to send the tx
let available_relayers: Vec<_> = relayers
.into_iter()
.filter(|r| {
!r.paused
&& !state.relayer_internal_only.restricted(&r.address, &r.chain_id)
&& state.relayers_allowed_for_random.is_allowed(&r.address, &r.chain_id)
})
.collect();
available_relayers.choose(&mut rng).cloned().ok_or_else(|| {
bad_request(format!(
"No available relayers for chain {} (all relayers are paused, internal-only, or not allowed for random selection)",
chain_id
))
})
}
27 changes: 19 additions & 8 deletions crates/core/src/transaction/api/send_transaction.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::types::TransactionSpeed;
use crate::rate_limiting::RateLimiter;
use crate::relayer::get_relayer;
use crate::relayer::{get_relayer, Relayer};
use crate::shared::utils::convert_blob_strings_to_blobs;
use crate::shared::{internal_server_error, not_found, unauthorized, HttpError};
use crate::{
Expand Down Expand Up @@ -52,7 +52,7 @@ pub struct SendTransactionResult {
}

/// API endpoint to send a new transaction through a relayer.
pub async fn send_transaction(
pub async fn handle_send_transaction(
State(state): State<Arc<AppState>>,
Path(relayer_id): Path<RelayerId>,
headers: HeaderMap,
Expand All @@ -64,7 +64,18 @@ pub async fn send_transaction(
.await?
.ok_or(not_found("Relayer does not exist".to_string()))?;

state.validate_auth_basic_or_api_key(&headers, &relayer.address, &relayer.chain_id)?;
let result = send_transaction(relayer, transaction, &state, &headers).await?;

Ok(Json(result))
}

pub async fn send_transaction(
relayer: Relayer,
transaction: RelayTransactionRequest,
state: &Arc<AppState>,
headers: &HeaderMap,
) -> Result<SendTransactionResult, HttpError> {
state.validate_auth_basic_or_api_key(headers, &relayer.address, &relayer.chain_id)?;

if state.relayer_internal_only.restricted(&relayer.address, &relayer.chain_id) {
return Err(unauthorized(Some("Relayer can only be used internally".to_string())));
Expand Down Expand Up @@ -93,9 +104,9 @@ pub async fn send_transaction(
}

let rate_limit_reservation = RateLimiter::check_and_reserve_rate_limit(
&state,
&headers,
&relayer_id,
state,
headers,
&relayer.id,
RateLimitOperation::Transaction,
)
.await?;
Expand All @@ -113,7 +124,7 @@ pub async fn send_transaction(
.transactions_queues
.lock()
.await
.add_transaction(&relayer_id, &transaction_to_send)
.add_transaction(&relayer.id, &transaction_to_send)
.await?;

let result = SendTransactionResult {
Expand All @@ -127,5 +138,5 @@ pub async fn send_transaction(
reservation.commit();
}

Ok(Json(result))
Ok(result)
}
2 changes: 2 additions & 0 deletions crates/core/src/yaml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,8 @@ pub struct NetworkSetupConfig {
pub gas_bump_blocks_every: GasBumpBlockConfig,
#[serde(default = "default_max_gas_price_multiplier")]
pub max_gas_price_multiplier: u64,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub allowed_random_relayers: Option<AllOrOneOrManyAddresses>,
}

impl From<NetworkSetupConfig> for Network {
Expand Down
6 changes: 6 additions & 0 deletions crates/e2e-tests/src/tests/transactions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod send_blob;
mod send_contract_interaction;
mod send_eth;
mod send_eth_legacy;
mod send_random_fails;
mod status;
mod validation;

Expand Down Expand Up @@ -140,6 +141,11 @@ impl TestModule for TransactionTests {
"Transaction validation - balance edge cases",
|runner| Box::pin(runner.transaction_validation_balance_edge_cases()),
),
TestDefinition::new(
"send_random_fails",
"Transaction should not send random when not enabled - send random fails",
|runner| Box::pin(runner.send_random_fails()),
),
]
}
}
42 changes: 42 additions & 0 deletions crates/e2e-tests/src/tests/transactions/send_random_fails.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use crate::tests::test_runner::TestRunner;
use anyhow::anyhow;
use rrelayer_core::transaction::api::{RelayTransactionRequest, TransactionSpeed};
use rrelayer_core::transaction::types::TransactionData;
use tracing::info;

impl TestRunner {
/// run single with:
/// RRELAYER_PROVIDERS="raw" make run-test-debug TEST=send_random_fails
/// RRELAYER_PROVIDERS="privy" make run-test-debug TEST=send_random_fails
/// RRELAYER_PROVIDERS="aws_secret_manager" make run-test-debug TEST=send_random_fails
/// RRELAYER_PROVIDERS="aws_kms" make run-test-debug TEST=send_random_fails
/// RRELAYER_PROVIDERS="gcp_secret_manager" make run-test-debug TEST=send_random_fails
/// RRELAYER_PROVIDERS="turnkey" make run-test-debug TEST=send_random_fails
/// RRELAYER_PROVIDERS="pkcs11" make run-test-debug TEST=send_random_fails
pub async fn send_random_fails(&self) -> anyhow::Result<()> {
info!("Testing simple eth transfer...");

let relayer = self.create_and_fund_relayer("send-random-fails").await?;
info!("Created relayer: {:?}", relayer);

let tx_request = RelayTransactionRequest {
to: self.config.anvil_accounts[1],
value: alloy::primitives::utils::parse_ether("0.1")?.into(),
data: TransactionData::empty(),
speed: Some(TransactionSpeed::FAST),
external_id: Some("send-random-fails".to_string()),
blobs: None,
};

let relayer_client = self
.relayer_client
.client
.transaction()
.send_random(self.config.chain_id, &tx_request, None)
.await;
match relayer_client {
Err(_) => Ok(()),
Ok(_) => Err(anyhow!("Should not send random tx")),
}
}
}
Loading