use std::convert::Infallible;
use std::sync::{Arc, Mutex};
use http_body_util::{BodyExt, Full};
use hyper::body::Bytes;
use hyper::service::service_fn;
use hyper::{Request, Response};
use hyper_util::rt::TokioIo;
use tokio::net::TcpListener;
use qorechain::client::ClientBuilder;
use qorechain::query::{JsonRpcClient, QorClient, RestClient, QOR_METHODS};
#[derive(Debug, Clone)]
struct Recorded {
method: String,
path: String,
query: Option<String>,
body: String,
}
struct MockServer {
base_url: String,
recorded: Arc<Mutex<Vec<Recorded>>>,
_shutdown: tokio::sync::oneshot::Sender<()>,
}
impl MockServer {
async fn start(response_body: &'static str) -> Self {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let base_url = format!("http://{addr}");
let recorded: Arc<Mutex<Vec<Recorded>>> = Arc::new(Mutex::new(Vec::new()));
let (tx, mut rx) = tokio::sync::oneshot::channel::<()>();
let rec = recorded.clone();
tokio::spawn(async move {
loop {
tokio::select! {
_ = &mut rx => break,
accepted = listener.accept() => {
let (stream, _) = match accepted {
Ok(v) => v,
Err(_) => continue,
};
let io = TokioIo::new(stream);
let rec = rec.clone();
tokio::spawn(async move {
let svc = service_fn(move |req: Request<hyper::body::Incoming>| {
let rec = rec.clone();
async move {
let method = req.method().to_string();
let path = req.uri().path().to_string();
let query = req.uri().query().map(|s| s.to_string());
let bytes = req.into_body().collect().await.unwrap().to_bytes();
let body = String::from_utf8_lossy(&bytes).to_string();
rec.lock().unwrap().push(Recorded {
method,
path,
query,
body,
});
Ok::<_, Infallible>(Response::new(Full::new(Bytes::from(
response_body,
))))
}
});
let _ = hyper::server::conn::http1::Builder::new()
.serve_connection(io, svc)
.await;
});
}
}
}
});
MockServer {
base_url,
recorded,
_shutdown: tx,
}
}
fn last(&self) -> Recorded {
self.recorded.lock().unwrap().last().cloned().unwrap()
}
fn all(&self) -> Vec<Recorded> {
self.recorded.lock().unwrap().clone()
}
}
#[tokio::test]
async fn rest_paths_are_correct() {
let server = MockServer::start(r#"{"ok":true}"#).await;
let rest = RestClient::new(server.base_url.clone());
rest.get_all_balances("qor1abc").await.unwrap();
assert_eq!(server.last().path, "/cosmos/bank/v1beta1/balances/qor1abc");
rest.get_ai_stats().await.unwrap();
assert_eq!(server.last().path, "/qorechain/ai/v1/stats");
rest.get_fee_estimate("fast").await.unwrap();
let last = server.last();
assert_eq!(last.path, "/qorechain/ai/v1/fee-estimate");
assert_eq!(last.query.as_deref(), Some("urgency=fast"));
rest.get_bridge_chains().await.unwrap();
assert_eq!(server.last().path, "/qorechain/bridge/v1/chains");
rest.get_pqc_account("qor1abc").await.unwrap();
assert_eq!(server.last().path, "/qorechain/pqc/v1/accounts/qor1abc");
rest.get_reputation("qorvaloper1xyz").await.unwrap();
assert_eq!(
server.last().path,
"/qorechain/reputation/v1/validators/qorvaloper1xyz"
);
rest.get_burn_stats().await.unwrap();
assert_eq!(server.last().path, "/qorechain/burn/v1/stats");
rest.get_xqore_position("qor1abc").await.unwrap();
assert_eq!(server.last().path, "/qorechain/xqore/v1/position/qor1abc");
rest.get_inflation_rate().await.unwrap();
assert_eq!(server.last().path, "/qorechain/inflation/v1/rate");
assert!(server.all().iter().all(|r| r.method == "GET"));
}
#[tokio::test]
async fn rest_http_error_is_surfaced() {
let server = MockServer::start(r#"{"ok":true}"#).await;
let rest = RestClient::new(server.base_url.clone());
let v = rest.get("/anything", &[]).await.unwrap();
assert_eq!(v["ok"], true);
}
#[tokio::test]
async fn jsonrpc_envelope_and_error_mapping() {
let server = MockServer::start(r#"{"jsonrpc":"2.0","id":1,"result":{"value":42}}"#).await;
let client = JsonRpcClient::new(server.base_url.clone());
let res = client
.call("some_method", serde_json::json!(["arg"]))
.await
.unwrap();
assert_eq!(res["value"], 42);
let last = server.last();
assert_eq!(last.method, "POST");
let body: serde_json::Value = serde_json::from_str(&last.body).unwrap();
assert_eq!(body["jsonrpc"], "2.0");
assert_eq!(body["method"], "some_method");
assert_eq!(body["params"][0], "arg");
let err_server = MockServer::start(
r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"not found"}}"#,
)
.await;
let err_client = JsonRpcClient::new(err_server.base_url.clone());
let err = err_client
.call("missing", serde_json::Value::Null)
.await
.unwrap_err();
match err {
qorechain::Error::JsonRpc { code, message } => {
assert_eq!(code, -32601);
assert_eq!(message, "not found");
}
other => panic!("expected JsonRpc error, got {other:?}"),
}
}
#[tokio::test]
async fn qor_methods_use_exact_wire_strings() {
let server = MockServer::start(r#"{"jsonrpc":"2.0","id":1,"result":{}}"#).await;
let qor = QorClient::new(server.base_url.clone());
macro_rules! check {
($call:expr, $wire:expr) => {{
$call.await.unwrap();
let body: serde_json::Value = serde_json::from_str(&server.last().body).unwrap();
assert_eq!(body["method"], $wire, "wrong wire method for {}", $wire);
}};
}
check!(qor.get_pqc_key_status("a"), "qor_getPQCKeyStatus");
check!(
qor.get_hybrid_signature_mode(),
"qor_getHybridSignatureMode"
);
check!(qor.get_ai_stats(), "qor_getAIStats");
check!(qor.get_cross_vm_message("m"), "qor_getCrossVMMessage");
check!(qor.get_reputation_score("v"), "qor_getReputationScore");
check!(qor.get_layer_info("l"), "qor_getLayerInfo");
check!(qor.get_bridge_status("c"), "qor_getBridgeStatus");
check!(qor.get_rl_agent_status(), "qor_getRLAgentStatus");
check!(qor.get_rl_observation(), "qor_getRLObservation");
check!(qor.get_rl_reward(), "qor_getRLReward");
check!(
qor.get_pool_classification("v"),
"qor_getPoolClassification"
);
check!(qor.get_burn_stats(), "qor_getBurnStats");
check!(qor.get_xqore_position("a"), "qor_getXQOREPosition");
check!(qor.get_inflation_rate(), "qor_getInflationRate");
check!(qor.get_tokenomics_overview(), "qor_getTokenomicsOverview");
check!(qor.get_rollup_status("r"), "qor_getRollupStatus");
check!(qor.list_rollups(), "qor_listRollups");
check!(qor.get_settlement_batch("r", 1), "qor_getSettlementBatch");
check!(qor.suggest_rollup_profile("u"), "qor_suggestRollupProfile");
check!(qor.get_da_blob_status("r", 0), "qor_getDABlobStatus");
check!(
qor.get_btc_staking_position("a"),
"qor_getBTCStakingPosition"
);
check!(qor.get_abstract_account("a"), "qor_getAbstractAccount");
check!(qor.get_fair_block_status(), "qor_getFairBlockStatus");
check!(
qor.get_gas_abstraction_config(),
"qor_getGasAbstractionConfig"
);
check!(qor.get_lane_configuration(), "qor_getLaneConfiguration");
}
#[test]
fn qor_methods_table_is_complete_and_exact() {
assert_eq!(QOR_METHODS.len(), 25);
let expected = [
"qor_getPQCKeyStatus",
"qor_getHybridSignatureMode",
"qor_getAIStats",
"qor_getCrossVMMessage",
"qor_getReputationScore",
"qor_getLayerInfo",
"qor_getBridgeStatus",
"qor_getRLAgentStatus",
"qor_getRLObservation",
"qor_getRLReward",
"qor_getPoolClassification",
"qor_getBurnStats",
"qor_getXQOREPosition",
"qor_getInflationRate",
"qor_getTokenomicsOverview",
"qor_getRollupStatus",
"qor_listRollups",
"qor_getSettlementBatch",
"qor_suggestRollupProfile",
"qor_getDABlobStatus",
"qor_getBTCStakingPosition",
"qor_getAbstractAccount",
"qor_getFairBlockStatus",
"qor_getGasAbstractionConfig",
"qor_getLaneConfiguration",
];
let actual: Vec<&str> = QOR_METHODS.iter().map(|(_, wire)| *wire).collect();
assert_eq!(actual, expected);
}
#[tokio::test]
async fn client_testnet_default_and_overrides() {
let server = MockServer::start(r#"{"ok":true}"#).await;
let client = ClientBuilder::new()
.rest(server.base_url.clone())
.evm_rpc(server.base_url.clone())
.build()
.unwrap();
assert_eq!(client.network.name, "testnet");
assert_eq!(client.network.chain_id.as_deref(), Some("qorechain-diana"));
client.rest.get_burn_stats().await.unwrap();
assert_eq!(server.last().path, "/qorechain/burn/v1/stats");
}
#[test]
fn client_mainnet_default_builds_live() {
let client = ClientBuilder::new().network("mainnet").build().unwrap();
assert_eq!(client.network.name, "mainnet");
assert!(client.network.live);
assert_eq!(client.network.chain_id.as_deref(), Some("qorechain-vladi"));
let eps = client.network.endpoints.as_ref().unwrap();
assert_eq!(eps.rest, "http://localhost:1317");
}
#[test]
fn client_mainnet_with_endpoints_builds() {
let client = ClientBuilder::new()
.network("mainnet")
.rest("https://rest.example.com")
.evm_rpc("https://evm.example.com")
.build()
.unwrap();
assert_eq!(client.network.name, "mainnet");
assert_eq!(client.network.chain_id.as_deref(), Some("qorechain-vladi"));
let eps = client.network.endpoints.as_ref().unwrap();
assert_eq!(eps.rest, "https://rest.example.com");
}
#[test]
fn client_unknown_network_errors() {
let err = ClientBuilder::new()
.network("does-not-exist")
.build()
.unwrap_err();
assert!(matches!(err, qorechain::Error::UnknownNetwork(_)));
}
#[tokio::test]
async fn client_fee_estimate_uses_oracle_then_falls_back() {
let server = MockServer::start(r#"{"suggested_fee_uqor":"1234"}"#).await;
let client = ClientBuilder::new()
.rest(server.base_url.clone())
.evm_rpc(server.base_url.clone())
.build()
.unwrap();
let fee = client.fees.estimate("fast").await.unwrap();
assert_eq!(fee["amount"][0]["amount"], "1234");
assert_eq!(fee["amount"][0]["denom"], "uqor");
assert_eq!(fee["gas"], "200000");
}