Add the sp wallet + daemon logic, some refactoring

This commit is contained in:
Sosthene00 2024-03-08 15:31:23 +01:00
parent 2d044ec2c2
commit 5f4efa5aa3
7 changed files with 1430 additions and 85 deletions

384
Cargo.lock generated
View File

@ -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"

View File

@ -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"

24
src/constants.rs Normal file
View File

@ -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,
}

View File

@ -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<u64> {
Ok(self
.rpc
.get_block_count()
.context("failed to get block count")?
)
}
pub(crate) fn list_unspent_from_to(&self, minconf: Option<usize>, maxconf: Option<usize>) -> Result<Vec<json::ListUnspentResultEntry>> {
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<String> {
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<String> {
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<Vec<u8>> {
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<Network> {
let blockchain_info = self.rpc
.get_blockchain_info()?;
Ok(blockchain_info.chain)
}
pub(crate) fn broadcast(&self, tx: &Transaction) -> Result<Txid> {
self.rpc
.send_raw_transaction(tx)

55
src/db.rs Normal file
View File

@ -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<Self> {
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<T: Serialize>(&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<T: for<'de> Deserialize<'de>>(&self) -> Result<T> {
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)
}
}

View File

@ -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<Message>;
@ -80,81 +90,80 @@ fn flatten_msg(parts: &[Vec<u8>]) -> Vec<u8> {
}
final_vec
}
}
async fn handle_zmq(peer_map: PeerMap, daemon: Daemon) {
fn process_raw_tx_message(core_msg: &bitcoincore_zmq::Message, daemon: Arc<Mutex<Daemon>>) -> Result<Vec<u8>> {
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<PublicKey> = 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<Mutex<Daemon>>) {
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<u8>;
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<PublicKey> = 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<u8> = 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(())

787
src/spclient.rs Normal file
View File

@ -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<String>,
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<String>,
pub sp_receiver: Receiver,
pub birthday: u32,
pub last_scan: u32,
#[serde_as(as = "HashMap<DisplayFromStr, _>")]
owned: HashMap<OutPoint, OwnedOutput>,
writer: FileWriter,
}
impl SpClient {
pub fn new(
label: String,
scan_sk: SecretKey,
spend_key: SpendKey,
mnemonic: Option<String>,
birthday: u32,
is_testnet: bool,
) -> Result<Self> {
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<SpClient> {
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<OwnedOutput> {
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> {
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<SecretKey> = 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<String> = 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::<String>(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::<String>(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<u32> = 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::<String>(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<OwnedOutput>,
mut recipients: Vec<Recipient>,
payload: Option<&[u8]>
) -> Result<Psbt> {
let mut tx_in: Vec<bitcoin::TxIn> = 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<Vec<bitcoin::TxOut>> = 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>) -> SecretKey {
let secp = Secp256k1::new();
let mut negated_keys: Vec<SecretKey> = 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<Target = Transaction> + std::borrow::Borrow<Transaction>,
>(
input: &Input,
prevouts: &Vec<&TxOut>,
input_index: usize,
cache: &mut SighashCache<T>,
tapleaf_hash: Option<TapLeafHash>,
) -> 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<Psbt> {
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<TapLeafHash> = 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))
}