Skip to content

Commit 69f5664

Browse files
committed
Add the daemon module and standalone CLI for it
1 parent 3108313 commit 69f5664

File tree

8 files changed

+1499
-8
lines changed

8 files changed

+1499
-8
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ repository = "https://github.com/BlockstreamResearch/hal-simplicity/"
99
description = "hal-simplicity: a Simplicity extension of hal"
1010
keywords = [ "crypto", "bitcoin", "elements", "liquid", "simplicity" ]
1111
readme = "README.md"
12+
default-run = "hal-simplicity"
1213

1314
[lib]
1415
name = "hal_simplicity"
@@ -18,6 +19,19 @@ path = "src/lib.rs"
1819
name = "hal-simplicity"
1920
path = "src/bin/hal-simplicity/main.rs"
2021

22+
[[bin]]
23+
name = "hal-simplicity-daemon"
24+
path = "src/bin/hal-simplicity-daemon/main.rs"
25+
26+
[features]
27+
default = []
28+
daemon = [
29+
"dep:chrono",
30+
"dep:hyper",
31+
"dep:hyper-util",
32+
"dep:http-body-util",
33+
"dep:tokio",
34+
]
2135

2236
[dependencies]
2337
hal = "0.10.0"
@@ -34,6 +48,13 @@ elements = { version = "0.25.2", features = [ "serde", "base64" ] }
3448
simplicity = { package = "simplicity-lang", version = "0.5.0", features = [ "base64", "serde" ] }
3549
thiserror = "2.0.17"
3650

51+
# Daemon-only dependencies
52+
chrono = { version = "0.4", optional = true }
53+
hyper = { version = "1.8.1", features = ["server", "http1"], optional = true }
54+
hyper-util = { version = "0.1", features = ["tokio"], optional = true }
55+
http-body-util = { version = "0.1", optional = true }
56+
tokio = { version = "1.48.0", features = ["full"], optional = true }
57+
3758
[lints.clippy]
3859
# Exclude lints we don't think are valuable.
3960
needless_question_mark = "allow" # https://github.com/rust-bitcoin/rust-bitcoin/pull/2134
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#[cfg(not(feature = "daemon"))]
2+
fn main() {
3+
eprintln!("hal-simplicity-daemon can only be built with the 'daemon' feature enabled");
4+
std::process::exit(1);
5+
}
6+
7+
#[cfg(feature = "daemon")]
8+
fn main() {
9+
use hal_simplicity::daemon::HalSimplicityDaemon;
10+
11+
/// Default address for the TCP listener
12+
const DEFAULT_ADDRESS: &str = "127.0.0.1:28579";
13+
14+
/// Setup logging with the given log level.
15+
fn setup_logger(lvl: log::LevelFilter) {
16+
fern::Dispatch::new()
17+
.format(|out, message, _record| out.finish(format_args!("{}", message)))
18+
.level(lvl)
19+
.chain(std::io::stderr())
20+
.apply()
21+
.expect("error setting up logger");
22+
}
23+
24+
/// Create the main app object.
25+
fn init_app<'a, 'b>() -> clap::App<'a, 'b> {
26+
clap::App::new("hal-simplicity-daemon")
27+
.bin_name("hal-simplicity-daemon")
28+
.version(clap::crate_version!())
29+
.about("hal-simplicity-daemon -- JSON-RPC daemon for Simplicity operations")
30+
.arg(
31+
clap::Arg::with_name("address")
32+
.short("a")
33+
.long("address")
34+
.value_name("ADDRESS")
35+
.help("TCP address to bind to (default: 127.0.0.1:28579)")
36+
.takes_value(true),
37+
)
38+
.arg(
39+
clap::Arg::with_name("verbose")
40+
.short("v")
41+
.long("verbose")
42+
.help("Enable verbose logging output to stderr")
43+
.takes_value(false),
44+
)
45+
}
46+
47+
let app = init_app();
48+
let matches = app.get_matches();
49+
50+
// Enable logging in verbose mode.
51+
match matches.is_present("verbose") {
52+
true => setup_logger(log::LevelFilter::Debug),
53+
false => setup_logger(log::LevelFilter::Info),
54+
}
55+
56+
// Get the address from command line or use default
57+
let address = matches.value_of("address").unwrap_or(DEFAULT_ADDRESS);
58+
59+
log::info!("Starting hal-simplicity-daemon on {}...", address);
60+
61+
// Create the daemon
62+
let daemon = match HalSimplicityDaemon::new(address) {
63+
Ok(d) => d,
64+
Err(e) => {
65+
log::error!("Failed to create daemon: {}", e);
66+
67+
std::process::exit(1);
68+
}
69+
};
70+
71+
// Start the daemon and block
72+
if let Err(e) = daemon.listen_blocking() {
73+
log::error!("Daemon error: {}", e);
74+
75+
std::process::exit(1);
76+
}
77+
}

src/daemon/handler.rs

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
use std::str::FromStr;
2+
3+
use super::jsonrpc::{ErrorCode, JsonRpcService, RpcError, RpcHandler};
4+
use serde_json::Value;
5+
6+
use super::types::*;
7+
use crate::actions;
8+
9+
use crate::Network;
10+
11+
/// RPC method names
12+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13+
pub enum RpcMethod {
14+
AddressCreate,
15+
AddressInspect,
16+
BlockCreate,
17+
BlockDecode,
18+
TxCreate,
19+
TxDecode,
20+
KeypairGenerate,
21+
SimplicityInfo,
22+
SimplicitySighash,
23+
PsetCreate,
24+
PsetExtract,
25+
PsetFinalize,
26+
PsetRun,
27+
PsetUpdateInput,
28+
}
29+
30+
impl FromStr for RpcMethod {
31+
type Err = RpcError;
32+
33+
fn from_str(s: &str) -> Result<Self, RpcError> {
34+
let method = match s {
35+
"address_create" => Self::AddressCreate,
36+
"address_inspect" => Self::AddressInspect,
37+
"block_create" => Self::BlockCreate,
38+
"block_decode" => Self::BlockDecode,
39+
"tx_create" => Self::TxCreate,
40+
"tx_decode" => Self::TxDecode,
41+
"keypair_generate" => Self::KeypairGenerate,
42+
"simplicity_info" => Self::SimplicityInfo,
43+
"simplicity_sighash" => Self::SimplicitySighash,
44+
"pset_create" => Self::PsetCreate,
45+
"pset_extract" => Self::PsetExtract,
46+
"pset_finalize" => Self::PsetFinalize,
47+
"pset_run" => Self::PsetRun,
48+
"pset_update_input" => Self::PsetUpdateInput,
49+
_ => return Err(RpcError::new(ErrorCode::MethodNotFound)),
50+
};
51+
52+
Ok(method)
53+
}
54+
}
55+
56+
/// Default RPC handler that provides basic methods
57+
#[derive(Default)]
58+
pub struct DefaultRpcHandler;
59+
60+
impl RpcHandler for DefaultRpcHandler {
61+
fn handle(&self, method: &str, params: Option<Value>) -> Result<Value, RpcError> {
62+
let rpc_method = RpcMethod::from_str(method)?;
63+
64+
match rpc_method {
65+
RpcMethod::AddressCreate => {
66+
let req: AddressCreateRequest = parse_params(params)?;
67+
let result = actions::address::address_create(
68+
req.pubkey.as_deref(),
69+
req.script.as_deref(),
70+
req.blinder.as_deref(),
71+
req.network.unwrap_or(Network::Liquid),
72+
)
73+
.map_err(|e| RpcError::custom(ErrorCode::InternalError.code(), e.to_string()))?;
74+
75+
serialize_result(result)
76+
}
77+
RpcMethod::AddressInspect => {
78+
let req: AddressInspectRequest = parse_params(params)?;
79+
let result = actions::address::address_inspect(&req.address).map_err(|e| {
80+
RpcError::custom(ErrorCode::InternalError.code(), e.to_string())
81+
})?;
82+
83+
serialize_result(result)
84+
}
85+
RpcMethod::BlockCreate => {
86+
let req: BlockCreateRequest = parse_params(params)?;
87+
88+
let block = actions::block::block_create(req.block_info).map_err(|e| {
89+
RpcError::custom(ErrorCode::InternalError.code(), e.to_string())
90+
})?;
91+
92+
let raw_block = hex::encode(elements::encode::serialize(&block));
93+
serialize_result(BlockCreateResponse {
94+
raw_block,
95+
})
96+
}
97+
RpcMethod::BlockDecode => {
98+
let req: BlockDecodeRequest = parse_params(params)?;
99+
let result = actions::block::block_decode(
100+
&req.raw_block,
101+
req.network.unwrap_or(Network::Liquid),
102+
req.txids.unwrap_or(false),
103+
)
104+
.map_err(|e| RpcError::custom(ErrorCode::InternalError.code(), e.to_string()))?;
105+
106+
serialize_result(result)
107+
}
108+
RpcMethod::TxCreate => {
109+
let req: TxCreateRequest = parse_params(params)?;
110+
let tx = actions::tx::tx_create(req.tx_info).map_err(|e| {
111+
RpcError::custom(ErrorCode::InternalError.code(), e.to_string())
112+
})?;
113+
114+
let raw_tx = hex::encode(elements::encode::serialize(&tx));
115+
serialize_result(TxCreateResponse {
116+
raw_tx,
117+
})
118+
}
119+
RpcMethod::TxDecode => {
120+
let req: TxDecodeRequest = parse_params(params)?;
121+
let result =
122+
actions::tx::tx_decode(&req.raw_tx, req.network.unwrap_or(Network::Liquid))
123+
.map_err(|e| {
124+
RpcError::custom(ErrorCode::InternalError.code(), e.to_string())
125+
})?;
126+
127+
serialize_result(result)
128+
}
129+
RpcMethod::KeypairGenerate => {
130+
let result = actions::keypair::keypair_generate();
131+
132+
serialize_result(result)
133+
}
134+
RpcMethod::SimplicityInfo => {
135+
let req: SimplicityInfoRequest = parse_params(params)?;
136+
let result = actions::simplicity::simplicity_info(
137+
&req.program,
138+
req.witness.as_deref(),
139+
req.state.as_deref(),
140+
)
141+
.map_err(|e| RpcError::custom(ErrorCode::InternalError.code(), e.to_string()))?;
142+
143+
serialize_result(result)
144+
}
145+
RpcMethod::SimplicitySighash => {
146+
let req: SimplicitySighashRequest = parse_params(params)?;
147+
// TODO(ivanlele): I don't like this flip flop conversion, maybe there is a better API
148+
let input_utxos = req
149+
.input_utxos
150+
.as_ref()
151+
.map(|v| v.iter().map(String::as_str).collect::<Vec<_>>());
152+
153+
let result = actions::simplicity::simplicity_sighash(
154+
&req.tx,
155+
&req.input_index.to_string(),
156+
&req.cmr,
157+
req.control_block.as_deref(),
158+
req.genesis_hash.as_deref(),
159+
req.secret_key.as_deref(),
160+
req.public_key.as_deref(),
161+
req.signature.as_deref(),
162+
input_utxos.as_deref(),
163+
)
164+
.map_err(|e| RpcError::custom(ErrorCode::InternalError.code(), e.to_string()))?;
165+
serialize_result(result)
166+
}
167+
RpcMethod::PsetCreate => {
168+
let req: PsetCreateRequest = parse_params(params)?;
169+
let result = actions::simplicity::pset::pset_create(&req.inputs, &req.outputs)
170+
.map_err(|e| {
171+
RpcError::custom(ErrorCode::InternalError.code(), e.to_string())
172+
})?;
173+
174+
serialize_result(result)
175+
}
176+
RpcMethod::PsetExtract => {
177+
let req: PsetExtractRequest = parse_params(params)?;
178+
let raw_tx = actions::simplicity::pset::pset_extract(&req.pset).map_err(|e| {
179+
RpcError::custom(ErrorCode::InternalError.code(), e.to_string())
180+
})?;
181+
182+
serialize_result(PsetExtractResponse {
183+
raw_tx,
184+
})
185+
}
186+
RpcMethod::PsetFinalize => {
187+
let req: PsetFinalizeRequest = parse_params(params)?;
188+
let result = actions::simplicity::pset::pset_finalize(
189+
&req.pset,
190+
&req.input_index.to_string(),
191+
&req.program,
192+
&req.witness,
193+
req.genesis_hash.as_deref(),
194+
)
195+
.map_err(|e| RpcError::custom(ErrorCode::InternalError.code(), e.to_string()))?;
196+
197+
serialize_result(result)
198+
}
199+
RpcMethod::PsetRun => {
200+
let req: PsetRunRequest = parse_params(params)?;
201+
let result = actions::simplicity::pset::pset_run(
202+
&req.pset,
203+
&req.input_index.to_string(),
204+
&req.program,
205+
&req.witness,
206+
req.genesis_hash.as_deref(),
207+
)
208+
.map_err(|e| RpcError::custom(ErrorCode::InternalError.code(), e.to_string()))?;
209+
210+
serialize_result(result)
211+
}
212+
RpcMethod::PsetUpdateInput => {
213+
let req: PsetUpdateInputRequest = parse_params(params)?;
214+
let result = actions::simplicity::pset::pset_update_input(
215+
&req.pset,
216+
&req.input_index.to_string(),
217+
&req.input_utxo,
218+
req.internal_key.as_deref(),
219+
req.cmr.as_deref(),
220+
req.state.as_deref(),
221+
)
222+
.map_err(|e| RpcError::custom(ErrorCode::InternalError.code(), e.to_string()))?;
223+
224+
serialize_result(result)
225+
}
226+
}
227+
}
228+
}
229+
230+
impl DefaultRpcHandler {
231+
fn new() -> Self {
232+
Self
233+
}
234+
}
235+
236+
/// Parse parameters from JSON value
237+
fn parse_params<T: serde::de::DeserializeOwned>(params: Option<Value>) -> Result<T, RpcError> {
238+
let params = params.ok_or_else(|| {
239+
RpcError::custom(ErrorCode::InvalidParams.code(), "Missing parameters".to_string())
240+
})?;
241+
242+
serde_json::from_value(params).map_err(|e| {
243+
RpcError::custom(ErrorCode::InvalidParams.code(), format!("Invalid parameters: {}", e))
244+
})
245+
}
246+
247+
/// Serialize result to JSON value
248+
fn serialize_result<T: serde::Serialize>(result: T) -> Result<Value, RpcError> {
249+
serde_json::to_value(result).map_err(|e| {
250+
RpcError::custom(
251+
ErrorCode::InternalError.code(),
252+
format!("Failed to serialize result: {}", e),
253+
)
254+
})
255+
}
256+
257+
/// Create a JSONRPC service with the default handler
258+
pub fn create_service() -> JsonRpcService<DefaultRpcHandler> {
259+
JsonRpcService::new(DefaultRpcHandler::new())
260+
}

0 commit comments

Comments
 (0)