From 69f5664cad672aa79411f4a87e4c0d5c7d9bfc67 Mon Sep 17 00:00:00 2001 From: ivanlele Date: Mon, 5 Jan 2026 13:17:21 +0200 Subject: [PATCH] Add the daemon module and standalone CLI for it --- Cargo.lock | 364 ++++++++++++++++++++++++- Cargo.toml | 21 ++ src/bin/hal-simplicity-daemon/main.rs | 77 ++++++ src/daemon/handler.rs | 260 ++++++++++++++++++ src/daemon/jsonrpc/mod.rs | 373 ++++++++++++++++++++++++++ src/daemon/mod.rs | 178 ++++++++++++ src/daemon/types.rs | 231 ++++++++++++++++ src/lib.rs | 3 + 8 files changed, 1499 insertions(+), 8 deletions(-) create mode 100644 src/bin/hal-simplicity-daemon/main.rs create mode 100644 src/daemon/handler.rs create mode 100644 src/daemon/jsonrpc/mod.rs create mode 100644 src/daemon/mod.rs create mode 100644 src/daemon/types.rs diff --git a/Cargo.lock b/Cargo.lock index 2e9c912..3fa7270 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atty" version = "0.2.11" @@ -180,6 +186,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + [[package]] name = "bumpalo" version = "3.17.0" @@ -192,6 +204,12 @@ version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a019b10a2a7cdeb292db131fc8113e57ea2a908f6e7894b0c3c671893b65dbeb" +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + [[package]] name = "cc" version = "1.2.25" @@ -233,7 +251,7 @@ checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" dependencies = [ "ansi_term", "atty", - "bitflags", + "bitflags 1.0.4", "strsim", "textwrap", "unicode-width", @@ -246,7 +264,7 @@ version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" dependencies = [ - "bitflags", + "bitflags 1.0.4", ] [[package]] @@ -268,6 +286,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "fern" version = "0.5.7" @@ -283,6 +311,21 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + [[package]] name = "getrandom" version = "0.2.8" @@ -326,17 +369,22 @@ dependencies = [ name = "hal-simplicity" version = "0.1.0" dependencies = [ + "chrono", "clap", "elements", "fern", "hal", "hex", + "http-body-util", + "hyper", + "hyper-util", "log", "serde", "serde_json", "serde_yaml", "simplicity-lang", "thiserror", + "tokio", ] [[package]] @@ -366,12 +414,99 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa 1.0.17", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa 1.0.17", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", +] + [[package]] name = "itoa" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b" +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + [[package]] name = "jobserver" version = "0.1.12" @@ -401,9 +536,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.140" +version = "0.2.179" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" [[package]] name = "lightning-invoice" @@ -433,6 +568,15 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.6" @@ -458,6 +602,17 @@ dependencies = [ "bitcoin", ] +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + [[package]] name = "num-integer" version = "0.1.39" @@ -482,6 +637,41 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -657,13 +847,22 @@ version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "423e376fffca3dfa06c9e9790a9ccd282fafb3cc6e6397d01dbf64f9bacc6b85" +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "redox_termios" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" dependencies = [ - "redox_syscall", + "redox_syscall 0.1.51", ] [[package]] @@ -710,6 +909,12 @@ dependencies = [ "regex", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "secp256k1" version = "0.29.1" @@ -780,7 +985,7 @@ version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a23aa71d4a4d43fdbfaac00eff68ba8a06a51759a89ac3304323e800c4dd40d" dependencies = [ - "itoa", + "itoa 0.4.3", "ryu", "serde", ] @@ -809,6 +1014,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + [[package]] name = "simplicity-lang" version = "0.5.0" @@ -837,6 +1052,22 @@ dependencies = [ "cc", ] +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "strsim" version = "0.8.0" @@ -872,7 +1103,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" dependencies = [ "libc", - "redox_syscall", + "redox_syscall 0.1.51", "redox_termios", ] @@ -912,7 +1143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" dependencies = [ "libc", - "redox_syscall", + "redox_syscall 0.1.51", "winapi", ] @@ -931,6 +1162,34 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "unicode-ident" version = "1.0.8" @@ -1043,6 +1302,95 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "yaml-rust" version = "0.4.3" diff --git a/Cargo.toml b/Cargo.toml index 4601593..7bd466b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" @@ -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" @@ -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 diff --git a/src/bin/hal-simplicity-daemon/main.rs b/src/bin/hal-simplicity-daemon/main.rs new file mode 100644 index 0000000..da281a8 --- /dev/null +++ b/src/bin/hal-simplicity-daemon/main.rs @@ -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); + } +} diff --git a/src/daemon/handler.rs b/src/daemon/handler.rs new file mode 100644 index 0000000..2298b8d --- /dev/null +++ b/src/daemon/handler.rs @@ -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 { + 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) -> Result { + 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::>()); + + 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(params: Option) -> Result { + 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(result: T) -> Result { + 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 { + JsonRpcService::new(DefaultRpcHandler::new()) +} diff --git a/src/daemon/jsonrpc/mod.rs b/src/daemon/jsonrpc/mod.rs new file mode 100644 index 0000000..c4f15e1 --- /dev/null +++ b/src/daemon/jsonrpc/mod.rs @@ -0,0 +1,373 @@ +//! Simple JSONRPC 2.0 implementation +//! +//! + +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::fmt; + +/// JSONRPC 2.0 Error codes as defined in the specification +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ErrorCode { + ParseError = -32700, + InvalidRequest = -32600, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603, +} + +impl ErrorCode { + pub fn code(&self) -> i64 { + *self as i64 + } + + pub fn message(&self) -> &str { + match self { + ErrorCode::ParseError => "Parse error", + ErrorCode::InvalidRequest => "Invalid Request", + ErrorCode::MethodNotFound => "Method not found", + ErrorCode::InvalidParams => "Invalid params", + ErrorCode::InternalError => "Internal error", + } + } +} + +/// JSONRPC 2.0 Error object +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RpcError { + pub code: i64, + pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +impl RpcError { + pub fn new(error_code: ErrorCode) -> Self { + Self { + code: error_code.code(), + message: error_code.message().to_string(), + data: None, + } + } + + pub fn with_data(mut self, data: Value) -> Self { + self.data = Some(data); + self + } + + pub fn custom(code: i64, message: String) -> Self { + Self { + code, + message, + data: None, + } + } +} + +impl fmt::Display for RpcError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "RPC Error {}: {}", self.code, self.message) + } +} + +impl std::error::Error for RpcError {} + +/// JSONRPC 2.0 Request object +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RpcRequest { + pub jsonrpc: String, + pub method: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub params: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, +} + +impl RpcRequest { + pub fn new(method: String, params: Option, id: Option) -> Self { + Self { + jsonrpc: "2.0".to_string(), + method, + params, + id, + } + } + + /// Check if this is a notification (no id field) + pub fn is_notification(&self) -> bool { + self.id.is_none() + } + + /// Validate the request according to JSONRPC 2.0 spec + pub fn validate(&self) -> Result<(), RpcError> { + if self.jsonrpc != "2.0" { + return Err(RpcError::new(ErrorCode::InvalidRequest) + .with_data(Value::String("jsonrpc field must be '2.0'".to_string()))); + } + + if self.method.is_empty() { + return Err(RpcError::new(ErrorCode::InvalidRequest) + .with_data(Value::String("method field cannot be empty".to_string()))); + } + + Ok(()) + } +} + +/// JSONRPC 2.0 Response object +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RpcResponse { + pub jsonrpc: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + pub id: Value, +} + +impl RpcResponse { + pub fn success(result: Value, id: Value) -> Self { + Self { + jsonrpc: "2.0".to_string(), + result: Some(result), + error: None, + id, + } + } + + pub fn error(error: RpcError, id: Value) -> Self { + Self { + jsonrpc: "2.0".to_string(), + result: None, + error: Some(error), + id, + } + } +} + +/// Represents either a single request or batch requests +#[derive(Debug)] +pub enum RpcCall { + Single(RpcRequest), + Batch(Vec), +} + +impl RpcCall { + /// Parse a JSON string into an RPC call + pub fn from_json(json: &str) -> Result { + // Try parsing as a single request first + if let Ok(request) = serde_json::from_str::(json) { + request.validate()?; + return Ok(RpcCall::Single(request)); + } + + // Try psrsing as a batch request + match serde_json::from_str::>(json) { + Ok(requests) => { + if requests.is_empty() { + return Err(RpcError::new(ErrorCode::InvalidRequest) + .with_data(Value::String("batch request cannot be empty".to_string()))); + } + + for request in &requests { + request.validate()?; + } + + Ok(RpcCall::Batch(requests)) + } + Err(_) => Err(RpcError::new(ErrorCode::ParseError)), + } + } +} + +/// Represents either a single response or batch responses +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum RpcOutput { + Single(RpcResponse), + Batch(Vec), +} + +impl RpcOutput { + pub fn to_json(&self) -> Result { + serde_json::to_string(self) + } +} + +/// Handler trait for RPC methods +pub trait RpcHandler: Send + Sync { + fn handle(&self, method: &str, params: Option) -> Result; +} + +/// Main JSONRPC service +pub struct JsonRpcService { + handler: H, +} + +impl JsonRpcService { + pub fn new(handler: H) -> Self { + Self { + handler, + } + } + + /// Process a raw JSON string and return a JSON response + pub fn handle_raw(&self, json: &str) -> String { + match RpcCall::from_json(json) { + Ok(call) => match call { + RpcCall::Single(request) => { + let response = self.handle_single(request); + if let Some(resp) = response { + serde_json::to_string(&resp).unwrap_or_else(|_| { + serde_json::to_string(&RpcResponse::error( + RpcError::new(ErrorCode::InternalError), + Value::Null, + )) + .unwrap() + }) + } else { + // Notification - no response + String::new() + } + } + RpcCall::Batch(requests) => { + let responses = self.handle_batch(requests); + if responses.is_empty() { + // All notifications - no response + String::new() + } else { + RpcOutput::Batch(responses).to_json().unwrap_or_else(|_| { + serde_json::to_string(&RpcResponse::error( + RpcError::new(ErrorCode::InternalError), + Value::Null, + )) + .unwrap() + }) + } + } + }, + Err(error) => { + serde_json::to_string(&RpcResponse::error(error, Value::Null)).expect("should ") + } + } + } + + /// Handle a single RPC request + fn handle_single(&self, request: RpcRequest) -> Option { + // Notifications don't get responses + if request.is_notification() { + let _ = self.handler.handle(&request.method, request.params); + return None; + } + + let id = request.id.clone().unwrap_or(Value::Null); + + let response = match self.handler.handle(&request.method, request.params) { + Ok(result) => RpcResponse::success(result, id), + Err(error) => RpcResponse::error(error, id), + }; + + Some(response) + } + + /// Handle a batch of RPC requests + fn handle_batch(&self, requests: Vec) -> Vec { + requests.into_iter().filter_map(|request| self.handle_single(request)).collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct TestHandler; + + impl RpcHandler for TestHandler { + fn handle(&self, method: &str, params: Option) -> Result { + match method { + "echo" => Ok(params.unwrap_or(Value::Null)), + "add" => { + let params = params.ok_or_else(|| RpcError::new(ErrorCode::InvalidParams))?; + let array = + params.as_array().ok_or_else(|| RpcError::new(ErrorCode::InvalidParams))?; + if array.len() != 2 { + return Err(RpcError::new(ErrorCode::InvalidParams)); + } + let a = + array[0].as_i64().ok_or_else(|| RpcError::new(ErrorCode::InvalidParams))?; + let b = + array[1].as_i64().ok_or_else(|| RpcError::new(ErrorCode::InvalidParams))?; + Ok(Value::Number((a + b).into())) + } + _ => Err(RpcError::new(ErrorCode::MethodNotFound)), + } + } + } + + #[test] + fn test_single_request() { + let service = JsonRpcService::new(TestHandler); + let request = r#"{"jsonrpc":"2.0","method":"echo","params":"hello","id":1}"#; + let response = service.handle_raw(request); + assert!(response.contains(r#""result":"hello""#)); + assert!(response.contains(r#""id":1"#)); + } + + #[test] + fn test_notification() { + let service = JsonRpcService::new(TestHandler); + let request = r#"{"jsonrpc":"2.0","method":"echo","params":"hello"}"#; + let response = service.handle_raw(request); + assert_eq!(response, ""); + } + + #[test] + fn test_batch_request() { + let service = JsonRpcService::new(TestHandler); + let request = r#"[ + {"jsonrpc":"2.0","method":"add","params":[1,2],"id":1}, + {"jsonrpc":"2.0","method":"add","params":[3,4],"id":2} + ]"#; + let response = service.handle_raw(request); + assert!(response.contains(r#""result":3"#)); + assert!(response.contains(r#""result":7"#)); + } + + #[test] + fn test_method_not_found() { + let service = JsonRpcService::new(TestHandler); + let request = r#"{"jsonrpc":"2.0","method":"unknown","id":1}"#; + let response = service.handle_raw(request); + assert!(response.contains(r#""code":-32601"#)); + assert!(response.contains("Method not found")); + } + + #[test] + fn test_invalid_json() { + let service = JsonRpcService::new(TestHandler); + let request = r#"{"jsonrpc":"2.0","method":"#; + let response = service.handle_raw(request); + assert!(response.contains(r#""code":-32700"#)); + } + + #[test] + fn test_invalid_request() { + let service = JsonRpcService::new(TestHandler); + let request = r#"{"jsonrpc":"1.0","method":"echo","id":1}"#; + let response = service.handle_raw(request); + assert!(response.contains(r#""code":-32600"#)); + } + + #[test] + fn test_batch_with_notifications() { + let service = JsonRpcService::new(TestHandler); + let request = r#"[ + {"jsonrpc":"2.0","method":"echo","params":"notify"}, + {"jsonrpc":"2.0","method":"add","params":[1,2],"id":1} + ]"#; + let response = service.handle_raw(request); + // Should only have one response (the non-notification) + assert!(response.contains(r#""result":3"#)); + assert!(response.contains(r#""id":1"#)); + } +} diff --git a/src/daemon/mod.rs b/src/daemon/mod.rs new file mode 100644 index 0000000..fee6436 --- /dev/null +++ b/src/daemon/mod.rs @@ -0,0 +1,178 @@ +pub mod handler; +pub mod types; + +pub mod jsonrpc; + +use std::net::SocketAddr; +use std::sync::Arc; + +use http_body_util::{BodyExt, Full}; +use hyper::body::Bytes; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper::{body::Incoming, Method, Request, Response, StatusCode}; +use hyper_util::rt::TokioIo; +use tokio::net::TcpListener; +use tokio::sync::broadcast; + +use thiserror::Error; + +use handler::DefaultRpcHandler; +use jsonrpc::JsonRpcService; + +/// Errors that can occur in the daemon, usually on startup. +#[derive(Error, Debug)] +pub enum DaemonError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("Address parse error: {0}")] + AddrParse(#[from] std::net::AddrParseError), +} + +/// The HAL Simplicity Daemon +/// +/// It listens for JSON-RPC requests over HTTP and handles them. +/// Does not block the current thread when started. Instead, it spawns a new thread. +pub struct HalSimplicityDaemon { + address: SocketAddr, + shutdown_tx: broadcast::Sender<()>, + rpc_service: Arc>, +} + +impl HalSimplicityDaemon { + pub fn new(address: &str) -> Result { + let address: SocketAddr = address.parse()?; + let (shutdown_tx, _) = broadcast::channel(1); + let rpc_service = Arc::new(handler::create_service()); + + Ok(Self { + address, + shutdown_tx, + rpc_service, + }) + } + + /// Core event loop that accepts connections and handles them + async fn run_event_loop( + listener: TcpListener, + rpc_service: Arc>, + mut shutdown_rx: broadcast::Receiver<()>, + ) -> Result<(), DaemonError> { + loop { + tokio::select! { + Ok((stream, _)) = listener.accept() => { + let io = TokioIo::new(stream); + let rpc_service_clone = rpc_service.clone(); + tokio::task::spawn(async move { + http1::Builder::new() + .serve_connection(io, service_fn(move |req| { + handle_request(req, rpc_service_clone.clone()) + })) + .await + }); + } + _ = shutdown_rx.recv() => { + break; + } + } + } + + Ok(()) + } + + /// Start the daemon on a new thread. + /// Useful when you need just to spawn the daemon and continue doing other things in the main thread. + pub fn start(&mut self) -> Result<(), DaemonError> { + let address = self.address; + let shutdown_tx = self.shutdown_tx.clone(); + let rpc_service = self.rpc_service.clone(); + + let runtime = tokio::runtime::Runtime::new()?; + let listener = runtime.block_on(async { TcpListener::bind(&address).await })?; + + std::thread::spawn(move || { + runtime.block_on(async move { + let shutdown_rx = shutdown_tx.subscribe(); + let _ = Self::run_event_loop(listener, rpc_service, shutdown_rx).await; + }); + }); + + Ok(()) + } + + /// Start the daemon and block the current thread, + /// Useful for CLI applications. + pub fn listen_blocking(self) -> Result<(), DaemonError> { + let runtime = tokio::runtime::Runtime::new()?; + + runtime.block_on(async move { + let listener = TcpListener::bind(&self.address).await?; + let shutdown_rx = self.shutdown_tx.subscribe(); + Self::run_event_loop(listener, self.rpc_service, shutdown_rx).await + }) + } + + /// Shutdown the daemon + pub fn shutdown(&self) { + let _ = self.shutdown_tx.send(()); + } +} + +/// Handles an incoming HTTP request and produces a response. +async fn handle_request( + req: Request, + rpc_service: Arc>, +) -> Result>, DaemonError> { + let path = req.uri().path(); + let method = req.method(); + + if method != Method::POST { + return Ok(create_status_response(StatusCode::METHOD_NOT_ALLOWED)); + } + + if path != "/rpc" && path != "/" { + return Ok(create_status_response(StatusCode::NOT_FOUND)); + } + + let body_str = match read_body_as_string(req).await { + Ok(body) => body, + Err(status) => return Ok(create_status_response(status)), + }; + + let response_str = rpc_service.handle_raw(&body_str); + + if response_str.is_empty() { + return Ok(create_status_response(StatusCode::NO_CONTENT)); + } + + Ok(create_json_response(response_str)) +} + +/// Creates an HTTP response with the given status code +fn create_status_response(status: StatusCode) -> Response> { + let body = if status == StatusCode::NO_CONTENT { + Bytes::new() + } else { + Bytes::from(status.canonical_reason().unwrap_or("Unknown Error")) + }; + let mut response = Response::new(Full::new(body)); + *response.status_mut() = status; + response +} + +/// Reads and validates the request body as a UTF-8 string +async fn read_body_as_string(req: Request) -> Result { + let body_bytes = req.collect().await.map_err(|_| StatusCode::BAD_REQUEST)?.to_bytes(); + + String::from_utf8(body_bytes.to_vec()).map_err(|_| StatusCode::BAD_REQUEST) +} + +/// Creates a successful JSON-RPC response +fn create_json_response(body: String) -> Response> { + let mut response = Response::new(Full::new(Bytes::from(body))); + response.headers_mut().insert( + hyper::header::CONTENT_TYPE, + hyper::header::HeaderValue::from_static("application/json"), + ); + response +} diff --git a/src/daemon/types.rs b/src/daemon/types.rs new file mode 100644 index 0000000..cddf6bf --- /dev/null +++ b/src/daemon/types.rs @@ -0,0 +1,231 @@ +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +pub use elements::bitcoin::secp256k1; +pub use elements::hashes::sha256; +pub use simplicity::bitcoin::secp256k1::schnorr; +pub use simplicity::{Amr, Cmr, Ihr}; + +use crate::block::BlockInfo; +use crate::tx::TransactionInfo; +use crate::Network; + +// Custom serialization for Parity as 0 or 1 +mod parity_serde { + use super::*; + + pub fn serialize(parity: &secp256k1::Parity, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u8(parity.to_i32() as u8) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = u8::deserialize(deserializer)?; + secp256k1::Parity::from_i32(value as i32) + .map_err(|_| serde::de::Error::custom(format!("invalid parity value: {}", value))) + } +} + +// Address types +#[derive(Debug, Serialize, Deserialize)] +pub struct AddressCreateRequest { + pub network: Option, + pub pubkey: Option, + pub script: Option, + pub blinder: Option, +} + +pub use crate::address::Addresses as AddressCreateResponse; + +#[derive(Debug, Serialize, Deserialize)] +pub struct AddressInspectRequest { + pub address: String, +} + +pub use crate::address::AddressInfo as AddressInspectResponse; + +// Block types +#[derive(Debug, Serialize, Deserialize)] +pub struct BlockCreateRequest { + pub block_info: BlockInfo, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BlockCreateResponse { + pub raw_block: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BlockDecodeRequest { + pub raw_block: String, + pub network: Option, + pub txids: Option, +} + +pub type BlockDecodeResponse = serde_json::Value; + +// Transaction types +#[derive(Debug, Serialize, Deserialize)] +pub struct TxCreateRequest { + pub tx_info: TransactionInfo, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TxCreateResponse { + pub raw_tx: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TxDecodeRequest { + pub raw_tx: String, + pub network: Option, +} + +pub type TxDecodeResponse = serde_json::Value; + +// Keypair types +#[derive(Debug, Serialize, Deserialize)] +pub struct KeypairGenerateRequest {} + +#[derive(Debug, Serialize, Deserialize)] +pub struct KeypairGenerateResponse { + pub secret: secp256k1::SecretKey, + pub x_only: secp256k1::XOnlyPublicKey, + #[serde(with = "parity_serde")] + pub parity: secp256k1::Parity, +} + +// Simplicity types +#[derive(Debug, Serialize, Deserialize)] +pub struct SimplicityInfoRequest { + pub program: String, + pub witness: Option, + pub state: Option, + pub network: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SimplicityInfoResponse { + pub jets: String, + pub commit_base64: String, + pub commit_decode: String, + pub type_arrow: String, + pub cmr: Cmr, + pub liquid_address_unconf: String, + pub liquid_testnet_address_unconf: String, + pub is_redeem: bool, + pub redeem_info: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RedeemInfo { + pub redeem_base64: String, + pub witness_hex: String, + pub amr: Amr, + pub ihr: Ihr, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SimplicitySighashRequest { + pub tx: String, + pub input_index: u32, + pub cmr: String, + pub control_block: Option, + pub genesis_hash: Option, + pub secret_key: Option, + pub public_key: Option, + pub signature: Option, + pub input_utxos: Option>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct SimplicitySighashResponse { + pub sighash: sha256::Hash, + pub signature: Option, + pub valid_signature: Option, +} + +// PSET types +#[derive(Debug, Serialize, Deserialize)] +pub struct PsetCreateRequest { + pub inputs: String, + pub outputs: String, + pub network: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PsetCreateResponse { + pub pset: String, + pub updated_values: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PsetExtractRequest { + pub pset: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PsetExtractResponse { + pub raw_tx: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PsetFinalizeRequest { + pub pset: String, + pub input_index: u32, + pub program: String, + pub witness: String, + pub genesis_hash: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PsetFinalizeResponse { + pub pset: String, + pub updated_values: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PsetRunRequest { + pub pset: String, + pub input_index: u32, + pub program: String, + pub witness: String, + pub genesis_hash: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PsetRunResponse { + pub success: bool, + pub jets: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct JetCall { + pub jet: String, + pub source_ty: String, + pub target_ty: String, + pub success: bool, + pub input_hex: String, + pub output_hex: String, + pub equality_check: Option<(String, String)>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PsetUpdateInputRequest { + pub pset: String, + pub input_index: u32, + pub input_utxo: String, + pub internal_key: Option, + pub cmr: Option, + pub state: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PsetUpdateInputResponse { + pub pset: String, + pub updated_values: Vec, +} diff --git a/src/lib.rs b/src/lib.rs index f86b934..11c4e75 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,9 @@ pub mod confidential; pub use elements::bitcoin; pub use hal::HexBytes; +#[cfg(feature = "daemon")] +pub mod daemon; + use elements::AddressParams; use serde::{Deserialize, Serialize};