Skip to content
Open
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
364 changes: 356 additions & 8 deletions Cargo.lock

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ repository = "https://github.com/BlockstreamResearch/hal-simplicity/"
description = "hal-simplicity: a Simplicity extension of hal"
keywords = [ "crypto", "bitcoin", "elements", "liquid", "simplicity" ]
readme = "README.md"
default-run = "hal-simplicity"

[lib]
name = "hal_simplicity"
Expand All @@ -18,6 +19,19 @@ path = "src/lib.rs"
name = "hal-simplicity"
path = "src/bin/hal-simplicity/main.rs"

[[bin]]
name = "hal-simplicity-daemon"
path = "src/bin/hal-simplicity-daemon/main.rs"

[features]
default = []
daemon = [
"dep:chrono",
"dep:hyper",
"dep:hyper-util",
"dep:http-body-util",
"dep:tokio",
]

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

# Daemon-only dependencies
chrono = { version = "0.4", optional = true }
hyper = { version = "1.8.1", features = ["server", "http1"], optional = true }
hyper-util = { version = "0.1", features = ["tokio"], optional = true }
http-body-util = { version = "0.1", optional = true }
tokio = { version = "1.48.0", features = ["full"], optional = true }

[lints.clippy]
# Exclude lints we don't think are valuable.
needless_question_mark = "allow" # https://github.com/rust-bitcoin/rust-bitcoin/pull/2134
Expand Down
77 changes: 77 additions & 0 deletions src/bin/hal-simplicity-daemon/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#[cfg(not(feature = "daemon"))]
fn main() {
eprintln!("hal-simplicity-daemon can only be built with the 'daemon' feature enabled");
std::process::exit(1);
}

#[cfg(feature = "daemon")]
fn main() {
use hal_simplicity::daemon::HalSimplicityDaemon;

/// Default address for the TCP listener
const DEFAULT_ADDRESS: &str = "127.0.0.1:28579";

/// Setup logging with the given log level.
fn setup_logger(lvl: log::LevelFilter) {
fern::Dispatch::new()
.format(|out, message, _record| out.finish(format_args!("{}", message)))
.level(lvl)
.chain(std::io::stderr())
.apply()
.expect("error setting up logger");
}

/// Create the main app object.
fn init_app<'a, 'b>() -> clap::App<'a, 'b> {
clap::App::new("hal-simplicity-daemon")
.bin_name("hal-simplicity-daemon")
.version(clap::crate_version!())
.about("hal-simplicity-daemon -- JSON-RPC daemon for Simplicity operations")
.arg(
clap::Arg::with_name("address")
.short("a")
.long("address")
.value_name("ADDRESS")
.help("TCP address to bind to (default: 127.0.0.1:28579)")
.takes_value(true),
)
.arg(
clap::Arg::with_name("verbose")
.short("v")
.long("verbose")
.help("Enable verbose logging output to stderr")
.takes_value(false),
)
}

let app = init_app();
let matches = app.get_matches();

// Enable logging in verbose mode.
match matches.is_present("verbose") {
true => setup_logger(log::LevelFilter::Debug),
false => setup_logger(log::LevelFilter::Info),
}

// Get the address from command line or use default
let address = matches.value_of("address").unwrap_or(DEFAULT_ADDRESS);

log::info!("Starting hal-simplicity-daemon on {}...", address);

// Create the daemon
let daemon = match HalSimplicityDaemon::new(address) {
Ok(d) => d,
Err(e) => {
log::error!("Failed to create daemon: {}", e);

std::process::exit(1);
}
};

// Start the daemon and block
if let Err(e) = daemon.listen_blocking() {
log::error!("Daemon error: {}", e);

std::process::exit(1);
}
}
260 changes: 260 additions & 0 deletions src/daemon/handler.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
use std::str::FromStr;

use super::jsonrpc::{ErrorCode, JsonRpcService, RpcError, RpcHandler};
use serde_json::Value;

use super::types::*;
use crate::actions;

use crate::Network;

/// RPC method names
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RpcMethod {
AddressCreate,
AddressInspect,
BlockCreate,
BlockDecode,
TxCreate,
TxDecode,
KeypairGenerate,
SimplicityInfo,
SimplicitySighash,
PsetCreate,
PsetExtract,
PsetFinalize,
PsetRun,
PsetUpdateInput,
}

impl FromStr for RpcMethod {
type Err = RpcError;

fn from_str(s: &str) -> Result<Self, RpcError> {
let method = match s {
"address_create" => Self::AddressCreate,
"address_inspect" => Self::AddressInspect,
"block_create" => Self::BlockCreate,
"block_decode" => Self::BlockDecode,
"tx_create" => Self::TxCreate,
"tx_decode" => Self::TxDecode,
"keypair_generate" => Self::KeypairGenerate,
"simplicity_info" => Self::SimplicityInfo,
"simplicity_sighash" => Self::SimplicitySighash,
"pset_create" => Self::PsetCreate,
"pset_extract" => Self::PsetExtract,
"pset_finalize" => Self::PsetFinalize,
"pset_run" => Self::PsetRun,
"pset_update_input" => Self::PsetUpdateInput,
_ => return Err(RpcError::new(ErrorCode::MethodNotFound)),
};

Ok(method)
}
}

/// Default RPC handler that provides basic methods
#[derive(Default)]
pub struct DefaultRpcHandler;

impl RpcHandler for DefaultRpcHandler {
fn handle(&self, method: &str, params: Option<Value>) -> Result<Value, RpcError> {
let rpc_method = RpcMethod::from_str(method)?;

match rpc_method {
RpcMethod::AddressCreate => {
let req: AddressCreateRequest = parse_params(params)?;
let result = actions::address::address_create(
req.pubkey.as_deref(),
req.script.as_deref(),
req.blinder.as_deref(),
req.network.unwrap_or(Network::Liquid),
)
.map_err(|e| RpcError::custom(ErrorCode::InternalError.code(), e.to_string()))?;

serialize_result(result)
}
RpcMethod::AddressInspect => {
let req: AddressInspectRequest = parse_params(params)?;
let result = actions::address::address_inspect(&req.address).map_err(|e| {
RpcError::custom(ErrorCode::InternalError.code(), e.to_string())
})?;

serialize_result(result)
}
RpcMethod::BlockCreate => {
let req: BlockCreateRequest = parse_params(params)?;

let block = actions::block::block_create(req.block_info).map_err(|e| {
RpcError::custom(ErrorCode::InternalError.code(), e.to_string())
})?;

let raw_block = hex::encode(elements::encode::serialize(&block));
serialize_result(BlockCreateResponse {
raw_block,
})
}
RpcMethod::BlockDecode => {
let req: BlockDecodeRequest = parse_params(params)?;
let result = actions::block::block_decode(
&req.raw_block,
req.network.unwrap_or(Network::Liquid),
req.txids.unwrap_or(false),
)
.map_err(|e| RpcError::custom(ErrorCode::InternalError.code(), e.to_string()))?;

serialize_result(result)
}
RpcMethod::TxCreate => {
let req: TxCreateRequest = parse_params(params)?;
let tx = actions::tx::tx_create(req.tx_info).map_err(|e| {
RpcError::custom(ErrorCode::InternalError.code(), e.to_string())
})?;

let raw_tx = hex::encode(elements::encode::serialize(&tx));
serialize_result(TxCreateResponse {
raw_tx,
})
}
RpcMethod::TxDecode => {
let req: TxDecodeRequest = parse_params(params)?;
let result =
actions::tx::tx_decode(&req.raw_tx, req.network.unwrap_or(Network::Liquid))
.map_err(|e| {
RpcError::custom(ErrorCode::InternalError.code(), e.to_string())
})?;

serialize_result(result)
}
RpcMethod::KeypairGenerate => {
let result = actions::keypair::keypair_generate();

serialize_result(result)
}
RpcMethod::SimplicityInfo => {
let req: SimplicityInfoRequest = parse_params(params)?;
let result = actions::simplicity::simplicity_info(
&req.program,
req.witness.as_deref(),
req.state.as_deref(),
)
.map_err(|e| RpcError::custom(ErrorCode::InternalError.code(), e.to_string()))?;

serialize_result(result)
}
RpcMethod::SimplicitySighash => {
let req: SimplicitySighashRequest = parse_params(params)?;
// TODO(ivanlele): I don't like this flip flop conversion, maybe there is a better API
let input_utxos = req
.input_utxos
.as_ref()
.map(|v| v.iter().map(String::as_str).collect::<Vec<_>>());

let result = actions::simplicity::simplicity_sighash(
&req.tx,
&req.input_index.to_string(),
&req.cmr,
req.control_block.as_deref(),
req.genesis_hash.as_deref(),
req.secret_key.as_deref(),
req.public_key.as_deref(),
req.signature.as_deref(),
input_utxos.as_deref(),
)
.map_err(|e| RpcError::custom(ErrorCode::InternalError.code(), e.to_string()))?;
serialize_result(result)
}
RpcMethod::PsetCreate => {
let req: PsetCreateRequest = parse_params(params)?;
let result = actions::simplicity::pset::pset_create(&req.inputs, &req.outputs)
.map_err(|e| {
RpcError::custom(ErrorCode::InternalError.code(), e.to_string())
})?;

serialize_result(result)
}
RpcMethod::PsetExtract => {
let req: PsetExtractRequest = parse_params(params)?;
let raw_tx = actions::simplicity::pset::pset_extract(&req.pset).map_err(|e| {
RpcError::custom(ErrorCode::InternalError.code(), e.to_string())
})?;

serialize_result(PsetExtractResponse {
raw_tx,
})
}
RpcMethod::PsetFinalize => {
let req: PsetFinalizeRequest = parse_params(params)?;
let result = actions::simplicity::pset::pset_finalize(
&req.pset,
&req.input_index.to_string(),
&req.program,
&req.witness,
req.genesis_hash.as_deref(),
)
.map_err(|e| RpcError::custom(ErrorCode::InternalError.code(), e.to_string()))?;

serialize_result(result)
}
RpcMethod::PsetRun => {
let req: PsetRunRequest = parse_params(params)?;
let result = actions::simplicity::pset::pset_run(
&req.pset,
&req.input_index.to_string(),
&req.program,
&req.witness,
req.genesis_hash.as_deref(),
)
.map_err(|e| RpcError::custom(ErrorCode::InternalError.code(), e.to_string()))?;

serialize_result(result)
}
RpcMethod::PsetUpdateInput => {
let req: PsetUpdateInputRequest = parse_params(params)?;
let result = actions::simplicity::pset::pset_update_input(
&req.pset,
&req.input_index.to_string(),
&req.input_utxo,
req.internal_key.as_deref(),
req.cmr.as_deref(),
req.state.as_deref(),
)
.map_err(|e| RpcError::custom(ErrorCode::InternalError.code(), e.to_string()))?;

serialize_result(result)
}
}
}
}

impl DefaultRpcHandler {
fn new() -> Self {
Self
}
}

/// Parse parameters from JSON value
fn parse_params<T: serde::de::DeserializeOwned>(params: Option<Value>) -> Result<T, RpcError> {
let params = params.ok_or_else(|| {
RpcError::custom(ErrorCode::InvalidParams.code(), "Missing parameters".to_string())
})?;

serde_json::from_value(params).map_err(|e| {
RpcError::custom(ErrorCode::InvalidParams.code(), format!("Invalid parameters: {}", e))
})
}

/// Serialize result to JSON value
fn serialize_result<T: serde::Serialize>(result: T) -> Result<Value, RpcError> {
serde_json::to_value(result).map_err(|e| {
RpcError::custom(
ErrorCode::InternalError.code(),
format!("Failed to serialize result: {}", e),
)
})
}

/// Create a JSONRPC service with the default handler
pub fn create_service() -> JsonRpcService<DefaultRpcHandler> {
JsonRpcService::new(DefaultRpcHandler::new())
}
Loading