Skip to content

Commit 77347ee

Browse files
thlorenzDodecahedr0x
authored andcommitted
feat: kind-based multi-remote configuration (#746)
1 parent 035f812 commit 77347ee

31 files changed

+1047
-367
lines changed

Cargo.lock

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config.example.toml

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,31 +31,47 @@
3131
# Env: MBV_LIFECYCLE
3232
lifecycle = "ephemeral"
3333

34-
# Remote endpoints for syncing with the base chain.
35-
# You can specify multiple remotes of different types.
36-
# The first HTTP/HTTPS remote will be used for JSON-RPC calls.
37-
# Each WebSocket/gRPC remote creates a subscription client.
34+
# Remote connections for RPC, WebSocket, and gRPC.
35+
# You can define multiple remotes of different kinds.
36+
# The first RPC remote will be used for JSON-RPC calls.
37+
# Each WebSocket/gRPC remote will create a pubsub client.
3838
#
39-
# Supported URL schemes:
40-
# - "http", "https": JSON-RPC HTTP connections
41-
# - "ws", "wss": WebSocket connections for PubSub
42-
# - "grpc", "grpcs": gRPC connections for streaming
39+
# Available kinds:
40+
# - "rpc": JSON-RPC HTTP connection (typically port 8899)
41+
# - "websocket": WebSocket connection for PubSub (typically wss://)
42+
# - "grpc": gRPC connection for streaming (typically port 50051)
4343
#
44-
# URL Aliases (resolved during parsing):
45-
# - "mainnet": resolves to https://api.mainnet-beta.solana.com/
46-
# - "devnet": resolves to https://api.devnet.solana.com/
47-
# - "testnet": resolves to https://api.testnet.solana.com/
48-
# - "localhost": resolves to http://localhost:8899/ (only for http/https schemes)
44+
# URL Aliases (automatically resolved based on kind):
45+
# RPC aliases: "mainnet", "devnet", "local"
46+
# WebSocket aliases: "mainnet", "devnet", "local"
4947
#
50-
# Examples:
51-
# remotes = ["devnet"] # Single devnet HTTP endpoint
52-
# remotes = ["mainnet", "wss://mainnet-beta.solana.com"] # Mainnet with explicit WebSocket
53-
# remotes = ["http://localhost:8899", "ws://localhost:8900"] # Local endpoints
48+
# Example 1: Using aliases
49+
# [[remote]]
50+
# kind = "rpc"
51+
# url = "devnet"
5452
#
55-
# If no remotes are specified, defaults to ["devnet"] with an auto-added WebSocket endpoint.
56-
# Default: ["https://api.devnet.solana.com/"]
57-
# Env: Not supported (must be configured via TOML or CLI)
58-
remotes = ["devnet", "wss://devnet.solana.com", "grpcs://solana.helius.com"]
53+
# [[remote]]
54+
# kind = "websocket"
55+
# url = "devnet"
56+
#
57+
# Example 2: Using full URLs with optional API key
58+
# [[remote]]
59+
# kind = "rpc"
60+
# url = "https://api.devnet.solana.com"
61+
# api-key = "optional-key"
62+
#
63+
# [[remote]]
64+
# kind = "websocket"
65+
# url = "wss://api.devnet.solana.com"
66+
#
67+
# [[remote]]
68+
# kind = "grpc"
69+
# url = "http://grpc.example.com:50051"
70+
# api-key = "optional-key"
71+
72+
[[remote]]
73+
kind = "rpc"
74+
url = "devnet"
5975

6076
# Root directory for application storage (ledger, accountsdb, snapshots).
6177
# Default: "magicblock-test-storage" (created in current working directory)

magicblock-api/src/magic_validator.rs

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ use magicblock_config::{
3737
config::{
3838
ChainOperationConfig, LedgerConfig, LifecycleMode, LoadableProgram,
3939
},
40+
consts::DEFAULT_REMOTE,
41+
types::{resolve_url, RemoteKind},
4042
ValidatorParams,
4143
};
4244
use magicblock_core::{
@@ -340,7 +342,7 @@ impl MagicValidator {
340342
config.validator.keypair.insecure_clone(),
341343
committor_persist_path,
342344
ChainConfig {
343-
rpc_uri: config.rpc_url().to_owned(),
345+
rpc_uri: config.rpc_url_or_default(),
344346
commitment: CommitmentConfig::confirmed(),
345347
compute_budget_config: ComputeBudgetConfig::new(
346348
config.commit.compute_unit_price,
@@ -371,14 +373,22 @@ impl MagicValidator {
371373
faucet_pubkey: Pubkey,
372374
) -> ApiResult<ChainlinkImpl> {
373375
use magicblock_chainlink::remote_account_provider::Endpoint;
374-
let rpc_url = config.rpc_url().to_owned();
375-
let endpoints = config
376-
.websocket_urls()
377-
.map(|pubsub_url| Endpoint {
376+
let rpc_url = config.rpc_url_or_default();
377+
let endpoints = if config.has_subscription_url() {
378+
config
379+
.websocket_urls()
380+
.map(|pubsub_url| Endpoint {
381+
rpc_url: rpc_url.clone(),
382+
pubsub_url: pubsub_url.to_string(),
383+
})
384+
.collect::<Vec<_>>()
385+
} else {
386+
let ws_url = resolve_url(RemoteKind::Websocket, DEFAULT_REMOTE);
387+
vec![Endpoint {
378388
rpc_url: rpc_url.clone(),
379-
pubsub_url: pubsub_url.to_string(),
380-
})
381-
.collect::<Vec<_>>();
389+
pubsub_url: ws_url,
390+
}]
391+
};
382392

383393
let cloner = ChainlinkCloner::new(
384394
committor_service,
@@ -530,7 +540,7 @@ impl MagicValidator {
530540
});
531541

532542
DomainRegistryManager::handle_registration_static(
533-
self.config.rpc_url(),
543+
self.config.rpc_url_or_default(),
534544
&validator_keypair,
535545
validator_info,
536546
)
@@ -543,7 +553,7 @@ impl MagicValidator {
543553
let validator_keypair = validator_authority();
544554

545555
DomainRegistryManager::handle_unregistration_static(
546-
self.config.rpc_url(),
556+
self.config.rpc_url_or_default(),
547557
&validator_keypair,
548558
)
549559
.map_err(|err| {
@@ -557,7 +567,7 @@ impl MagicValidator {
557567
const MIN_BALANCE_SOL: u64 = 5;
558568

559569
let lamports = RpcClient::new_with_commitment(
560-
self.config.rpc_url().to_owned(),
570+
self.config.rpc_url_or_default(),
561571
CommitmentConfig::confirmed(),
562572
)
563573
.get_balance(&self.identity)
@@ -604,7 +614,7 @@ impl MagicValidator {
604614
.map(|co| co.claim_fees_frequency)
605615
{
606616
self.claim_fees_task
607-
.start(frequency, self.config.rpc_url().to_owned());
617+
.start(frequency, self.config.rpc_url_or_default());
608618
}
609619

610620
self.slot_ticker = Some(init_slot_ticker(

magicblock-config/src/config/cli.rs

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use serde::Serialize;
55

66
use crate::{
77
config::LifecycleMode,
8-
types::{network::Remote, BindAddress, SerdeKeypair},
8+
types::{
9+
remote::parse_remote_config, BindAddress, RemoteConfig, SerdeKeypair,
10+
},
911
};
1012

1113
/// CLI Arguments mirroring the structure of ValidatorParams.
@@ -16,22 +18,13 @@ pub struct CliParams {
1618
/// Path to the TOML configuration file.
1719
pub config: Option<PathBuf>,
1820

19-
/// List of remote endpoints for syncing with the base chain.
20-
/// Can be specified multiple times.
21-
///
22-
/// SUPPORTED SCHEMES: http(s), ws(s), grpc(s)
23-
///
24-
/// ALIASES: mainnet, devnet, testnet, localhost
25-
///
26-
/// EXAMPLES:
27-
/// - `--remote devnet`
28-
/// - `--remote wss://devnet.solana.com`
29-
/// - `--remote grpcs://grpc.example.com`
30-
///
31-
/// DEFAULT: devnet (HTTP endpoint with auto-added WS endpoint)
32-
#[arg(long)]
33-
#[serde(skip_serializing_if = "Option::is_none")]
34-
pub remotes: Option<Vec<Remote>>,
21+
/// Remote Solana cluster connections. Can be specified multiple times.
22+
/// Format: --remote <kind>:<url> or --remote <kind>:<alias>
23+
/// Examples: --remote rpc:devnet --remote websocket:devnet --remote grpc:http://localhost:50051
24+
/// Aliases: mainnet, devnet, local (resolved based on kind)
25+
#[arg(long, short, value_parser = parse_remote_config)]
26+
#[serde(skip_serializing_if = "Vec::is_empty", default)]
27+
pub remote: Vec<RemoteConfig>,
3528

3629
/// The application's operational mode.
3730
#[arg(long)]

magicblock-config/src/consts.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,15 @@ pub const DEFAULT_BASE_FEE: u64 = 0;
2020
/// Default compute unit price in microlamports
2121
pub const DEFAULT_COMPUTE_UNIT_PRICE: u64 = 1_000_000;
2222

23-
/// Remote URL Aliases - Mainnet, Testnet, Devnet, and Localhost
24-
/// Solana mainnet-beta RPC endpoint
25-
pub const MAINNET_URL: &str = "https://api.mainnet-beta.solana.com/";
23+
// Remote URL Aliases - RPC
24+
pub const RPC_MAINNET: &str = "https://api.mainnet-beta.solana.com/";
25+
pub const RPC_DEVNET: &str = "https://api.devnet.solana.com/";
26+
pub const RPC_LOCAL: &str = "http://localhost:8899/";
27+
28+
// Remote URL Aliases - WebSocket
29+
pub const WS_MAINNET: &str = "wss://api.mainnet-beta.solana.com/";
30+
pub const WS_DEVNET: &str = "wss://api.devnet.solana.com/";
31+
pub const WS_LOCAL: &str = "ws://localhost:8899/";
2632

2733
/// Solana testnet RPC endpoint
2834
pub const TESTNET_URL: &str = "https://api.testnet.solana.com/";

magicblock-config/src/lib.rs

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use crate::{
2424
CommittorConfig, LedgerConfig, LoadableProgram, TaskSchedulerConfig,
2525
ValidatorConfig,
2626
},
27-
types::{network::Remote, BindAddress},
27+
types::{resolve_url, BindAddress, RemoteConfig, RemoteKind},
2828
};
2929

3030
/// Top-level configuration, assembled from multiple sources.
@@ -34,9 +34,10 @@ pub struct ValidatorParams {
3434
/// Path to the TOML configuration file (overrides CLI args).
3535
pub config: Option<PathBuf>,
3636

37-
/// Remote endpoints for syncing with the base chain.
38-
/// Can include HTTP (for JSON-RPC), WebSocket (for PubSub), and gRPC (for streaming) connections.
39-
pub remotes: Vec<Remote>,
37+
/// Array-based remote configurations for RPC, WebSocket, and gRPC.
38+
/// Configured via [[remote]] sections in TOML (array-of-tables syntax).
39+
#[serde(default, rename = "remote")]
40+
pub remotes: Vec<RemoteConfig>,
4041

4142
/// The application's operational mode.
4243
pub lifecycle: LifecycleMode,
@@ -173,6 +174,44 @@ impl ValidatorParams {
173174
.filter(|r| matches!(r, Remote::Grpc(_)))
174175
.map(|r| r.url_str())
175176
}
177+
178+
/// Returns the first RPC remote URL as an Option.
179+
pub fn rpc_url(&self) -> Option<&str> {
180+
self.remotes
181+
.iter()
182+
.find(|r| r.kind == RemoteKind::Rpc)
183+
.map(|r| r.url.as_str())
184+
}
185+
186+
/// Returns an iterator over all WebSocket remote URLs.
187+
pub fn websocket_urls(&self) -> impl Iterator<Item = &str> + '_ {
188+
self.remotes
189+
.iter()
190+
.filter(|r| r.kind == RemoteKind::Websocket)
191+
.map(|r| r.url.as_str())
192+
}
193+
194+
/// Returns an iterator over all gRPC remote URLs.
195+
pub fn grpc_urls(&self) -> impl Iterator<Item = &str> + '_ {
196+
self.remotes
197+
.iter()
198+
.filter(|r| r.kind == RemoteKind::Grpc)
199+
.map(|r| r.url.as_str())
200+
}
201+
202+
pub fn has_subscription_url(&self) -> bool {
203+
self.remotes.iter().any(|r| {
204+
r.kind == RemoteKind::Websocket || r.kind == RemoteKind::Grpc
205+
})
206+
}
207+
208+
/// Returns the RPC URL, using DEFAULT_REMOTE as fallback if not
209+
/// configured.
210+
pub fn rpc_url_or_default(&self) -> String {
211+
self.rpc_url().map(|s| s.to_string()).unwrap_or_else(|| {
212+
resolve_url(RemoteKind::Rpc, consts::DEFAULT_REMOTE)
213+
})
214+
}
176215
}
177216

178217
impl Display for ValidatorParams {

0 commit comments

Comments
 (0)