diff --git a/Cargo.lock b/Cargo.lock index 91c17d2..a591bca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.80" @@ -70,6 +85,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "bech32" version = "0.9.1" @@ -173,6 +194,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" + [[package]] name = "byteorder" version = "1.5.0" @@ -211,6 +238,25 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.4", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -286,12 +332,57 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "data-encoding" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "digest" version = "0.10.7" @@ -412,6 +503,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.3" @@ -480,6 +577,35 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -490,6 +616,17 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.2.3" @@ -497,7 +634,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.3", + "serde", ] [[package]] @@ -515,13 +653,22 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "jsonrpc" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8128f36b47411cd3f044be8c1f5cc0c9e24d1d1bfdc45f0a57897b32513053f2" dependencies = [ - "base64", + "base64 0.13.1", "serde", "serde_json", ] @@ -574,6 +721,21 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.16.0" @@ -593,6 +755,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -617,6 +785,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -754,6 +928,7 @@ dependencies = [ "log", "serde", "serde_json", + "serde_with", "silentpayments", "tokio", "tokio-stream", @@ -821,6 +996,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270" +dependencies = [ + "base64 0.21.7", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.3", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.6" @@ -871,6 +1076,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "2.0.49" @@ -930,6 +1141,37 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1023,7 +1265,7 @@ version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" dependencies = [ - "indexmap", + "indexmap 2.2.3", "serde", "serde_spanned", "toml_datetime", @@ -1121,6 +1363,60 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + [[package]] name = "winapi" version = "0.3.9" @@ -1152,13 +1448,22 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -1167,13 +1472,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] @@ -1182,42 +1502,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + [[package]] name = "winnow" version = "0.6.1" diff --git a/Cargo.toml b/Cargo.toml index bdf1d79..0a17f3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ futures-util = { version = "0.3.28", default-features = false, features = ["sink log = "0.4.20" serde = { version = "1.0.193", features = ["derive"]} serde_json = "1.0" +serde_with = "3.6.0" silentpayments = { git = "https://github.com/cygnet3/rust-silentpayments", branch = "master", features = ['utils'] } tokio = { version = "1.0.0", features = ["io-util", "rt-multi-thread", "macros", "sync"] } tokio-stream = "0.1" diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..c49a64b --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; + +type SecretKeyString = String; +type PublicKeyString = String; + +pub const PSBT_SP_PREFIX: &str = "sp"; +pub const PSBT_SP_SUBTYPE: u8 = 0; +pub const PSBT_SP_TWEAK_KEY: &str = "tweak"; +pub const PSBT_SP_ADDRESS_KEY: &str = "address"; + +pub const NUMS: &str = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"; + +pub struct LogEntry { + // pub time_millis: i64, + // pub level: i32, + // pub tag: String, + pub msg: String, +} + +pub struct SyncStatus { + pub peer_count: u32, + pub blockheight: u64, + pub bestblockhash: String, +} diff --git a/src/daemon.rs b/src/daemon.rs index fde0250..c955cc9 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,12 +1,14 @@ -use anyhow::{Context, Result}; +use anyhow::{anyhow, Context, Result, Error}; use bitcoin::{consensus::deserialize, hashes::hex::FromHex}; -use bitcoin::{Amount, BlockHash, Transaction, Txid}; +use bitcoin::{block, Address, Amount, BlockHash, Network, OutPoint, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid}; +use bitcoincore_rpc::json::{CreateRawTransactionInput, ListUnspentQueryOptions, ListUnspentResultEntry}; use bitcoincore_rpc::{json, jsonrpc, Auth, Client, RpcApi}; // use crossbeam_channel::Receiver; // use parking_lot::Mutex; use serde_json::{json, Value}; +use std::collections::HashMap; use std::fs::File; use std::io::Read; use std::path::{Path, PathBuf}; @@ -113,7 +115,7 @@ impl Daemon { let mut rpc = rpc_connect()?; loop { - match rpc_poll(&mut rpc, true) { + match rpc_poll(&mut rpc, false) { PollResult::Done(result) => { result.context("bitcoind RPC polling failed")?; break; // on success, finish polling @@ -161,6 +163,78 @@ impl Daemon { .relay_fee) } + pub(crate) fn get_current_height(&self) -> Result { + Ok(self + .rpc + .get_block_count() + .context("failed to get block count")? + ) + } + + pub(crate) fn list_unspent_from_to(&self, minconf: Option, maxconf: Option) -> Result> { + Ok(self.rpc + .list_unspent( + minconf, + maxconf, + None, + Some(false), + Some(ListUnspentQueryOptions { + maximum_count: Some(5), + ..Default::default() + }) + )?) + } + + pub(crate) fn create_psbt(&self, utxo: ListUnspentResultEntry, spk: ScriptBuf, network: Network) -> Result { + let input = CreateRawTransactionInput { + txid: utxo.txid, + vout: utxo.vout, + sequence: None + }; + let address = Address::from_script(&spk, network)?; + let mut outputs = HashMap::new(); + outputs.insert(address.to_string(), utxo.amount); + let psbt = self.rpc + .create_psbt( + &vec![input], + &outputs, + None, + None + )?; + Ok(psbt.to_string()) + } + + pub(crate) fn process_psbt(&self, psbt: String) -> Result { + let processed_psbt = self.rpc + .wallet_process_psbt( + &psbt, + None, + None, + None + )?; + match processed_psbt.complete { + true => Ok(processed_psbt.psbt), + false => Err(Error::msg("Failed to complete the psbt")) + } + } + + pub(crate) fn finalize_psbt(&self, psbt: String) -> Result> { + let final_tx = self.rpc + .finalize_psbt(&psbt, Some(true))?; + + match final_tx.complete { + true => Ok(final_tx.hex.expect("We shouldn't have an empty tx for a complete return")), + false => Err(Error::msg("Failed to finalize psbt")) + } + } + + pub(crate) fn get_network(&self) -> Result { + let blockchain_info = self.rpc + .get_blockchain_info()?; + + Ok(blockchain_info.chain) + } + pub(crate) fn broadcast(&self, tx: &Transaction) -> Result { self.rpc .send_raw_transaction(tx) diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..10c33da --- /dev/null +++ b/src/db.rs @@ -0,0 +1,55 @@ +use std::{ + fs::{create_dir_all, remove_file, File}, + io::{Read, Write}, + path::PathBuf, + str::FromStr, + env +}; + +use anyhow::{Error, Result}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct FileWriter { + path: PathBuf, +} + +impl FileWriter { + pub fn new(file: &str) -> Result { + match env::var("HOME") { + Ok(home_dir) => { + let mut config_path = PathBuf::from(home_dir); + config_path.push(".4nk"); + config_path.push(file); + + // Create the directory if it doesn't exist + match create_dir_all(&config_path) { + Ok(_) => Ok(Self { path: config_path }), + Err(e) => Err(Error::new(e)), + } + }, + Err(e) => Err(Error::new(e)), + } + } + + pub fn write_to_file(&self, data: &T) -> Result<()> { + let json = serde_json::to_string(data)?; + let mut file = File::create(self.path.clone())?; + file.write_all(json.as_bytes())?; + + Ok(()) + } + + pub fn read_from_file Deserialize<'de>>(&self) -> Result { + let mut file = File::open(self.path.clone())?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + let data: T = serde_json::from_str(&contents)?; + + Ok(data) + } + + pub fn delete(self) -> Result<()> { + remove_file(self.path).map_err(Error::new) + } +} diff --git a/src/main.rs b/src/main.rs index 7c34968..5cdb014 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,23 +3,33 @@ use std::{ env, io::Error as IoError, net::SocketAddr, - sync::{Arc, Mutex}, + sync::{Arc, Mutex, MutexGuard}, }; -use bitcoin::{consensus::deserialize, secp256k1::PublicKey}; +use bitcoin::{consensus::deserialize, key::TapTweak, secp256k1::PublicKey, OutPoint, ScriptBuf, XOnlyPublicKey}; +use bitcoincore_rpc::json as bitcoin_json; use futures_util::{future, pin_mut, stream::TryStreamExt, FutureExt, StreamExt}; use log::{debug, error}; +use silentpayments::sending::SilentPaymentAddress; +use silentpayments::secp256k1::rand::{thread_rng, Rng}; +use spclient::Recipient; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_tungstenite::tungstenite::Message; +use anyhow::{Result, Error}; + mod sp; mod daemon; +mod spclient; +mod constants; +mod db; use crate::daemon::Daemon; use crate::sp::VinData; +use crate::spclient::{SpClient, SpendKey, OutputSpendStatus}; type Tx = UnboundedSender; @@ -80,81 +90,80 @@ fn flatten_msg(parts: &[Vec]) -> Vec { } final_vec -} +} -async fn handle_zmq(peer_map: PeerMap, daemon: Daemon) { +fn process_raw_tx_message(core_msg: &bitcoincore_zmq::Message, daemon: Arc>) -> Result> { + let tx: bitcoin::Transaction = deserialize(&core_msg.serialize_data_to_vec())?; + + if tx.is_coinbase() { + return Err(Error::msg("Can't process coinbase transaction")); + } + + let mut outpoints: Vec<(String, u32)> = Vec::with_capacity(tx.input.len()); + let mut pubkeys: Vec = Vec::with_capacity(tx.input.len()); + for input in tx.input { + outpoints.push((input.previous_output.txid.to_string(), input.previous_output.vout)); + let prev_tx = daemon.lock() + .map_err(|e| Error::msg(format!("Failed to lock the daemon: {}", e)))? + .get_transaction(&input.previous_output.txid, None) + .map_err(|_| Error::msg("Failed to find previous transaction"))?; + + if let Some(output) = prev_tx.output.get(input.previous_output.vout as usize) { + let vin_data = VinData { + script_sig: input.script_sig.to_bytes().to_vec(), + txinwitness: input.witness.to_vec(), + script_pub_key: output.script_pubkey.to_bytes() + }; + match sp::get_pubkey_from_input(&vin_data) { + Ok(Some(pubkey)) => pubkeys.push(pubkey), + Ok(None) => continue, + Err(e) => return Err(Error::msg(format!("Can't extract pubkey from input: {}", e))), + } + } else { + return Err(Error::msg("Transaction with a non-existing input")); + } + } + + let input_pub_keys: Vec<&PublicKey> = pubkeys.iter().collect(); + match silentpayments::utils::receiving::recipient_calculate_tweak_data(&input_pub_keys, &outpoints) { + Ok(partial_tweak) => { + let mut vecs = core_msg.serialize_to_vecs().to_vec(); + vecs.push(partial_tweak.serialize().to_vec()); + Ok(flatten_msg(&vecs)) + }, + Err(e) => Err(Error::msg(format!("Failed to compute tweak data: {}", e.to_string()))) + } +} + +async fn handle_zmq(peer_map: PeerMap, daemon: Arc>) { tokio::task::spawn_blocking(move || { debug!("Starting listening on Core"); for msg in bitcoincore_zmq::subscribe_receiver(&["tcp://127.0.0.1:29000"]).unwrap() { - match msg { - Ok(core_msg) => { - debug!("Received a {} message", core_msg.topic_str()); - let peers = peer_map.lock().unwrap(); - - let payload: Vec; - match core_msg.topic_str() { - "rawtx" => { - let tx: bitcoin::Transaction = deserialize(&core_msg.serialize_data_to_vec()).unwrap(); - - if tx.is_coinbase() { - continue; - } - - let mut outpoints: Vec<(String, u32)> = Vec::with_capacity(tx.input.len()); - let mut pubkeys: Vec = Vec::with_capacity(tx.input.len()); - for input in tx.input { - outpoints.push((input.previous_output.txid.to_string(), input.previous_output.vout)); - let prev_tx = daemon.get_transaction(&input.previous_output.txid, None).unwrap(); - if let Some(output) = prev_tx.output.get(input.previous_output.vout as usize) { - let vin_data = VinData { - script_sig: input.script_sig.to_bytes().to_vec(), - txinwitness: input.witness.to_vec(), - script_pub_key: output.script_pubkey.to_bytes() - }; - match sp::get_pubkey_from_input(&vin_data) { - Ok(res) => { - if let Some(pubkey) = res { - pubkeys.push(pubkey); - } else { - continue; - } - }, - Err(e) => { - log::error!("Can't extract pubkey from input: {}", e.to_string()); - continue; - } - } - } else { - log::error!("Transaction with a non existing input"); - continue; - } - } - let input_pub_keys: Vec<&PublicKey> = pubkeys.iter().collect(); - match silentpayments::utils::receiving::recipient_calculate_tweak_data(&input_pub_keys, &outpoints) { - Ok(partial_tweak) => { - let mut vecs = core_msg.serialize_to_vecs().to_vec(); - vecs.push(partial_tweak.serialize().to_vec()); - payload = flatten_msg(&vecs); - }, - Err(e) => { - log::error!("Failed to compute tweak data: {}", e.to_string()); - continue; - } - } - }, - _ => { - payload = flatten_msg(&core_msg.serialize_to_vecs()); - } - } - - for tx in peers.values() { - let _ = tx.send(Message::Binary(payload.clone())); - } - } + let core_msg = match msg { + Ok(core_msg) => core_msg, Err(e) => { error!("Error receiving ZMQ message: {}", e); continue; } + }; + debug!("Received a {} message", core_msg.topic_str()); + let peers = peer_map.lock().unwrap(); + + let payload: Vec = match core_msg.topic_str() { + "rawtx" => { + let processed = process_raw_tx_message(&core_msg, daemon.clone()); + match processed { + Ok(p) => p, + Err(_) => continue + } + }, + _ => { + flatten_msg(&core_msg.serialize_to_vecs()) + } + }; + + for tx in peers.values() { + let _ = tx.send(Message::Binary(payload.clone())); } } }); @@ -167,14 +176,47 @@ async fn main() -> Result<(), IoError> { let addr = env::args() .nth(1) .unwrap_or_else(|| "127.0.0.1:8080".to_string()); + let wallet_name = env::args() + .nth(2) + .unwrap_or_else(|| "default".to_owned()); + let is_testnet: bool = env::args() + .nth(3) + .unwrap_or_else(|| "true".to_owned()) + .parse() + .expect("Please provide either \"true\" or \"false\""); let state = PeerMap::new(Mutex::new(HashMap::new())); // Connect the rpc daemon let daemon = Daemon::connect().unwrap(); + + let current_tip: u32 = daemon.get_current_height().expect("Failed to make rpc call").try_into().expect("block count is higher than u32::MAX"); + // load an existing sp_wallet, or create a new one + let sp_client = match spclient::SpClient::try_init_from_disk(wallet_name.clone()) { + Ok(existing) => existing, + Err(_) => { + let mut seed = [0u8;64]; + thread_rng().fill(&mut seed); + let (scan_sk, spend_sk) = spclient::derive_keys_from_seed(&seed, is_testnet).expect("Couldn't generate a new sp_wallet"); + SpClient::new( + wallet_name, + scan_sk, + SpendKey::Secret(spend_sk), + None, + current_tip, + is_testnet + ).expect("Failed to create a new SpClient") + } + }; + + log::info!("Using wallet {} with address {}", sp_client.label, sp_client.get_receiving_address()); + + let shared_sp_client = Arc::new(Mutex::new(sp_client)); + let shared_daemon = Arc::new(Mutex::new(daemon)); + // Subscribe to Bitcoin Core - tokio::spawn(handle_zmq(state.clone(), daemon)); + tokio::spawn(handle_zmq(state.clone(), shared_daemon.clone())); // Create the event loop and TCP listener we'll accept connections on. let try_socket = TcpListener::bind(&addr).await; @@ -183,7 +225,7 @@ async fn main() -> Result<(), IoError> { // Let's spawn the handling of each connection in a separate task. while let Ok((stream, addr)) = listener.accept().await { - tokio::spawn(handle_connection(state.clone(), stream, addr)); + tokio::spawn(handle_connection(state.clone(), stream, addr, shared_sp_client.clone(), shared_daemon.clone())); } Ok(()) diff --git a/src/spclient.rs b/src/spclient.rs new file mode 100644 index 0000000..7a3893d --- /dev/null +++ b/src/spclient.rs @@ -0,0 +1,787 @@ +use std::{ + collections::{BTreeMap, HashMap}, + str::FromStr, +}; + +use bitcoin::psbt::{raw, Input, Output}; +use bitcoin::{ + bip32::{DerivationPath, Xpriv}, + consensus::{deserialize, serialize}, + hashes::hex::FromHex, + key::TapTweak, + psbt::PsbtSighashType, + secp256k1::{ + constants::SECRET_KEY_SIZE, Keypair, Message, PublicKey, Scalar, Secp256k1, SecretKey, + ThirtyTwoByteHash, + }, + sighash::{Prevouts, SighashCache}, + taproot::Signature, + Address, Amount, BlockHash, Network, OutPoint, ScriptBuf, TapLeafHash, Transaction, Txid, TxIn, TxOut, Witness, +}; +use log::info; + +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use serde_with::DisplayFromStr; + +use silentpayments::sending::SilentPaymentAddress; +use silentpayments::utils as sp_utils; +use silentpayments::{receiving::Receiver, utils::LabelHash}; +use silentpayments::secp256k1::rand::{thread_rng, prelude::SliceRandom}; + +use anyhow::{Error, Result}; + +use crate::db::FileWriter; +use crate::constants::{NUMS, PSBT_SP_ADDRESS_KEY, PSBT_SP_PREFIX, PSBT_SP_SUBTYPE, PSBT_SP_TWEAK_KEY}; + +pub use bitcoin::psbt::Psbt; + +pub struct ScanProgress { + pub start: u32, + pub current: u32, + pub end: u32, +} + +type SpendingTxId = String; +type MinedInBlock = String; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub enum OutputSpendStatus { + Unspent, + Spent(SpendingTxId), + Mined(MinedInBlock), +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct OwnedOutput { + pub txoutpoint: String, + pub blockheight: u32, + pub tweak: String, + pub amount: u64, + pub script: String, + pub label: Option, + pub spend_status: OutputSpendStatus, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct Recipient { + pub address: String, // either old school or silent payment + pub amount: u64, + pub nb_outputs: u32, // if address is not SP, only 1 is valid +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub enum SpendKey { + Secret(SecretKey), + Public(PublicKey), +} + +#[serde_as] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct SpClient { + pub label: String, + scan_sk: SecretKey, + spend_key: SpendKey, + pub mnemonic: Option, + pub sp_receiver: Receiver, + pub birthday: u32, + pub last_scan: u32, + #[serde_as(as = "HashMap")] + owned: HashMap, + writer: FileWriter, +} + +impl SpClient { + pub fn new( + label: String, + scan_sk: SecretKey, + spend_key: SpendKey, + mnemonic: Option, + birthday: u32, + is_testnet: bool, + ) -> Result { + let secp = Secp256k1::signing_only(); + let scan_pubkey = scan_sk.public_key(&secp); + let sp_receiver: Receiver; + let change_label = LabelHash::from_b_scan_and_m(scan_sk, 0).to_scalar(); + match spend_key { + SpendKey::Public(key) => { + sp_receiver = Receiver::new(0, scan_pubkey, key, change_label.into(), is_testnet)?; + } + SpendKey::Secret(key) => { + let spend_pubkey = key.public_key(&secp); + sp_receiver = Receiver::new( + 0, + scan_pubkey, + spend_pubkey, + change_label.into(), + is_testnet, + )?; + } + } + let writer = FileWriter::new(&label)?; + + Ok(Self { + label, + scan_sk, + spend_key, + mnemonic, + sp_receiver, + birthday, + last_scan: if birthday == 0 { 0 } else { birthday - 1 }, + owned: HashMap::new(), + writer, + }) + } + + pub fn try_init_from_disk(label: String) -> Result { + let empty = SpClient::new( + label, + SecretKey::from_slice(&[1u8; SECRET_KEY_SIZE]).unwrap(), + SpendKey::Secret(SecretKey::from_slice(&[1u8; SECRET_KEY_SIZE]).unwrap()), + None, + 0, + false, + )?; + + empty.retrieve_from_disk() + } + + pub fn update_last_scan(&mut self, scan_height: u32) { + self.last_scan = scan_height; + } + + pub fn get_spendable_amt(&self) -> u64 { + self.owned + .values() + .filter(|x| x.spend_status == OutputSpendStatus::Unspent) + .fold(0, |acc, x| acc + x.amount) + } + + #[allow(dead_code)] + pub fn get_unconfirmed_amt(&self) -> u64 { + self.owned + .values() + .filter(|x| match x.spend_status { + OutputSpendStatus::Spent(_) => true, + _ => false, + }) + .fold(0, |acc, x| acc + x.amount) + } + + pub fn extend_owned(&mut self, owned: Vec<(OutPoint, OwnedOutput)>) { + self.owned.extend(owned); + } + + pub fn check_outpoint_owned(&self, outpoint: OutPoint) -> bool { + self.owned.contains_key(&outpoint) + } + + // pub fn mark_transaction_inputs_as_spent( + // &mut self, + // tx: nakamoto::chain::Transaction, + // ) -> Result<()> { + // let txid = tx.txid(); + + // // note: this currently fails for collaborative transactions + // for input in tx.input { + // self.mark_outpoint_spent(input.previous_output, txid)?; + // } + + // send_amount_update(self.get_spendable_amt()); + + // self.save_to_disk() + // } + + pub fn mark_outpoint_spent(&mut self, outpoint: OutPoint, txid: Txid) -> Result<()> { + if let Some(owned) = self.owned.get_mut(&outpoint) { + match owned.spend_status { + OutputSpendStatus::Unspent => { + info!("marking {} as spent by tx {}", owned.txoutpoint, txid); + owned.spend_status = OutputSpendStatus::Spent(txid.to_string()); + } + _ => return Err(Error::msg("owned outpoint is already spent")), + } + Ok(()) + } else { + Err(anyhow::anyhow!("owned outpoint not found")) + } + } + + pub fn mark_outpoint_mined(&mut self, outpoint: OutPoint, blkhash: BlockHash) -> Result<()> { + if let Some(owned) = self.owned.get_mut(&outpoint) { + match owned.spend_status { + OutputSpendStatus::Mined(_) => { + return Err(Error::msg("owned outpoint is already mined")) + } + _ => { + info!("marking {} as mined in block {}", owned.txoutpoint, blkhash); + owned.spend_status = OutputSpendStatus::Mined(blkhash.to_string()); + } + } + Ok(()) + } else { + Err(anyhow::anyhow!("owned outpoint not found")) + } + } + + pub fn list_outpoints(&self) -> Vec { + self.owned.values().cloned().collect() + } + + pub fn reset_from_blockheight(self, blockheight: u32) -> Self { + let mut new = self.clone(); + new.owned = HashMap::new(); + new.owned = self + .owned + .into_iter() + .filter(|o| o.1.blockheight <= blockheight) + .collect(); + new.last_scan = blockheight; + + new + } + + pub fn save_to_disk(&self) -> Result<()> { + self.writer.write_to_file(self) + } + + pub fn retrieve_from_disk(self) -> Result { + self.writer.read_from_file() + } + + pub fn delete_from_disk(self) -> Result<()> { + self.writer.delete() + } + + pub fn get_receiving_address(&self) -> String { + self.sp_receiver.get_receiving_address() + } + + pub fn get_scan_key(&self) -> SecretKey { + self.scan_sk + } + + pub fn fill_sp_outputs(&self, psbt: &mut Psbt) -> Result<()> { + let b_spend = match self.spend_key { + SpendKey::Secret(key) => key, + SpendKey::Public(_) => return Err(Error::msg("Watch-only wallet, can't spend")), + }; + + let mut input_privkeys: Vec = vec![]; + for (i, input) in psbt.inputs.iter().enumerate() { + if let Some(tweak) = input.proprietary.get(&raw::ProprietaryKey { + prefix: PSBT_SP_PREFIX.as_bytes().to_vec(), + subtype: PSBT_SP_SUBTYPE, + key: PSBT_SP_TWEAK_KEY.as_bytes().to_vec(), + }) { + let mut buffer = [0u8; 32]; + if tweak.len() != 32 { + return Err(Error::msg(format!("Invalid tweak at input {}", i))); + } + buffer.copy_from_slice(tweak.as_slice()); + let scalar = Scalar::from_be_bytes(buffer)?; + input_privkeys.push(b_spend.add_tweak(&scalar)?); + } else { + // For now all inputs belong to us + return Err(Error::msg(format!("Missing tweak at input {}", i))); + } + } + + let a_sum = Self::get_a_sum_secret_keys(&input_privkeys); + let outpoints: Vec<(String, u32)> = psbt + .unsigned_tx + .input + .iter() + .map(|i| { + let prev_out = i.previous_output; + (prev_out.txid.to_string(), prev_out.vout) + }) + .collect(); + let outpoints_hash: Scalar = + sp_utils::hash_outpoints(&outpoints, a_sum.public_key(&Secp256k1::signing_only()))?; + let partial_secret = + sp_utils::sending::sender_calculate_partial_secret(a_sum, outpoints_hash)?; + + // get all the silent addresses + let mut sp_addresses: Vec = Vec::with_capacity(psbt.outputs.len()); + for output in psbt.outputs.iter() { + // get the sp address from psbt + if let Some(value) = output.proprietary.get(&raw::ProprietaryKey { + prefix: PSBT_SP_PREFIX.as_bytes().to_vec(), + subtype: PSBT_SP_SUBTYPE, + key: PSBT_SP_ADDRESS_KEY.as_bytes().to_vec(), + }) { + let sp_address = SilentPaymentAddress::try_from(deserialize::(value)?)?; + sp_addresses.push(sp_address.into()); + } else { + // Not a sp output + continue; + } + } + + let mut sp_address2xonlypubkeys = + silentpayments::sending::generate_multiple_recipient_pubkeys( + sp_addresses, + partial_secret, + )?; + for (i, output) in psbt.unsigned_tx.output.iter_mut().enumerate() { + // get the sp address from psbt + let output_data = &psbt.outputs[i]; + if let Some(value) = output_data.proprietary.get(&raw::ProprietaryKey { + prefix: PSBT_SP_PREFIX.as_bytes().to_vec(), + subtype: PSBT_SP_SUBTYPE, + key: PSBT_SP_ADDRESS_KEY.as_bytes().to_vec(), + }) { + let sp_address = SilentPaymentAddress::try_from(deserialize::(value)?)?; + if let Some(xonlypubkeys) = sp_address2xonlypubkeys.get_mut(&sp_address.to_string()) + { + if !xonlypubkeys.is_empty() { + let output_key = xonlypubkeys.remove(0); // actually we could randomize it + // update the script pubkey + output.script_pubkey = + ScriptBuf::new_p2tr_tweaked(output_key.dangerous_assume_tweaked()); + } else { + return Err(Error::msg(format!( + "We're missing a key for address {}", + sp_address + ))); + } + } else { + return Err(Error::msg(format!("Can't find address {}", sp_address))); + } + } else { + // Not a sp output + continue; + } + } + Ok(()) + } + + pub fn set_fees(psbt: &mut Psbt, fee_rate: u32, payer: String) -> Result<()> { + let payer_vouts: Vec = match SilentPaymentAddress::try_from(payer.clone()) { + Ok(sp_address) => psbt + .outputs + .iter() + .enumerate() + .filter_map(|(i, o)| { + if let Some(value) = o.proprietary.get(&raw::ProprietaryKey { + prefix: PSBT_SP_PREFIX.as_bytes().to_vec(), + subtype: PSBT_SP_SUBTYPE, + key: PSBT_SP_ADDRESS_KEY.as_bytes().to_vec(), + }) { + let candidate = + SilentPaymentAddress::try_from(deserialize::(value).unwrap()) + .unwrap(); + if sp_address == candidate { + Some(i as u32) + } else { + None + } + } else { + None + } + }) + .collect(), + Err(_) => { + let address = Address::from_str(&payer)?; + let spk = address.assume_checked().script_pubkey(); + psbt.unsigned_tx + .output + .iter() + .enumerate() + .filter_map(|(i, o)| { + if o.script_pubkey == spk { + Some(i as u32) + } else { + None + } + }) + .collect() // Actually we should have only one output for normal address + } + }; + + if payer_vouts.is_empty() { + return Err(Error::msg("Payer is not part of this transaction")); + } + + // check against the total amt in inputs + let total_input_amt: u64 = psbt + .iter_funding_utxos() + .try_fold(0u64, |sum, utxo_result| { + utxo_result.map(|utxo| sum + utxo.value.to_sat()) + })?; + + // total amt in outputs should be equal + let total_output_amt: u64 = psbt + .unsigned_tx + .output + .iter() + .fold(0, |sum, add| sum + add.value.to_sat()); + + let dust = total_input_amt - total_output_amt; + + // now compute the size of the tx + let fake = Self::sign_psbt_fake(psbt); + let vsize = fake.vsize(); + + // absolut amount of fees + let fee_amt: u64 = (fee_rate * vsize as u32).into(); + + // now deduce the fees from one of the payer outputs + // TODO deduce fee from the change address + if fee_amt > dust { + let mut rng = thread_rng(); + if let Some(deduce_from) = payer_vouts.choose(&mut rng) { + let output = &mut psbt.unsigned_tx.output[*deduce_from as usize]; + let old_value = output.value; + output.value = old_value - Amount::from_sat(fee_amt - dust); // account for eventual dust + } else { + return Err(Error::msg("no payer vout")); + } + } + + Ok(()) + } + + pub fn create_new_psbt( + &self, + inputs: Vec, + mut recipients: Vec, + payload: Option<&[u8]> + ) -> Result { + let mut tx_in: Vec = vec![]; + let mut inputs_data: Vec<(ScriptBuf, u64, Scalar)> = vec![]; + let mut total_input_amount = 0u64; + let mut total_output_amount = 0u64; + + for i in inputs { + tx_in.push(TxIn { + previous_output: bitcoin::OutPoint::from_str(&i.txoutpoint)?, + script_sig: ScriptBuf::new(), + sequence: bitcoin::Sequence::MAX, + witness: bitcoin::Witness::new(), + }); + + let scalar = Scalar::from_be_bytes(FromHex::from_hex(&i.tweak)?)?; + + total_input_amount += i.amount; + + inputs_data.push((ScriptBuf::from_hex(&i.script)?, i.amount, scalar)); + } + + // We could compute the outputs key right away, + // but keeping things separated may be interesting, + // for example creating transactions in a watch-only wallet + // and using another signer + let placeholder_spk = ScriptBuf::new_p2tr_tweaked( + bitcoin::XOnlyPublicKey::from_str(NUMS)?.dangerous_assume_tweaked(), + ); + + let _outputs: Result> = recipients + .iter() + .map(|o| { + let script_pubkey: ScriptBuf; + + match SilentPaymentAddress::try_from(o.address.as_str()) { + Ok(sp_address) => { + if self.sp_receiver.is_testnet != sp_address.is_testnet() { + return Err(Error::msg(format!( + "Wrong network for address {}", + sp_address + ))); + } + + script_pubkey = placeholder_spk.clone(); + } + Err(_) => { + let unchecked_address = Address::from_str(&o.address)?; // TODO: handle better garbage string + + let address_is_testnet = match *unchecked_address.network() { + Network::Bitcoin => false, + _ => true, + }; + + if self.sp_receiver.is_testnet != address_is_testnet { + return Err(Error::msg(format!( + "Wrong network for address {}", + unchecked_address.assume_checked() + ))); + } + + script_pubkey = ScriptBuf::from_bytes( + unchecked_address + .assume_checked() + .script_pubkey() + .to_bytes(), + ); + } + } + + total_output_amount += o.amount; + + Ok(TxOut { + value: Amount::from_sat(o.amount), + script_pubkey, + }) + }) + .collect(); + + let mut outputs = _outputs?; + + let change_amt = total_input_amount - total_output_amount; + + // Add change output + let change_address = self.sp_receiver.get_change_address(); + + outputs.push(TxOut { + value: Amount::from_sat(change_amt), + script_pubkey: placeholder_spk, + }); + + recipients.push(Recipient { + address: change_address, + amount: change_amt, + nb_outputs: 1, + }); + + if let Some(data) = payload { + if data.len() > 40 { + return Err(Error::msg("Payload must be max 40B")); + } + let mut op_return = bitcoin::script::PushBytesBuf::new(); + op_return.extend_from_slice(data); + outputs.push(TxOut { + value: Amount::from_sat(0), + script_pubkey: ScriptBuf::new_op_return(op_return), + }); + } + + let tx = bitcoin::Transaction { + version: bitcoin::transaction::Version(2), + lock_time: bitcoin::absolute::LockTime::ZERO, + input: tx_in, + output: outputs, + }; + + let mut psbt = Psbt::from_unsigned_tx(tx)?; + + // Add the witness utxo to the input in psbt + for (i, input_data) in inputs_data.iter().enumerate() { + let (script_pubkey, value, tweak) = input_data; + let witness_txout = TxOut { + value: Amount::from_sat(*value), + script_pubkey: script_pubkey.clone(), + }; + let mut psbt_input = Input { + witness_utxo: Some(witness_txout), + ..Default::default() + }; + psbt_input.proprietary.insert( + raw::ProprietaryKey { + prefix: PSBT_SP_PREFIX.as_bytes().to_vec(), + subtype: PSBT_SP_SUBTYPE, + key: PSBT_SP_TWEAK_KEY.as_bytes().to_vec(), + }, + tweak.to_be_bytes().to_vec(), + ); + psbt.inputs[i] = psbt_input; + } + + for (i, recipient) in recipients.iter().enumerate() { + if let Ok(sp_address) = SilentPaymentAddress::try_from(recipient.address.as_str()) { + // Add silentpayment address to the output + let mut psbt_output = Output { + ..Default::default() + }; + psbt_output.proprietary.insert( + raw::ProprietaryKey { + prefix: PSBT_SP_PREFIX.as_bytes().to_vec(), + subtype: PSBT_SP_SUBTYPE, + key: PSBT_SP_ADDRESS_KEY.as_bytes().to_vec(), + }, + serialize(&sp_address.to_string()), + ); + psbt.outputs[i] = psbt_output; + } else { + // Regular address, we don't need to add more data + continue; + } + } + + Ok(psbt) + } + + pub fn get_a_sum_secret_keys(input: &Vec) -> SecretKey { + let secp = Secp256k1::new(); + + let mut negated_keys: Vec = vec![]; + + for key in input { + let (_, parity) = key.x_only_public_key(&secp); + + if parity == bitcoin::secp256k1::Parity::Odd { + negated_keys.push(key.negate()); + } else { + negated_keys.push(*key); + } + } + + let (head, tail) = negated_keys.split_first().unwrap(); + + let result: SecretKey = tail + .iter() + .fold(*head, |acc, &item| acc.add_tweak(&item.into()).unwrap()); + + result + } + + fn taproot_sighash< + T: std::ops::Deref + std::borrow::Borrow, + >( + input: &Input, + prevouts: &Vec<&TxOut>, + input_index: usize, + cache: &mut SighashCache, + tapleaf_hash: Option, + ) -> Result<(Message, PsbtSighashType), Error> { + let prevouts = Prevouts::All(prevouts); + + let hash_ty = input + .sighash_type + .map(|ty| ty.taproot_hash_ty()) + .unwrap_or(Ok(bitcoin::TapSighashType::Default))?; + + let sighash = match tapleaf_hash { + Some(leaf_hash) => cache.taproot_script_spend_signature_hash( + input_index, + &prevouts, + leaf_hash, + hash_ty, + )?, + None => cache.taproot_key_spend_signature_hash(input_index, &prevouts, hash_ty)?, + }; + let msg = Message::from_digest(sighash.into_32()); + Ok((msg, hash_ty.into())) + } + + // Sign a transaction with garbage, used for easier fee estimation + fn sign_psbt_fake(psbt: &Psbt) -> Transaction { + let mut fake_psbt = psbt.clone(); + + let fake_sig = [1u8; 64]; + + for i in fake_psbt.inputs.iter_mut() { + i.tap_key_sig = Some(Signature::from_slice(&fake_sig).unwrap()); + } + + Self::finalize_psbt(&mut fake_psbt).unwrap(); + + fake_psbt.extract_tx().expect("Invalid fake tx") + } + + pub fn sign_psbt(&self, psbt: Psbt) -> Result { + let b_spend = match self.spend_key { + SpendKey::Secret(key) => key, + SpendKey::Public(_) => return Err(Error::msg("Watch-only wallet, can't spend")), + }; + + let mut cache = SighashCache::new(&psbt.unsigned_tx); + + let mut prevouts: Vec<&TxOut> = vec![]; + + for input in &psbt.inputs { + if let Some(witness_utxo) = &input.witness_utxo { + prevouts.push(witness_utxo); + } + } + + let mut signed_psbt = psbt.clone(); + + let secp = Secp256k1::signing_only(); + + for (i, input) in psbt.inputs.iter().enumerate() { + let tap_leaf_hash: Option = None; + + let (msg, sighash_ty) = + Self::taproot_sighash(input, &prevouts, i, &mut cache, tap_leaf_hash)?; + + // Construct the signing key + let tweak = input.proprietary.get(&raw::ProprietaryKey { + prefix: PSBT_SP_PREFIX.as_bytes().to_vec(), + subtype: PSBT_SP_SUBTYPE, + key: PSBT_SP_TWEAK_KEY.as_bytes().to_vec(), + }); + + if tweak.is_none() { + panic!("Missing tweak") + }; + + let tweak = SecretKey::from_slice(tweak.unwrap().as_slice()).unwrap(); + + let sk = b_spend.add_tweak(&tweak.into())?; + + let keypair = Keypair::from_secret_key(&secp, &sk); + + let sig = secp.sign_schnorr_with_rng(&msg, &keypair, &mut thread_rng()); + + signed_psbt.inputs[i].tap_key_sig = Some(Signature { + sig, + hash_ty: sighash_ty.taproot_hash_ty()?, + }); + } + + Ok(signed_psbt) + } + + pub(crate) fn finalize_psbt(psbt: &mut Psbt) -> Result<()> { + psbt.inputs.iter_mut().for_each(|i| { + let mut script_witness = Witness::new(); + if let Some(sig) = i.tap_key_sig { + script_witness.push(sig.to_vec()); + } else { + panic!("Missing signature"); + } + + i.final_script_witness = Some(script_witness); + + // Clear all the data fields as per the spec. + i.tap_key_sig = None; + i.partial_sigs = BTreeMap::new(); + i.sighash_type = None; + i.redeem_script = None; + i.witness_script = None; + i.bip32_derivation = BTreeMap::new(); + }); + Ok(()) + } +} + +pub fn derive_keys_from_seed(seed: &[u8], is_testnet: bool) -> Result<(SecretKey, SecretKey)> { + let network = if is_testnet { + Network::Testnet + } else { + Network::Bitcoin + }; + + let xprv = Xpriv::new_master(network, seed)?; + + let (scan_privkey, spend_privkey) = derive_keys_from_xprv(xprv)?; + + Ok((scan_privkey, spend_privkey)) +} + +fn derive_keys_from_xprv(xprv: Xpriv) -> Result<(SecretKey, SecretKey)> { + let (scan_path, spend_path) = match xprv.network { + bitcoin::Network::Bitcoin => ("m/352h/0h/0h/1h/0", "m/352h/0h/0h/0h/0"), + _ => ("m/352h/1h/0h/1h/0", "m/352h/1h/0h/0h/0"), + }; + + let secp = Secp256k1::signing_only(); + let scan_path = DerivationPath::from_str(scan_path)?; + let spend_path = DerivationPath::from_str(spend_path)?; + let scan_privkey = xprv.derive_priv(&secp, &scan_path)?.private_key; + let spend_privkey = xprv.derive_priv(&secp, &spend_path)?.private_key; + + Ok((scan_privkey, spend_privkey)) +}