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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ tempdb.sqlite
swap-orchestrator/docker-compose.yml
swap-orchestrator/config.toml

# ASB RPC auth keyfile
rpc-auth

# release build generator scripts
release-build.sh
cn_macos
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- ASB+CONTROLLER: The JSON-RPC server now requires authentication. The ASB verifies a password against a hashed keyfile (`--rpc-auth-file`), and `asb-controller` prompts for the password on startup. Generate the keyfile with `orchestrator gen-rpc-auth`. Clients authenticate by sending the password with every request in an `Authorization: Bearer <password>` header.
- ASB+GUI: Skip publishing the Monero redeem/refund transaction if it is already present on chain (e.g. after a restart)

## [4.8.4] - 2026-06-09
Expand Down
8 changes: 8 additions & 0 deletions Cargo.lock

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

6 changes: 5 additions & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,13 @@ test_monero_sys:
swap:
cargo build -p swap-asb --bin asb && cd swap && cargo build --bin=swap

# Generate the ASB RPC auth keyfile
gen-rpc-auth:
cargo run -p swap-orchestrator --bin orchestrator -- gen-rpc-auth

# Run the asb on testnet
asb-testnet:
cargo run -p swap-asb --bin asb -- --testnet --trace start --rpc-bind-port 9944 --rpc-bind-host 0.0.0.0
cargo run -p swap-asb --bin asb -- --testnet --trace start --rpc-bind-port 9944 --rpc-bind-host 0.0.0.0 --rpc-auth-file rpc-auth

# Launch the ASB controller REPL against a local testnet ASB instance
asb-testnet-controller:
Expand Down
11 changes: 11 additions & 0 deletions swap-asb/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ where
resume_only,
rpc_bind_host,
rpc_bind_port,
rpc_auth_file,
} => {
// Validate RPC bind arguments early
validate_rpc_bind_args(&rpc_bind_host, &rpc_bind_port)?;
Expand All @@ -44,6 +45,7 @@ where
resume_only,
rpc_bind_host,
rpc_bind_port,
rpc_auth_file,
},
}
}
Expand Down Expand Up @@ -226,6 +228,7 @@ pub enum Command {
resume_only: bool,
rpc_bind_host: Option<String>,
rpc_bind_port: Option<u16>,
rpc_auth_file: Option<PathBuf>,
},
History {
only_unfinished: bool,
Expand Down Expand Up @@ -319,6 +322,11 @@ pub enum RawCommand {
help = "Port to bind the JSON-RPC server to (e.g., 9944). Must be used together with --rpc-bind-host."
)]
rpc_bind_port: Option<u16>,
#[structopt(
long = "rpc-auth-file",
help = "Path to the RPC auth verifier file. Required when the JSON-RPC server is enabled."
)]
rpc_auth_file: Option<PathBuf>,
},
#[structopt(about = "Prints all logging messages issued in the past.")]
Logs {
Expand Down Expand Up @@ -494,6 +502,7 @@ mod tests {
resume_only: false,
rpc_bind_host: None,
rpc_bind_port: None,
rpc_auth_file: None,
},
};
let args = parse_args(raw_ars).unwrap();
Expand Down Expand Up @@ -707,6 +716,7 @@ mod tests {
resume_only: false,
rpc_bind_host: None,
rpc_bind_port: None,
rpc_auth_file: None,
},
};
let args = parse_args(raw_ars).unwrap();
Expand Down Expand Up @@ -948,6 +958,7 @@ mod tests {
resume_only: false,
rpc_bind_host: None,
rpc_bind_port: None,
rpc_auth_file: None,
},
};
let args = parse_args(raw_ars).unwrap();
Expand Down
12 changes: 12 additions & 0 deletions swap-asb/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,18 @@ pub async fn main() -> Result<()> {
resume_only,
rpc_bind_host,
rpc_bind_port,
rpc_auth_file,
} => {
let rpc_auth_verifier = match (&rpc_bind_host, &rpc_bind_port) {
(Some(_), Some(_)) => {
let auth_file = rpc_auth_file.context(
"The JSON-RPC server requires authentication: pass --rpc-auth-file pointing at the RPC auth verifier file",
)?;
Some(swap_env::rpc_auth::load_verifier(&auth_file)?)
}
_ => None,
};

let db = open_db(db_file, AccessMode::ReadWrite, None).await?;

let developer_tip = config.maker.developer_tip;
Expand Down Expand Up @@ -391,6 +402,7 @@ pub async fn main() -> Result<()> {
let rpc_server = RpcServer::start(
host,
port,
rpc_auth_verifier,
bitcoin_wallet.clone(),
monero_wallet.clone(),
event_loop_service,
Expand Down
1 change: 1 addition & 0 deletions swap-controller/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ anyhow = { workspace = true }
bitcoin = { workspace = true }
clap = { version = "4", features = ["derive"] }
comfy-table = "7.2.1"
dialoguer = { workspace = true }
jsonrpsee = { workspace = true, features = ["client-core", "http-client"] }
monero-oxide-ext = { path = "../monero-oxide-ext" }
rustyline = "17.0.0"
Expand Down
66 changes: 63 additions & 3 deletions swap-controller/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
mod cli;
mod repl;

use anyhow::Context;
use clap::Parser;
use cli::{Cli, Cmd};
use jsonrpsee::http_client::{HeaderMap, HeaderValue, HttpClient, HttpClientBuilder};
use swap_controller_api::{AsbApiClient, MoneroSeedResponse};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();

let client = jsonrpsee::http_client::HttpClientBuilder::default().build(&cli.url)?;
let client = authenticate(&cli.url).await?;

match cli.cmd {
None => repl::run(client, dispatch).await?,
None => repl::run(client, dispatch_or_exit).await?,
Some(cmd) => {
if let Err(e) = dispatch(cmd.clone(), client.clone()).await {
if let Err(e) = dispatch_or_exit(cmd.clone(), client.clone()).await {
eprintln!("Command failed with error: {e:?}");
}
}
Expand All @@ -23,6 +25,64 @@ async fn main() -> anyhow::Result<()> {
Ok(())
}

/// Exits when the ASB rejects the session's password (it changed while the
/// controller was running); re-authenticating requires a restart.
async fn dispatch_or_exit(cmd: Cmd, client: impl AsbApiClient) -> anyhow::Result<()> {
let result = dispatch(cmd, client).await;

if let Err(e) = &result {
let rejected = e
.downcast_ref::<jsonrpsee::core::ClientError>()
.is_some_and(is_auth_failure);
if rejected {
eprintln!("The ASB rejected the password. It must have changed, exiting.");
std::process::exit(1);
}
}

result
}

/// Prompts for the RPC password and returns a client once the server accepts
/// it, re-prompting on an authentication failure and bailing if the server is
/// unreachable for any other reason.
async fn authenticate(url: &str) -> anyhow::Result<HttpClient> {
loop {
let password = dialoguer::Password::new()
.with_prompt("ASB RPC password")
.interact()
.context("Failed to read password")?;

let mut headers = HeaderMap::new();
headers.insert(
"authorization",
HeaderValue::from_str(&format!("Bearer {password}"))
.context("Password is not a valid HTTP header value")?,
);
let client = HttpClientBuilder::default()
.set_headers(headers)
.build(url)?;

match client.check_connection().await {
Ok(()) => return Ok(client),
Err(e) if is_auth_failure(&e) => eprintln!("Authentication failed, try again."),
Err(e) => return Err(e).context("Failed to reach the ASB RPC server"),
}
}
}

fn is_auth_failure(error: &jsonrpsee::core::ClientError) -> bool {
use jsonrpsee::http_client::transport::Error as TransportError;

let jsonrpsee::core::ClientError::Transport(source) = error else {
return false;
};
matches!(
source.downcast_ref::<TransportError>(),
Some(TransportError::Rejected { status_code: 401 })
)
}

async fn dispatch(cmd: Cmd, client: impl AsbApiClient) -> anyhow::Result<()> {
match cmd {
Cmd::CheckConnection => {
Expand Down
4 changes: 4 additions & 0 deletions swap-env/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ bitcoin = { workspace = true }
config = { version = "0.14", default-features = false, features = ["toml"] }
console = { workspace = true }
dialoguer = { workspace = true }
hex = "0.4"
hmac = "0.12"
libp2p = { workspace = true, features = ["serde"] }
monero-address = { workspace = true }
rand = { workspace = true }
rust_decimal = { workspace = true }
serde = { workspace = true }
sha2 = { workspace = true }
swap-fs = { path = "../swap-fs" }
swap-serde = { path = "../swap-serde" }
thiserror = { workspace = true }
Expand Down
1 change: 1 addition & 0 deletions swap-env/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub mod config;
pub mod defaults;
pub mod env;
pub mod prompt;
pub mod rpc_auth;
Loading
Loading