Skip to content

Commit 0f94135

Browse files
committed
feat(rpc): add validateaddress handler using bitcoin::Address::from_str + chain_network
Op: extend
1 parent 8264431 commit 0f94135

2 files changed

Lines changed: 98 additions & 1 deletion

File tree

crates/rpc/src/handlers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ impl Handler {
5555
"getmempooldescendants" => mempool::getmempooldescendants(&self.ctx, params),
5656
"estimatesmartfee" => util::estimatesmartfee(&self.ctx, params),
5757
"estimaterawfee" => util::estimaterawfee(&self.ctx, params),
58+
"validateaddress" => util::validateaddress(&self.ctx, params),
5859
"getnetworkinfo" => network::getnetworkinfo(&self.ctx, params),
5960
"getpeerinfo" => network::getpeerinfo(&self.ctx, params),
6061
"addnode" => network::addnode(&self.ctx, params),

crates/rpc/src/handlers/util.rs

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use sonic_rs::{Value, json};
55

66
use crate::context::Context;
77
use crate::error::RpcError;
8-
use crate::handlers::required_u64;
8+
use crate::handlers::{required_str, required_u64, serde_to_sonic};
99

1010
const BLOCK_VSIZE_TARGET: u64 = 1_000_000;
1111
const DEFAULT_MIN_FEERATE_SAT_PER_KVB: u64 = 1_000; // 1 sat/vB
@@ -68,6 +68,65 @@ pub(crate) fn estimaterawfee(ctx: &Arc<Context>, params: &Value) -> Result<Value
6868
"long": {"feerate": feerate, "decay": 0.962, "scale": 1}
6969
}))
7070
}
71+
72+
pub(crate) fn validateaddress(ctx: &Arc<Context>, params: &Value) -> Result<Value, RpcError> {
73+
use core::str::FromStr as _;
74+
75+
use bitcoin::hex::DisplayHex as _;
76+
77+
let address_str = required_str(params, 0, "address is required")?;
78+
let network = match ctx.chain_network {
79+
bitcoin_rs_primitives::Network::Mainnet => bitcoin::Network::Bitcoin,
80+
bitcoin_rs_primitives::Network::Testnet3 => bitcoin::Network::Testnet,
81+
bitcoin_rs_primitives::Network::Testnet4 => bitcoin::Network::Testnet4,
82+
bitcoin_rs_primitives::Network::Signet => bitcoin::Network::Signet,
83+
bitcoin_rs_primitives::Network::Regtest => bitcoin::Network::Regtest,
84+
};
85+
let Ok(unchecked) = bitcoin::Address::from_str(address_str) else {
86+
return Ok(json!({ "isvalid": false }));
87+
};
88+
let Ok(address) = unchecked.require_network(network) else {
89+
return Ok(json!({ "isvalid": false }));
90+
};
91+
92+
let script = address.script_pubkey();
93+
let script_hex = script.as_bytes().to_lower_hex_string();
94+
let address_canon = address.to_string();
95+
let mut response = serde_json::Map::new();
96+
response.insert("isvalid".to_owned(), serde_json::Value::Bool(true));
97+
response.insert(
98+
"address".to_owned(),
99+
serde_json::Value::String(address_canon),
100+
);
101+
response.insert(
102+
"scriptPubKey".to_owned(),
103+
serde_json::Value::String(script_hex),
104+
);
105+
response.insert(
106+
"isscript".to_owned(),
107+
serde_json::Value::Bool(script.is_p2sh() || script.is_p2wsh()),
108+
);
109+
response.insert(
110+
"iswitness".to_owned(),
111+
serde_json::Value::Bool(script.is_witness_program()),
112+
);
113+
if let Some(version) = script.witness_version() {
114+
response.insert(
115+
"witness_version".to_owned(),
116+
serde_json::Value::Number(i64::from(version.to_num()).into()),
117+
);
118+
// Witness program is the bytes after the 1-byte version prefix and 1-byte push opcode.
119+
let bytes = script.as_bytes();
120+
if bytes.len() >= 2 {
121+
response.insert(
122+
"witness_program".to_owned(),
123+
serde_json::Value::String(bytes[2..].to_lower_hex_string()),
124+
);
125+
}
126+
}
127+
128+
serde_to_sonic(&serde_json::Value::Object(response))
129+
}
71130
#[cfg(test)]
72131
mod tests {
73132
use super::*;
@@ -89,3 +148,40 @@ mod tests {
89148
);
90149
}
91150
}
151+
152+
#[cfg(test)]
153+
mod validateaddress_tests {
154+
use super::*;
155+
use alloc::sync::Arc;
156+
use sonic_rs::JsonValueTrait;
157+
158+
#[test]
159+
fn validateaddress_returns_isvalid_false_for_garbage() {
160+
let ctx = Arc::new(Context::new());
161+
let result = validateaddress(&ctx, &json!(["not a real address"]))
162+
.unwrap_or_else(|err| panic!("validateaddress failed: {err}"));
163+
let Some(isvalid) = result
164+
.get("isvalid")
165+
.and_then(sonic_rs::JsonValueTrait::as_bool)
166+
else {
167+
panic!("isvalid missing: {result:?}");
168+
};
169+
assert!(!isvalid);
170+
}
171+
172+
#[test]
173+
fn validateaddress_returns_isvalid_true_for_p2pkh_mainnet() {
174+
// ctx defaults to Mainnet network selector.
175+
let ctx = Arc::new(Context::new());
176+
// 1BoatSLRHtKNngkdXEeobR76b53LETtpyT is a famous P2PKH address.
177+
let result = validateaddress(&ctx, &json!(["1BoatSLRHtKNngkdXEeobR76b53LETtpyT"]))
178+
.unwrap_or_else(|err| panic!("validateaddress failed: {err}"));
179+
let Some(isvalid) = result
180+
.get("isvalid")
181+
.and_then(sonic_rs::JsonValueTrait::as_bool)
182+
else {
183+
panic!("isvalid missing: {result:?}");
184+
};
185+
assert!(isvalid, "expected valid: {result:?}");
186+
}
187+
}

0 commit comments

Comments
 (0)