rostrum 14.0.1

An efficient implementation of Electrum Server with token support
Documentation
use crate::chaindef::BlockHash;
use crate::def::{PROTOCOL_VERSION_MAX, PROTOCOL_VERSION_MIN};
use crate::discovery::discoverer::PeerDiscoverer;
use crate::discovery::peer::ServerPeer;
use crate::discovery::utilparser::parse_candidates_from_features;
use crate::edition::Edition;
use crate::errors::rpc_invalid_params;
use crate::query::Query;
use crate::rpc::parseutil::str_from_value;
use anyhow::{Context, Result};
use ip_rfc;
use serde_json::Value;
use std::sync::Arc;

use version_compare::Version;

use bitcoincash::network::constants::Network;

/*
 * This file contains implementations and tests of RPC calls starting with
 * server.*
 */

// The default argument to server.version
const SPEC_DEFAULT_VERSION: &str = "1.4";

pub struct ServerRPC {
    server_donation_address: Option<String>,
    network: Network,
}

/**
 * Implements `server.*` RPC calls
 */
impl ServerRPC {
    pub fn new(server_donation_address: Option<String>, network: Network) -> ServerRPC {
        ServerRPC {
            server_donation_address,
            network,
        }
    }

    /**
     * Return a server donation address.
     */
    pub fn server_donation_address(&self) -> Result<Value> {
        Ok(json!(self.server_donation_address))
    }

    pub async fn server_add_peer(
        &self,
        params: &[Value],
        addr: &std::net::SocketAddr,
        genesis_hash: &BlockHash,
        discoverer: &PeerDiscoverer,
    ) -> Result<Value> {
        debug!("discover: Add peer attempt by server peer {addr}");

        // Basic validation
        if self.network != Network::Regtest && !ip_rfc::global(&addr.ip()) {
            debug!("Rejecting server peer '{addr}': Peer does not have a globally routable IP.");
            return Ok(json!(false));
        }

        let peer_features = params.first().context("argument missing")?;

        let mut candidates = match parse_candidates_from_features(
            peer_features,
            addr.ip(),
            Some(genesis_hash),
        )
        .await
        {
            Ok(c) => c,
            Err(e) => {
                trace!("discover: Rejecting peer {}: {}", addr, e);
                return Ok(json!(false));
            }
        };

        let mut added = false;
        for c in candidates.drain(..) {
            let hostname = c.pretty_hostname();
            trace!("discover: Adding candidate {hostname}");
            if let Err(e) = discoverer.add_candidate(c).await {
                trace!("discover: rejecting candidate {hostname}: {e}");
                return Ok(json!(added));
            }
            added = true;
        }

        Ok(json!(true))
    }
}

fn best_match(client_min: &Version, client_max: &Version) -> String {
    let our_min = Version::from(PROTOCOL_VERSION_MIN).unwrap();
    let our_max = Version::from(PROTOCOL_VERSION_MAX).unwrap();

    if *client_max >= our_max {
        return our_max.as_str().into();
    }

    if *client_max <= our_min {
        return our_min.as_str().into();
    }

    if *client_min >= our_min && *client_max <= our_max {
        return client_max.as_str().into();
    }

    our_min.as_str().into()
}

fn best_match_response(version: &str, client_min: &Version, client_max: &Version) -> Value {
    json!([version, best_match(client_min, client_max)])
}

pub fn parse_version(version: &str) -> Result<Version<'_>> {
    let version =
        Version::from(version).context(rpc_invalid_params("invalid version string".to_string()))?;
    Ok(version)
}

pub fn parse_edition(params: &[Value]) -> Result<Edition> {
    if let Some(edition_val) = params.get(2) {
        if let Ok(edition_str) = str_from_value(Some(edition_val), "edition") {
            return Edition::parse(&edition_str).ok_or_else(|| {
                rpc_invalid_params(format!("unsupported edition: {}", edition_str))
            });
        }
    }
    Ok(Edition::Original)
}

pub fn server_version(params: &[Value], server_version: &str) -> (Edition, Result<Value>) {
    let edition = match parse_edition(params) {
        Ok(e) => e,
        Err(e) => return (Edition::Original, Err(e)),
    };

    let default_version = json!(SPEC_DEFAULT_VERSION);
    let val = params.get(1).unwrap_or(&default_version);

    if let Ok(versionstr) = str_from_value(Some(val), "version") {
        let version = match parse_version(&versionstr) {
            Ok(v) => v,
            Err(e) => return (edition, Err(e)),
        };
        return (
            edition,
            Ok(best_match_response(server_version, &version, &version)),
        );
    }

    if let Some(minmax_list) = val.as_array() {
        let min = match str_from_value(Some(&minmax_list[0]), "version") {
            Ok(v) => v,
            Err(e) => return (edition, Err(e)),
        };
        let min = match parse_version(&min) {
            Ok(v) => v,
            Err(e) => return (edition, Err(e)),
        };
        let max = match str_from_value(Some(&minmax_list[1]), "version") {
            Ok(v) => v,
            Err(e) => return (edition, Err(e)),
        };
        let max = match parse_version(&max) {
            Ok(v) => v,
            Err(e) => return (edition, Err(e)),
        };
        return (edition, Ok(best_match_response(server_version, &min, &max)));
    }

    (
        edition,
        Err(rpc_invalid_params(
            "invalid value in version argument".to_string(),
        )),
    )
}

pub async fn server_banner(query: &Arc<Query>) -> Result<Value> {
    Ok(json!(query.get_banner().await?))
}

pub async fn server_peers_subscribe(discoverer: &PeerDiscoverer) -> Value {
    json!(discoverer
        .get_peers()
        .await
        .iter()
        .map(|p| {
            let ip = p.ip().to_string();
            let host = p.hostname().unwrap_or(ip.clone());

            let mut properties: Vec<String> = Vec::default();
            if let Some(port) = p.tcp_port() {
                properties.push(format!("t{}", port));
            }
            if let Some(port) = p.ssl_port() {
                properties.push(format!("s{}", port));
            }
            properties.push(format!("v{}", p.version()));
            json!(vec![json!(ip), json!(host), json!(properties)])
        })
        .collect::<Vec<Value>>())
}

pub fn server_features(query: &Arc<Query>) -> Result<Value> {
    Ok(json!(query.announcer().server_features()))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_edition_missing() {
        let params: &[Value] = &[];
        let result = parse_edition(params).unwrap();
        assert_eq!(result, Edition::Original);
    }

    #[test]
    fn test_parse_edition_null() {
        let params = [json!(null)];
        let result = parse_edition(params.as_slice()).unwrap();
        assert_eq!(result, Edition::Original);
    }

    #[test]
    fn test_parse_edition_valid() {
        let p1 = json!("dummy");
        let p2 = json!("dummy");
        let p3 = json!("2027-pre");
        let arr: Vec<Value> = vec![p1, p2, p3];
        let result = parse_edition(&arr).unwrap();
        assert_eq!(result, Edition::Edition2027Pre);

        let p1 = json!("dummy");
        let p2 = json!("dummy");
        let p3 = json!("original");
        let arr: Vec<Value> = vec![p1, p2, p3];
        let result = parse_edition(&arr).unwrap();
        assert_eq!(result, Edition::Original);
    }

    #[test]
    fn test_parse_edition_invalid() {
        let p1 = json!("dummy");
        let p2 = json!("dummy");
        let p3 = json!("invalid-edition");
        let arr: Vec<Value> = vec![p1, p2, p3];
        let result = parse_edition(&arr);
        assert!(result.is_err());
    }

    #[test]
    fn test_server_version_edition_not_set_on_error() {
        // Valid edition but invalid version — edition should still be returned,
        // but the caller should only apply it when result is Ok.
        let (edition, result) = server_version(
            &[json!("client"), json!(42), json!("2027-pre")],
            "dummy server",
        );
        assert!(result.is_err());
        // The returned edition is parsed, but callers must not apply it on error.
        assert_eq!(edition, Edition::Edition2027Pre);
    }

    #[test]
    fn test_server_version_noarg() {
        let (_, resp) = server_version(&[], "dummy server");
        let resp = resp.unwrap();
        let resp = resp.as_array().unwrap();

        assert!(resp[0].is_string());
        assert_eq!(resp[1].as_str().unwrap(), SPEC_DEFAULT_VERSION);
    }

    #[test]
    fn test_server_version_strarg() {
        let clientver = json!("bestclient 1.0");
        let (_, resp) = server_version(&[clientver.clone(), json!("1.3")], "dummy server");
        let resp = resp.unwrap();
        assert_eq!(resp[1].as_str().unwrap(), PROTOCOL_VERSION_MIN);
        let (_, resp) = server_version(&[clientver.clone(), json!("13.3.7")], "dummy server");
        let resp = resp.unwrap();
        assert_eq!(resp[1].as_str().unwrap(), PROTOCOL_VERSION_MAX);
    }

    #[test]
    fn test_server_version_minmax() {
        let clientver = json!("bestclient 1.0");
        // client max is higher than our max, we should return our max
        let (_, resp) = server_version(
            &[clientver.clone(), json!(["1.4", "13.3.7"])],
            "dummy server",
        );
        let resp = resp.unwrap();
        assert_eq!(resp[1].as_str().unwrap(), PROTOCOL_VERSION_MAX);

        // client max is lower than our min, we shoud return our min
        let (_, resp) = server_version(&[clientver.clone(), json!(["1.2", "1.3"])], "dummy server");
        let resp = resp.unwrap();
        assert_eq!(resp[1].as_str().unwrap(), PROTOCOL_VERSION_MIN);

        // client max is somewhere between our max and min, return same version
        let client_max = "1.4.1";
        let (_, resp) = server_version(
            &[clientver.clone(), json!([PROTOCOL_VERSION_MIN, client_max])],
            "dummy server",
        );
        let resp = resp.unwrap();
        assert_eq!(resp[1].as_str().unwrap(), client_max);
    }

    #[test]
    fn test_donation_address() {
        let rpc = ServerRPC::new(None, Network::Bitcoin);
        let result: Option<String> = None;
        assert_eq!(rpc.server_donation_address().unwrap(), json!(result));
        let rpc = ServerRPC::new(
            Some("bitcoincash:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqu08dsyxz98whc".to_string()),
            Network::Bitcoin,
        );
        assert_eq!(
            rpc.server_donation_address().unwrap(),
            json!("bitcoincash:qqqqqqqqqqqqqqqqqqqqqqqqqqqqqu08dsyxz98whc")
        );
    }
}