use sp_backend
This commit is contained in:
parent
3ebc319a26
commit
d816115929
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
330
Cargo.lock
generated
330
Cargo.lock
generated
@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.2"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@ -43,9 +43,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.80"
|
||||
version = "1.0.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
|
||||
checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
@ -109,18 +109,33 @@ version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin"
|
||||
version = "0.30.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1945a5048598e4189e239d3f809b19bdad4845c4b2ba400d304d2dcf26d2c462"
|
||||
dependencies = [
|
||||
"bech32 0.9.1",
|
||||
"bitcoin-private",
|
||||
"bitcoin_hashes 0.12.0",
|
||||
"hex_lit",
|
||||
"secp256k1 0.27.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin"
|
||||
version = "0.31.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd00f3c09b5f21fb357abe32d29946eb8bb7a0862bae62c0b5e4a692acbbe73c"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"bech32 0.10.0-beta",
|
||||
"bitcoin-internals",
|
||||
"bitcoin_hashes",
|
||||
"bitcoin_hashes 0.13.0",
|
||||
"hex-conservative",
|
||||
"hex_lit",
|
||||
"secp256k1",
|
||||
"secp256k1 0.28.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@ -133,6 +148,22 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin-private"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57"
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501"
|
||||
dependencies = [
|
||||
"bitcoin-private",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.13.0"
|
||||
@ -163,7 +194,7 @@ version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "856ffbee2e492c23bca715d72ea34aae80d58400f2bda26a82015d6bc2ec3662"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"bitcoin 0.31.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
@ -174,7 +205,7 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff1e21acea23b5ec33f912350f18adee9a08bd513dca9f66f0e2cfe9d756ef46"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"bitcoin 0.31.1",
|
||||
"zmq",
|
||||
"zmq-sys",
|
||||
]
|
||||
@ -196,9 +227,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.15.3"
|
||||
version = "3.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b"
|
||||
checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
@ -214,9 +245,9 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.83"
|
||||
version = "1.0.90"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
|
||||
checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
@ -281,9 +312,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.11"
|
||||
version = "0.5.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b"
|
||||
checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
@ -410,6 +441,24 @@ version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
|
||||
|
||||
[[package]]
|
||||
name = "electrum-client"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/cygnet3/rust-electrum-client?branch=sp_tweaks#9332bb9681e9b47d806000da48408229d712e4e6"
|
||||
dependencies = [
|
||||
"bitcoin 0.30.2",
|
||||
"bitcoin-private",
|
||||
"byteorder",
|
||||
"libc",
|
||||
"log",
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"webpki",
|
||||
"webpki-roots",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.9.3"
|
||||
@ -517,9 +566,9 @@ checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
@ -532,9 +581,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.6"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
@ -556,9 +605,9 @@ checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd"
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea"
|
||||
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
@ -629,9 +678,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.2.3"
|
||||
version = "2.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177"
|
||||
checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.3",
|
||||
@ -691,9 +740,9 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.20"
|
||||
version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
@ -712,13 +761,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.10"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -742,7 +791,7 @@ version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.6",
|
||||
"hermit-abi 0.3.9",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@ -799,9 +848,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.78"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||
checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@ -847,9 +896,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.8.1"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051"
|
||||
checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
@ -879,9 +928,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.5"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
|
||||
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@ -894,12 +943,49 @@ version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"libc",
|
||||
"spin",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
"rustls-webpki",
|
||||
"sct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.17"
|
||||
@ -915,38 +1001,69 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sdk_relay"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitcoin",
|
||||
"bitcoincore-rpc",
|
||||
"bitcoincore-zmq",
|
||||
"electrum-client",
|
||||
"env_logger",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"silentpayments",
|
||||
"sp_backend",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f"
|
||||
dependencies = [
|
||||
"bitcoin_hashes 0.12.0",
|
||||
"secp256k1-sys 0.8.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1"
|
||||
version = "0.28.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10"
|
||||
dependencies = [
|
||||
"bitcoin_hashes",
|
||||
"bitcoin_hashes 0.13.0",
|
||||
"rand",
|
||||
"secp256k1-sys",
|
||||
"secp256k1-sys 0.9.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1-sys"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1-sys"
|
||||
version = "0.9.2"
|
||||
@ -958,18 +1075,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.196"
|
||||
version = "1.0.197"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
|
||||
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.196"
|
||||
version = "1.0.197"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
|
||||
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -998,15 +1115,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.6.1"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270"
|
||||
checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.2.3",
|
||||
"indexmap 2.2.5",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@ -1016,9 +1133,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.6.1"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d"
|
||||
checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
@ -1039,14 +1156,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "silentpayments"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/cygnet3/rust-silentpayments?branch=master#e915f5f8daef4b39ea32902963c143a5c0ed1746"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f3a0169e01dc753fe00f82d0b08a68d49e69f912f592e95ff5ade8b623728a5"
|
||||
dependencies = [
|
||||
"bech32 0.9.1",
|
||||
"bimap",
|
||||
"bitcoin_hashes",
|
||||
"bitcoin_hashes 0.13.0",
|
||||
"hex",
|
||||
"secp256k1",
|
||||
"secp256k1 0.28.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
@ -1068,14 +1186,32 @@ checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.5"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
|
||||
checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sp_backend"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/Sosthene00/sp-backend?branch=sp_client#0213188a95921081f5c74e5099ac46e6737a07d0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitcoin 0.31.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"silentpayments",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
@ -1084,9 +1220,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.49"
|
||||
version = "2.0.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496"
|
||||
checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1095,9 +1231,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "6.2.0"
|
||||
version = "6.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331"
|
||||
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
|
||||
dependencies = [
|
||||
"cfg-expr",
|
||||
"heck",
|
||||
@ -1108,9 +1244,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.13"
|
||||
version = "0.12.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae"
|
||||
checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
@ -1123,18 +1259,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.57"
|
||||
version = "1.0.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
|
||||
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.57"
|
||||
version = "1.0.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
|
||||
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1201,7 +1337,7 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1217,9 +1353,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
|
||||
checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
@ -1240,9 +1376,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.10"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290"
|
||||
checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
@ -1261,11 +1397,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.6"
|
||||
version = "0.22.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6"
|
||||
checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4"
|
||||
dependencies = [
|
||||
"indexmap 2.2.3",
|
||||
"indexmap 2.2.5",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
@ -1311,13 +1447,19 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.22"
|
||||
version = "0.1.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
|
||||
checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.0"
|
||||
@ -1337,9 +1479,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
|
||||
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
@ -1349,9 +1491,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
@ -1417,6 +1559,25 @@ version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
||||
|
||||
[[package]]
|
||||
name = "webpki"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.22.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87"
|
||||
dependencies = [
|
||||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@ -1466,6 +1627,15 @@ dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
@ -1582,9 +1752,9 @@ checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.1"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d90f4e0f530c4c69f62b80d839e9ef3855edc9cba471a160c4d692deed62b401"
|
||||
checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
@ -5,17 +5,17 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
bitcoin = { version = "0.31.1", features = ["serde"] }
|
||||
bitcoincore-rpc = { version = "0.18" }
|
||||
bitcoincore-zmq = "1.4.0"
|
||||
electrum-client = { git = "https://github.com/cygnet3/rust-electrum-client", branch = "sp_tweaks" }
|
||||
env_logger = "0.9"
|
||||
futures-util = { version = "0.3.28", default-features = false, features = ["sink", "std"] }
|
||||
hex = "0.4.3"
|
||||
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'] }
|
||||
sp_backend = { git = "https://github.com/Sosthene00/sp-backend", branch = "sp_client" }
|
||||
tokio = { version = "1.0.0", features = ["io-util", "rt-multi-thread", "macros", "sync"] }
|
||||
tokio-stream = "0.1"
|
||||
tokio-tungstenite = "0.21.0"
|
||||
|
@ -1,26 +0,0 @@
|
||||
use bitcoin::Amount;
|
||||
|
||||
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 const DUST: Amount = Amount::from_sat(546);
|
||||
|
||||
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,
|
||||
}
|
148
src/daemon.rs
148
src/daemon.rs
@ -1,9 +1,16 @@
|
||||
use anyhow::{anyhow, Context, Result, Error};
|
||||
use anyhow::{Context, Error, Result};
|
||||
|
||||
use bitcoin::{consensus::deserialize, hashes::hex::FromHex};
|
||||
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::{
|
||||
CreateRawTransactionInput, ListUnspentQueryOptions, ListUnspentResultEntry,
|
||||
WalletCreateFundedPsbtOptions,
|
||||
};
|
||||
use bitcoincore_rpc::{json, jsonrpc, Auth, Client, RpcApi};
|
||||
use sp_backend::bitcoin::bip158::BlockFilter;
|
||||
use sp_backend::bitcoin::{
|
||||
block, Address, Amount, Block, BlockHash, Network, OutPoint, Psbt, ScriptBuf, Sequence,
|
||||
Transaction, TxIn, TxOut, Txid,
|
||||
};
|
||||
use sp_backend::bitcoin::{consensus::deserialize, hashes::hex::FromHex};
|
||||
// use crossbeam_channel::Receiver;
|
||||
// use parking_lot::Mutex;
|
||||
use serde_json::{json, Value};
|
||||
@ -15,6 +22,8 @@ use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::FAUCET_AMT;
|
||||
|
||||
pub struct SensitiveAuth(pub Auth);
|
||||
|
||||
impl SensitiveAuth {
|
||||
@ -80,14 +89,22 @@ fn read_cookie(path: &Path) -> Result<(String, String)> {
|
||||
Ok((parts[0].to_owned(), parts[1].to_owned()))
|
||||
}
|
||||
|
||||
fn rpc_connect() -> Result<Client> {
|
||||
let rpc_url = "http://127.0.0.1:39332";
|
||||
fn rpc_connect(rpcwallet: Option<String>) -> Result<Client> {
|
||||
let mut rpc_url = "http://127.0.0.1:39332".to_owned();
|
||||
|
||||
match rpcwallet {
|
||||
Some(rpcwallet) => rpc_url.push_str(&rpcwallet),
|
||||
None => (),
|
||||
}
|
||||
|
||||
// Allow `wait_for_new_block` to take a bit longer before timing out.
|
||||
// See https://github.com/romanz/electrs/issues/495 for more details.
|
||||
let builder = jsonrpc::simple_http::SimpleHttpTransport::builder()
|
||||
.url(&rpc_url)?
|
||||
.timeout(Duration::from_secs(30));
|
||||
let daemon_auth = SensitiveAuth(Auth::CookieFile(PathBuf::from_str("/home/sosthene/.bitcoin/signet/.cookie").unwrap()));
|
||||
let daemon_auth = SensitiveAuth(Auth::CookieFile(
|
||||
PathBuf::from_str("/home/sosthene/.bitcoin/signet/.cookie").unwrap(),
|
||||
));
|
||||
let builder = match daemon_auth.get_auth() {
|
||||
Auth::None => builder,
|
||||
Auth::UserPass(user, pass) => builder.auth(user, Some(pass)),
|
||||
@ -108,11 +125,12 @@ pub struct Daemon {
|
||||
|
||||
impl Daemon {
|
||||
pub(crate) fn connect(
|
||||
rpcwallet: Option<String>,
|
||||
// config: &Config,
|
||||
// exit_flag: &ExitFlag,
|
||||
// metrics: &Metrics,
|
||||
) -> Result<Self> {
|
||||
let mut rpc = rpc_connect()?;
|
||||
let mut rpc = rpc_connect(rpcwallet)?;
|
||||
|
||||
loop {
|
||||
match rpc_poll(&mut rpc, false) {
|
||||
@ -167,78 +185,110 @@ impl Daemon {
|
||||
Ok(self
|
||||
.rpc
|
||||
.get_block_count()
|
||||
.context("failed to 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,
|
||||
pub(crate) fn get_block(&self, block_hash: BlockHash) -> Result<Block> {
|
||||
Ok(self
|
||||
.rpc
|
||||
.get_block(&block_hash)
|
||||
.context("failed to get block")?)
|
||||
}
|
||||
|
||||
pub(crate) fn get_filters(&self, block_height: u32) -> Result<(u32, BlockHash, BlockFilter)> {
|
||||
let block_hash = self.rpc.get_block_hash(block_height.try_into()?)?;
|
||||
let filter = self
|
||||
.rpc
|
||||
.get_block_filter(&block_hash)
|
||||
.context("failed to get block filter")?
|
||||
.into_filter();
|
||||
Ok((block_height, block_hash, filter))
|
||||
}
|
||||
|
||||
pub(crate) fn list_unspent_from_to(
|
||||
&self,
|
||||
minamt: Option<Amount>,
|
||||
) -> Result<Vec<json::ListUnspentResultEntry>> {
|
||||
let minimum_sum_amount = if minamt.is_none() || minamt <= FAUCET_AMT.checked_mul(2) {
|
||||
FAUCET_AMT.checked_mul(2)
|
||||
} else {
|
||||
minamt
|
||||
};
|
||||
Ok(self.rpc.list_unspent(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(false),
|
||||
Some(ListUnspentQueryOptions {
|
||||
maximum_count: Some(5),
|
||||
minimum_sum_amount,
|
||||
..Default::default()
|
||||
})
|
||||
}),
|
||||
)?)
|
||||
}
|
||||
|
||||
pub(crate) fn create_psbt(&self, utxo: ListUnspentResultEntry, spk: ScriptBuf, network: Network) -> Result<String> {
|
||||
let input = CreateRawTransactionInput {
|
||||
pub(crate) fn create_psbt(
|
||||
&self,
|
||||
unspents: &[ListUnspentResultEntry],
|
||||
spk: ScriptBuf,
|
||||
network: Network,
|
||||
) -> Result<String> {
|
||||
let inputs: Vec<CreateRawTransactionInput> = unspents
|
||||
.iter()
|
||||
.map(|utxo| CreateRawTransactionInput {
|
||||
txid: utxo.txid,
|
||||
vout: utxo.vout,
|
||||
sequence: None
|
||||
};
|
||||
sequence: None,
|
||||
})
|
||||
.collect();
|
||||
let address = Address::from_script(&spk, network)?;
|
||||
let total_amt = unspents
|
||||
.iter()
|
||||
.fold(Amount::from_sat(0), |acc, x| acc + x.amount);
|
||||
|
||||
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())
|
||||
outputs.insert(address.to_string(), total_amt);
|
||||
|
||||
let options = WalletCreateFundedPsbtOptions {
|
||||
subtract_fee_from_outputs: vec![0],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let wallet_create_funded_result =
|
||||
self.rpc
|
||||
.wallet_create_funded_psbt(&inputs, &outputs, None, Some(options), None)?;
|
||||
|
||||
Ok(wallet_create_funded_result.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
|
||||
)?;
|
||||
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"))
|
||||
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))?;
|
||||
pub(crate) fn finalize_psbt(&self, psbt: String) -> Result<String> {
|
||||
let final_tx = self.rpc.finalize_psbt(&psbt, Some(false))?;
|
||||
|
||||
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"))
|
||||
true => Ok(final_tx
|
||||
.psbt
|
||||
.expect("We shouldn't have an empty psbt 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()?;
|
||||
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)
|
||||
.context("failed to broadcast transaction")
|
||||
let txid = self.rpc.send_raw_transaction(tx)?;
|
||||
|
||||
Ok(txid)
|
||||
}
|
||||
|
||||
pub(crate) fn get_transaction_info(
|
||||
@ -260,7 +310,7 @@ impl Daemon {
|
||||
txid: &Txid,
|
||||
blockhash: Option<BlockHash>,
|
||||
) -> Result<Value> {
|
||||
use bitcoin::consensus::serde::{hex::Lower, Hex, With};
|
||||
use sp_backend::bitcoin::consensus::serde::{hex::Lower, Hex, With};
|
||||
|
||||
let tx = self.get_transaction(txid, blockhash)?;
|
||||
#[derive(serde::Serialize)]
|
||||
|
55
src/db.rs
55
src/db.rs
@ -1,55 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
673
src/main.rs
673
src/main.rs
@ -1,33 +1,56 @@
|
||||
use std::{
|
||||
collections::HashMap, env, io::Error as IoError, net::SocketAddr, sync::{Arc, Mutex}
|
||||
collections::HashMap,
|
||||
env,
|
||||
net::SocketAddr,
|
||||
ops::Deref,
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use bitcoin::{absolute::LockTime, consensus::deserialize, key::TapTweak, secp256k1::PublicKey, transaction::Version, Amount, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, XOnlyPublicKey};
|
||||
use bitcoin::secp256k1::{Message as Secp256k1Message, ThirtyTwoByteHash};
|
||||
use bitcoincore_rpc::json as bitcoin_json;
|
||||
use bitcoincore_rpc::json::{self 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 sp_backend::bitcoin::secp256k1::{
|
||||
rand::{thread_rng, Rng},
|
||||
Error as Secp256k1Error, Keypair, Message as Secp256k1Message, PublicKey, Scalar, Secp256k1,
|
||||
SecretKey, ThirtyTwoByteHash,
|
||||
};
|
||||
use sp_backend::bitcoin::{
|
||||
absolute::LockTime,
|
||||
consensus::deserialize,
|
||||
hex::DisplayHex,
|
||||
key::TapTweak,
|
||||
sighash::{Prevouts, SighashCache},
|
||||
taproot::Signature,
|
||||
transaction::Version,
|
||||
Amount, OutPoint, Psbt, ScriptBuf, TapSighashType, Transaction, TxIn, TxOut, Txid, Witness,
|
||||
XOnlyPublicKey,
|
||||
};
|
||||
use sp_backend::spclient::OutputList;
|
||||
|
||||
use sp_backend::db::{JsonFile, Storage};
|
||||
use sp_backend::silentpayments::receiving::Label;
|
||||
use sp_backend::silentpayments::sending::{generate_recipient_pubkeys, SilentPaymentAddress};
|
||||
use sp_backend::silentpayments::utils::receiving::{
|
||||
calculate_shared_secret, calculate_tweak_data, get_pubkey_from_input,
|
||||
};
|
||||
use sp_backend::silentpayments::utils::sending::calculate_partial_secret;
|
||||
use sp_backend::spclient::{
|
||||
derive_keys_from_seed, OutputSpendStatus, OwnedOutput, Recipient, SpClient, SpendKey,
|
||||
};
|
||||
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};
|
||||
use anyhow::{Error, Result};
|
||||
|
||||
mod sp;
|
||||
mod daemon;
|
||||
mod spclient;
|
||||
mod constants;
|
||||
mod db;
|
||||
mod electrumclient;
|
||||
mod scan;
|
||||
|
||||
use crate::daemon::Daemon;
|
||||
use crate::sp::VinData;
|
||||
use crate::spclient::{SpClient, SpendKey, OutputSpendStatus};
|
||||
use crate::{daemon::Daemon, scan::scan_blocks};
|
||||
|
||||
type Tx = UnboundedSender<Message>;
|
||||
|
||||
@ -35,125 +58,400 @@ type PeerMap = Arc<Mutex<HashMap<SocketAddr, Tx>>>;
|
||||
|
||||
const FAUCET_AMT: Amount = Amount::from_sat(1000);
|
||||
|
||||
fn spend_from_core(dest: XOnlyPublicKey, daemon: Arc<Mutex<Daemon>>) -> Result<Transaction> {
|
||||
let core = daemon.lock().map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e.to_string())))?;
|
||||
let unspent_list: Vec<bitcoin_json::ListUnspentResultEntry> = core.list_unspent_from_to(Some(101), None)?; // we're (probably) spending coinbase, so let's be extra cautious and not spend before 101 confirmations
|
||||
enum BroadcastType {
|
||||
Sender(SocketAddr),
|
||||
ExcludeSender(SocketAddr),
|
||||
#[allow(dead_code)]
|
||||
ToAll,
|
||||
}
|
||||
|
||||
// just take the first of the list
|
||||
if let Some(unspent) = unspent_list.get(0) {
|
||||
fn broadcast_message(peers: PeerMap, msg: Message, broadcast: BroadcastType) -> Result<()> {
|
||||
// log::debug!("Broadcasting message: {}", msg);
|
||||
match broadcast {
|
||||
BroadcastType::Sender(addr) => {
|
||||
peers
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(format!("Failed to lock peers: {}", e.to_string())))?
|
||||
.iter()
|
||||
.find(|(peer_addr, _)| peer_addr == &&addr)
|
||||
.ok_or(Error::msg("Failed to find the sender in the peer_map"))?
|
||||
.1
|
||||
.send(msg.clone())?;
|
||||
}
|
||||
BroadcastType::ExcludeSender(addr) => {
|
||||
peers
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(format!("Failed to lock peers: {}", e.to_string())))?
|
||||
.iter()
|
||||
.filter(|(peer_addr, _)| peer_addr != &&addr)
|
||||
.for_each(|(_, peer_tx)| {
|
||||
let _ = peer_tx.send(msg.clone());
|
||||
});
|
||||
}
|
||||
BroadcastType::ToAll => {
|
||||
peers
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(format!("Failed to lock peers: {}", e.to_string())))?
|
||||
.iter()
|
||||
.for_each(|(_, peer_tx)| {
|
||||
let _ = peer_tx.send(msg.clone());
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spend_from_core(
|
||||
dest: XOnlyPublicKey,
|
||||
daemon: Arc<Mutex<Daemon>>,
|
||||
) -> Result<(Transaction, Amount)> {
|
||||
let core = daemon
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e.to_string())))?;
|
||||
let unspent_list: Vec<bitcoin_json::ListUnspentResultEntry> =
|
||||
core.list_unspent_from_to(None)?;
|
||||
|
||||
if !unspent_list.is_empty() {
|
||||
let network = core.get_network()?;
|
||||
|
||||
let spk = ScriptBuf::new_p2tr_tweaked(dest.dangerous_assume_tweaked());
|
||||
|
||||
let new_psbt = core.create_psbt(unspent.clone(), spk, network)?;
|
||||
let new_psbt = core.create_psbt(&unspent_list, spk, network)?;
|
||||
let processed_psbt = core.process_psbt(new_psbt)?;
|
||||
let tx = core.finalize_psbt(processed_psbt)?;
|
||||
let final_tx = deserialize::<bitcoin::Transaction>(&tx)?;
|
||||
let finalize_psbt_result = core.finalize_psbt(processed_psbt)?;
|
||||
let final_psbt = Psbt::from_str(&finalize_psbt_result)?;
|
||||
let total_fee = final_psbt.fee()?;
|
||||
let final_tx = final_psbt.extract_tx()?;
|
||||
let fee_rate = total_fee
|
||||
.checked_div(final_tx.weight().to_vbytes_ceil())
|
||||
.unwrap();
|
||||
|
||||
let _ = core.broadcast(&final_tx)?;
|
||||
|
||||
Ok(final_tx)
|
||||
Ok((final_tx, fee_rate))
|
||||
} else {
|
||||
// we have 0 spendable outputs
|
||||
// we don't have enough available coins to pay for this faucet request
|
||||
Err(Error::msg("No spendable outputs"))
|
||||
}
|
||||
}
|
||||
|
||||
fn faucet_send(sp_address: SilentPaymentAddress, sp_client: Arc<Mutex<SpClient>>, daemon: Arc<Mutex<Daemon>>) -> Result<Txid> {
|
||||
let wallet = sp_client.lock().map_err(|e| Error::msg(format!("{}", e.to_string())))?;
|
||||
fn find_owned_outputs(
|
||||
tx: &Transaction,
|
||||
ours: HashMap<Option<Label>, HashMap<XOnlyPublicKey, Scalar>>,
|
||||
) -> Result<HashMap<OutPoint, OwnedOutput>> {
|
||||
let mut res: HashMap<OutPoint, OwnedOutput> = HashMap::new();
|
||||
for (label, map) in ours {
|
||||
res.extend(tx.output.iter().enumerate().filter_map(
|
||||
|(i, o)| match XOnlyPublicKey::from_slice(&o.script_pubkey.as_bytes()[2..]) {
|
||||
Ok(key) => {
|
||||
if let Some(scalar) = map.get(&key) {
|
||||
match SecretKey::from_slice(&scalar.to_be_bytes()) {
|
||||
Ok(tweak) => {
|
||||
let outpoint = OutPoint {
|
||||
txid: tx.txid(),
|
||||
vout: i as u32,
|
||||
};
|
||||
let label_str: Option<String>;
|
||||
if let Some(l) = &label {
|
||||
label_str =
|
||||
Some(l.as_inner().to_be_bytes().to_lower_hex_string());
|
||||
} else {
|
||||
label_str = None;
|
||||
}
|
||||
return Some((
|
||||
outpoint,
|
||||
OwnedOutput {
|
||||
blockheight: 0,
|
||||
tweak: tweak.secret_bytes().to_lower_hex_string(),
|
||||
amount: o.value,
|
||||
script: o.script_pubkey.as_bytes().to_lower_hex_string(),
|
||||
label: label_str,
|
||||
spend_status: OutputSpendStatus::Unspent,
|
||||
},
|
||||
));
|
||||
}
|
||||
Err(_) => {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
Err(_) => None,
|
||||
},
|
||||
));
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn faucet_send(
|
||||
sp_address: SilentPaymentAddress,
|
||||
sp_client: Arc<Mutex<SpClient>>,
|
||||
sp_outputs: Arc<Mutex<OutputList>>,
|
||||
daemon: Arc<Mutex<Daemon>>,
|
||||
) -> Result<Txid> {
|
||||
let mut first_tx: Option<Transaction> = None;
|
||||
let final_tx: Transaction;
|
||||
if let Some(utxo) = wallet.list_outpoints()
|
||||
.into_iter()
|
||||
let mut new_outpoints: HashMap<OutPoint, OwnedOutput>;
|
||||
|
||||
// do we have a sp output available ?
|
||||
.find(|o| o.spend_status == OutputSpendStatus::Unspent) {
|
||||
// create a new transaction with an available output
|
||||
let available_outpoints = sp_outputs
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(e.to_string()))?
|
||||
.to_spendable_list();
|
||||
|
||||
let available_amt = available_outpoints
|
||||
.iter()
|
||||
.fold(Amount::from_sat(0), |acc, (_, x)| acc + x.amount);
|
||||
|
||||
// If we don't have at least 4 times the amount we need to send, we take some reserves out
|
||||
if available_amt > FAUCET_AMT.checked_mul(4).unwrap() {
|
||||
let mut total_amt = Amount::from_sat(0);
|
||||
let mut inputs = HashMap::new();
|
||||
for (outpoint, output) in available_outpoints {
|
||||
total_amt += output.amount;
|
||||
inputs.insert(outpoint, output);
|
||||
if total_amt >= FAUCET_AMT {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let recipient = Recipient {
|
||||
address: sp_address.into(),
|
||||
amount: utxo.amount,
|
||||
nb_outputs: 1
|
||||
amount: FAUCET_AMT,
|
||||
nb_outputs: 1,
|
||||
};
|
||||
let mut new_psbt = wallet.create_new_psbt(vec![utxo], vec![recipient], None)?;
|
||||
SpClient::set_fees(&mut new_psbt, 1, sp_address.into())?;
|
||||
|
||||
let fee_estimate = daemon
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(format!("{}", e.to_string())))?
|
||||
.estimate_fee(6)?
|
||||
.unwrap_or(Amount::from_sat(1000))
|
||||
.checked_div(1000)
|
||||
.unwrap();
|
||||
|
||||
log::debug!("fee estimate for 6 blocks: {}", fee_estimate);
|
||||
|
||||
let wallet = sp_client.lock().map_err(|e| Error::msg(e.to_string()))?;
|
||||
|
||||
let mut new_psbt = wallet.create_new_psbt(inputs.clone(), vec![recipient], None)?;
|
||||
log::debug!("Created psbt: {}", new_psbt);
|
||||
SpClient::set_fees(&mut new_psbt, fee_estimate, sp_address.into())?;
|
||||
wallet.fill_sp_outputs(&mut new_psbt)?;
|
||||
let mut signed = wallet.sign_psbt(new_psbt)?;
|
||||
log::debug!("Definitive psbt: {}", new_psbt);
|
||||
let mut aux_rand = [0u8; 32];
|
||||
thread_rng().fill(&mut aux_rand);
|
||||
let mut signed = wallet.sign_psbt(new_psbt, &aux_rand)?;
|
||||
log::debug!("signed psbt: {}", signed);
|
||||
SpClient::finalize_psbt(&mut signed)?;
|
||||
|
||||
final_tx = signed.extract_tx()?;
|
||||
|
||||
// take all we need to register the new sp output
|
||||
let outpoints: Vec<(String, u32)> = final_tx
|
||||
.input
|
||||
.iter()
|
||||
.map(|i| (i.previous_output.txid.to_string(), i.previous_output.vout))
|
||||
.collect();
|
||||
|
||||
let our_sp_address: SilentPaymentAddress =
|
||||
wallet.sp_receiver.get_receiving_address().try_into()?;
|
||||
let our_spend_pubkey = our_sp_address.get_spend_key();
|
||||
let secp = Secp256k1::verification_only();
|
||||
let input_pubkeys: Result<Vec<PublicKey>, Secp256k1Error> = inputs
|
||||
.iter()
|
||||
.map(|(_, o)| {
|
||||
let tweak = SecretKey::from_str(&o.tweak)?;
|
||||
our_spend_pubkey.mul_tweak(&secp, &tweak.into())
|
||||
})
|
||||
.collect();
|
||||
let input_pubkeys = input_pubkeys?;
|
||||
let input_pubkeys: Vec<&PublicKey> = input_pubkeys.iter().collect();
|
||||
let partial_tweak = calculate_tweak_data(&input_pubkeys, &outpoints)?;
|
||||
let ecdh_shared_secret = calculate_shared_secret(partial_tweak, wallet.get_scan_key())?;
|
||||
|
||||
let outputs_to_check: Result<Vec<XOnlyPublicKey>, Secp256k1Error> = final_tx
|
||||
.output
|
||||
.iter()
|
||||
.map(|o| XOnlyPublicKey::from_slice(&o.script_pubkey.as_bytes()[2..]))
|
||||
.collect();
|
||||
|
||||
let ours = wallet
|
||||
.sp_receiver
|
||||
.scan_transaction(&ecdh_shared_secret, outputs_to_check?)?;
|
||||
|
||||
new_outpoints = find_owned_outputs(&final_tx, ours)?;
|
||||
} else {
|
||||
drop(wallet); // we don't want to keep locking it
|
||||
// let's try to spend directly from the mining address
|
||||
let secp = bitcoin::secp256k1::Secp256k1::signing_only();
|
||||
let keypair = bitcoin::secp256k1::Keypair::new(&secp, &mut thread_rng());
|
||||
let secp = Secp256k1::signing_only();
|
||||
let keypair = Keypair::new(&secp, &mut thread_rng());
|
||||
|
||||
// we first spend from core to the pubkey we just created
|
||||
let first_tx = spend_from_core(keypair.x_only_public_key().0, daemon.clone())?;
|
||||
let (core_tx, fee_rate) = spend_from_core(keypair.x_only_public_key().0, daemon.clone())?;
|
||||
|
||||
// check that the first output of the transaction pays to the key we just created
|
||||
assert!(first_tx.output[0].script_pubkey == ScriptBuf::new_p2tr_tweaked(keypair.x_only_public_key().0.dangerous_assume_tweaked()));
|
||||
assert!(
|
||||
core_tx.output[0].script_pubkey
|
||||
== ScriptBuf::new_p2tr_tweaked(
|
||||
keypair.x_only_public_key().0.dangerous_assume_tweaked()
|
||||
)
|
||||
);
|
||||
|
||||
// create a new transaction that spends the newly created UTXO to the sp_address
|
||||
let mut faucet_tx = Transaction {
|
||||
input: vec![
|
||||
TxIn {
|
||||
previous_output: OutPoint::new(first_tx.txid(), 0),
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(core_tx.txid(), 0),
|
||||
..Default::default()
|
||||
}
|
||||
],
|
||||
}],
|
||||
output: vec![],
|
||||
version: Version::TWO,
|
||||
lock_time: LockTime::ZERO
|
||||
lock_time: LockTime::ZERO,
|
||||
};
|
||||
|
||||
// now do the silent payment operations with the final recipient address
|
||||
let a_sum = SpClient::get_a_sum_secret_keys(&vec![keypair.secret_key()]);
|
||||
let prev_outpoint = faucet_tx.input[0].previous_output;
|
||||
let outpoints_hash = silentpayments::utils::hash_outpoints(&vec![(prev_outpoint.txid.to_string(), prev_outpoint.vout)], keypair.public_key())?;
|
||||
let partial_secret = silentpayments::utils::sending::sender_calculate_partial_secret(a_sum, outpoints_hash)?;
|
||||
let partial_secret = calculate_partial_secret(
|
||||
&[(keypair.secret_key(), true)],
|
||||
&[(core_tx.txid().to_string(), 0)],
|
||||
)?;
|
||||
|
||||
let ext_output_key = silentpayments::sending::generate_recipient_pubkey(sp_address.into(), partial_secret)?;
|
||||
let change_sp_address = sp_client.lock()
|
||||
let ext_output_key: XOnlyPublicKey =
|
||||
generate_recipient_pubkeys(vec![sp_address.into()], partial_secret)?
|
||||
.into_values()
|
||||
.flatten()
|
||||
.collect::<Vec<XOnlyPublicKey>>()
|
||||
.get(0)
|
||||
.expect("Failed to generate keys")
|
||||
.to_owned();
|
||||
let change_sp_address = sp_client
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(format!("Failed to lock sp_client: {}", e.to_string())))?
|
||||
.get_receiving_address();
|
||||
let change_output_key = silentpayments::sending::generate_recipient_pubkey(change_sp_address, partial_secret)?;
|
||||
let change_output_key: XOnlyPublicKey =
|
||||
generate_recipient_pubkeys(vec![change_sp_address], partial_secret)?
|
||||
.into_values()
|
||||
.flatten()
|
||||
.collect::<Vec<XOnlyPublicKey>>()
|
||||
.get(0)
|
||||
.expect("Failed to generate keys")
|
||||
.to_owned();
|
||||
|
||||
let ext_spk = ScriptBuf::new_p2tr_tweaked(ext_output_key.dangerous_assume_tweaked());
|
||||
let change_spk = ScriptBuf::new_p2tr_tweaked(change_output_key.dangerous_assume_tweaked());
|
||||
|
||||
faucet_tx.output.push(TxOut {
|
||||
value: FAUCET_AMT,
|
||||
script_pubkey: ext_spk
|
||||
script_pubkey: ext_spk,
|
||||
});
|
||||
faucet_tx.output.push(TxOut {
|
||||
value: first_tx.output[0].value - FAUCET_AMT,
|
||||
script_pubkey: change_spk
|
||||
value: core_tx.output[0].value - FAUCET_AMT,
|
||||
script_pubkey: change_spk,
|
||||
});
|
||||
|
||||
let first_tx_outputs = vec![first_tx.output[0].clone()];
|
||||
let prevouts = bitcoin::sighash::Prevouts::All(&first_tx_outputs);
|
||||
// dummy signature only used for fee estimation
|
||||
faucet_tx.input[0].witness.push([1; 64].to_vec());
|
||||
|
||||
let hash_ty = bitcoin::TapSighashType::Default;
|
||||
let abs_fee = fee_rate
|
||||
.checked_mul(faucet_tx.weight().to_vbytes_ceil())
|
||||
.ok_or_else(|| Error::msg("Fee rate multiplication overflowed"))?;
|
||||
|
||||
let mut cache = bitcoin::sighash::SighashCache::new(&faucet_tx);
|
||||
// reset the witness to empty
|
||||
faucet_tx.input[0].witness = Witness::new();
|
||||
|
||||
faucet_tx.output[1].value -= abs_fee;
|
||||
|
||||
let first_tx_outputs = vec![core_tx.output[0].clone()];
|
||||
let prevouts = Prevouts::All(&first_tx_outputs);
|
||||
|
||||
let hash_ty = TapSighashType::Default;
|
||||
|
||||
let mut cache = SighashCache::new(&faucet_tx);
|
||||
|
||||
let sighash = cache.taproot_key_spend_signature_hash(0, &prevouts, hash_ty)?;
|
||||
|
||||
let msg = Secp256k1Message::from_digest(sighash.into_32());
|
||||
|
||||
let sig = secp.sign_schnorr_with_rng(&msg, &keypair, &mut thread_rng());
|
||||
let final_sig = bitcoin::taproot::Signature{ sig, hash_ty };
|
||||
let final_sig = Signature { sig, hash_ty };
|
||||
|
||||
faucet_tx.input[0].witness.push(final_sig.to_vec());
|
||||
|
||||
// take all we need to register the new sp output
|
||||
let outpoints: Vec<(String, u32)> = vec![(core_tx.txid().to_string(), 0)];
|
||||
|
||||
first_tx = Some(core_tx);
|
||||
|
||||
let client = sp_client
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(format!("{}", e.to_string())))?;
|
||||
|
||||
let input_pubkey = &keypair.public_key();
|
||||
|
||||
let input_pub_keys: Vec<&PublicKey> = vec![input_pubkey];
|
||||
let partial_tweak = calculate_tweak_data(&input_pub_keys, &outpoints)?;
|
||||
let ecdh_shared_secret = calculate_shared_secret(partial_tweak, client.get_scan_key())?;
|
||||
|
||||
let p2tr_outs = vec![ext_output_key, change_output_key];
|
||||
|
||||
let ours = client
|
||||
.sp_receiver
|
||||
.scan_transaction(&ecdh_shared_secret, p2tr_outs)?;
|
||||
|
||||
final_tx = faucet_tx;
|
||||
|
||||
new_outpoints = find_owned_outputs(&final_tx, ours)?;
|
||||
}
|
||||
daemon.lock()
|
||||
.map_err(|e| Error::msg(format!("{}", e.to_string())))?
|
||||
.broadcast(&final_tx)?;
|
||||
|
||||
if let Ok(core) = daemon.lock() {
|
||||
// get current blockheight
|
||||
let blkheight: u32 = core.get_current_height()?.try_into()?;
|
||||
// update the new outpoints
|
||||
for o in new_outpoints.iter_mut() {
|
||||
o.1.blockheight = blkheight;
|
||||
}
|
||||
|
||||
// broadcast one or two transactions
|
||||
if first_tx.is_some() {
|
||||
core.broadcast(&first_tx.unwrap())?;
|
||||
}
|
||||
core.broadcast(&final_tx)?;
|
||||
} else {
|
||||
return Err(Error::msg("Failed to lock daemon"));
|
||||
}
|
||||
|
||||
// update our sp_client with the change output(s)
|
||||
let mut outputs = sp_outputs
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(format!("{}", e.to_string())))?;
|
||||
|
||||
outputs.extend_from(new_outpoints);
|
||||
|
||||
log::debug!("{:?}", outputs.to_outpoints_list());
|
||||
|
||||
Ok(final_tx.txid())
|
||||
}
|
||||
|
||||
async fn handle_connection(peer_map: PeerMap, raw_stream: TcpStream, addr: SocketAddr, sp_client: Arc<Mutex<SpClient>>, daemon: Arc<Mutex<Daemon>>) {
|
||||
fn handle_faucet_request(
|
||||
msg: &str,
|
||||
sp_client: Arc<Mutex<SpClient>>,
|
||||
sp_outputs: Arc<Mutex<OutputList>>,
|
||||
daemon: Arc<Mutex<Daemon>>,
|
||||
) -> Result<Txid> {
|
||||
if let Ok(sp_address) = SilentPaymentAddress::try_from(&msg["faucet".len()..]) {
|
||||
// send bootstrap coins to this sp_address
|
||||
faucet_send(sp_address, sp_client, sp_outputs, daemon)
|
||||
} else {
|
||||
Err(Error::msg(format!(
|
||||
"faucet message with unparsable sp_address"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection(
|
||||
peer_map: PeerMap,
|
||||
raw_stream: TcpStream,
|
||||
addr: SocketAddr,
|
||||
sp_client: Arc<Mutex<SpClient>>,
|
||||
sp_outputs: Arc<Mutex<OutputList>>,
|
||||
daemon: Arc<Mutex<Daemon>>,
|
||||
) {
|
||||
debug!("Incoming TCP connection from: {}", addr);
|
||||
|
||||
let ws_stream = tokio_tungstenite::accept_async(raw_stream)
|
||||
@ -170,53 +468,46 @@ async fn handle_connection(peer_map: PeerMap, raw_stream: TcpStream, addr: Socke
|
||||
let broadcast_incoming = incoming.try_for_each({
|
||||
let peer_map = peer_map.clone();
|
||||
move |msg| {
|
||||
match msg.is_text() {
|
||||
true => {
|
||||
let msg_str = msg.to_string();
|
||||
match msg_str.starts_with("faucet") {
|
||||
true => {
|
||||
match SilentPaymentAddress::try_from(&msg_str["faucet".len()..]) {
|
||||
Ok(sp_address) => {
|
||||
// send bootstrap coins to this sp_address
|
||||
match faucet_send(sp_address, sp_client.clone(), daemon.clone()) {
|
||||
if msg.is_text() {
|
||||
if msg.to_string().starts_with("faucet") {
|
||||
match handle_faucet_request(
|
||||
&msg.to_string(),
|
||||
sp_client.clone(),
|
||||
sp_outputs.clone(),
|
||||
daemon.clone(),
|
||||
) {
|
||||
Ok(txid) => {
|
||||
log::info!("New faucet payment: {}", txid);
|
||||
},
|
||||
if let Err(e) = broadcast_message(
|
||||
peer_map.clone(),
|
||||
Message::Text(format!("faucet{}", txid.to_string())),
|
||||
BroadcastType::Sender(addr),
|
||||
) {
|
||||
log::error!("Failed to broadcast message: {}", e.to_string());
|
||||
} else {
|
||||
log::debug!("Successfully broadcasted message: {}", txid);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("faucet failed with error {}", e);
|
||||
let peers = peer_map.lock().unwrap();
|
||||
|
||||
let (_, peer_tx) = peers
|
||||
.iter()
|
||||
.find(|(peer_addr, _)| peer_addr == &&addr)
|
||||
.unwrap();
|
||||
|
||||
let _ = peer_tx.send(Message::Text(format!("RELAY_ERROR: {}", e)));
|
||||
if let Err(e) = broadcast_message(
|
||||
peer_map.clone(),
|
||||
Message::Text(e.to_string()),
|
||||
BroadcastType::Sender(addr),
|
||||
) {
|
||||
log::error!("Failed to broadcast message: {}", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(_) => {
|
||||
log::error!("faucet message with unparsable sp_address received from {}", addr);
|
||||
}
|
||||
} else {
|
||||
// we just send it `as is` to everyone except sender
|
||||
if let Err(e) =
|
||||
broadcast_message(peer_map.clone(), msg, BroadcastType::ExcludeSender(addr))
|
||||
{
|
||||
log::error!("Failed to broadcast message: {}", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
false => {
|
||||
let peers = peer_map.lock().unwrap();
|
||||
|
||||
// Broadcast message to other peers
|
||||
peers
|
||||
.iter()
|
||||
.filter(|(peer_addr, _)| peer_addr != &&addr)
|
||||
.for_each(|(_, peer_tx)| {
|
||||
let _ = peer_tx.send(msg.clone());
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
false => {
|
||||
} else {
|
||||
// we don't care
|
||||
log::debug!("Received non-text message from peer {}", addr);
|
||||
}
|
||||
log::debug!("Received non-text message {} from peer {}", msg, addr);
|
||||
}
|
||||
future::ok(())
|
||||
}
|
||||
@ -248,8 +539,11 @@ fn flatten_msg(parts: &[Vec<u8>]) -> Vec<u8> {
|
||||
final_vec
|
||||
}
|
||||
|
||||
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())?;
|
||||
fn process_raw_tx_message(
|
||||
core_msg: &bitcoincore_zmq::Message,
|
||||
daemon: Arc<Mutex<Daemon>>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let tx: Transaction = deserialize(&core_msg.serialize_data_to_vec())?;
|
||||
|
||||
if tx.is_coinbase() {
|
||||
return Err(Error::msg("Can't process coinbase transaction"));
|
||||
@ -258,22 +552,30 @@ fn process_raw_tx_message(core_msg: &bitcoincore_zmq::Message, daemon: Arc<Mutex
|
||||
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()
|
||||
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"))?;
|
||||
.map_err(|e| Error::msg(format!("Failed to find previous transaction: {}", e)))?;
|
||||
|
||||
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) {
|
||||
match get_pubkey_from_input(
|
||||
&input.script_sig.to_bytes(),
|
||||
&input.witness.to_vec(),
|
||||
&output.script_pubkey.to_bytes(),
|
||||
) {
|
||||
Ok(Some(pubkey)) => pubkeys.push(pubkey),
|
||||
Ok(None) => continue,
|
||||
Err(e) => return Err(Error::msg(format!("Can't extract pubkey from input: {}", e))),
|
||||
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"));
|
||||
@ -281,13 +583,16 @@ fn process_raw_tx_message(core_msg: &bitcoincore_zmq::Message, daemon: Arc<Mutex
|
||||
}
|
||||
|
||||
let input_pub_keys: Vec<&PublicKey> = pubkeys.iter().collect();
|
||||
match silentpayments::utils::receiving::recipient_calculate_tweak_data(&input_pub_keys, &outpoints) {
|
||||
match 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())))
|
||||
}
|
||||
Err(e) => Err(Error::msg(format!(
|
||||
"Failed to compute tweak data: {}",
|
||||
e.to_string()
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
@ -303,73 +608,140 @@ async fn handle_zmq(peer_map: PeerMap, daemon: Arc<Mutex<Daemon>>) {
|
||||
}
|
||||
};
|
||||
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
|
||||
Err(_) => continue,
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
flatten_msg(&core_msg.serialize_to_vecs())
|
||||
}
|
||||
_ => flatten_msg(&core_msg.serialize_to_vecs()),
|
||||
};
|
||||
|
||||
for tx in peers.values() {
|
||||
let _ = tx.send(Message::Binary(payload.clone()));
|
||||
if let Err(e) = broadcast_message(
|
||||
peer_map.clone(),
|
||||
Message::Binary(payload),
|
||||
BroadcastType::ToAll,
|
||||
) {
|
||||
log::error!("{}", e.to_string());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() -> Result<(), IoError> {
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
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());
|
||||
.unwrap_or_else(|| "127.0.0.1:8090".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 core_wallet: Option<String> = env::args().nth(4);
|
||||
|
||||
let state = PeerMap::new(Mutex::new(HashMap::new()));
|
||||
|
||||
// Connect the rpc daemon
|
||||
let daemon = Daemon::connect().unwrap();
|
||||
let daemon = Daemon::connect(core_wallet).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");
|
||||
let current_tip: u32 = daemon
|
||||
.get_current_height()
|
||||
.expect("Failed to make rpc call")
|
||||
.try_into()
|
||||
.expect("block count is higher than u32::MAX");
|
||||
|
||||
let mut config_dir = PathBuf::from_str(&env::var("HOME")?)?;
|
||||
config_dir.push(".4nk");
|
||||
let sp_client_file = JsonFile::new(&config_dir, wallet_name.clone())?;
|
||||
let sp_outputs_file = JsonFile::new(&config_dir, format!("{}.db", wallet_name))?;
|
||||
|
||||
// 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,
|
||||
let (sp_client, sp_outputs) = match <JsonFile as Storage<SpClient>>::load(&sp_client_file) {
|
||||
Ok(existing) => {
|
||||
if let Ok(our_outputs) = <JsonFile as Storage<OutputList>>::load(&sp_outputs_file) {
|
||||
(existing, our_outputs)
|
||||
} else {
|
||||
let our_address = SilentPaymentAddress::try_from(existing.get_receiving_address())?;
|
||||
let new_outputs = OutputList::new(
|
||||
our_address.get_scan_key(),
|
||||
our_address.get_spend_key(),
|
||||
current_tip,
|
||||
);
|
||||
sp_outputs_file.save(&new_outputs)?;
|
||||
(existing, new_outputs)
|
||||
}
|
||||
}
|
||||
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(
|
||||
let (scan_sk, spend_sk) = derive_keys_from_seed(&seed, is_testnet)
|
||||
.expect("Couldn't generate a new sp_wallet");
|
||||
let new_client = SpClient::new(
|
||||
wallet_name,
|
||||
scan_sk,
|
||||
SpendKey::Secret(spend_sk),
|
||||
None,
|
||||
is_testnet,
|
||||
)
|
||||
.expect("Failed to create a new SpClient");
|
||||
|
||||
let new_address = SilentPaymentAddress::try_from(new_client.get_receiving_address())?;
|
||||
|
||||
let new_outputs = OutputList::new(
|
||||
new_address.get_scan_key(),
|
||||
new_address.get_spend_key(),
|
||||
current_tip,
|
||||
is_testnet
|
||||
).expect("Failed to create a new SpClient")
|
||||
);
|
||||
|
||||
sp_client_file.save(&new_client)?;
|
||||
sp_outputs_file.save(&new_outputs)?;
|
||||
|
||||
(new_client, new_outputs)
|
||||
}
|
||||
};
|
||||
|
||||
log::info!("Using wallet {} with address {}", sp_client.label, sp_client.get_receiving_address());
|
||||
log::info!(
|
||||
"Using wallet {} with address {}",
|
||||
sp_client.label,
|
||||
sp_client.get_receiving_address()
|
||||
);
|
||||
|
||||
log::info!(
|
||||
"Found {} outputs for a total balance of {}",
|
||||
sp_outputs.to_spendable_list().len(),
|
||||
sp_outputs.get_balance()
|
||||
);
|
||||
|
||||
let last_scan = sp_outputs.get_last_scan();
|
||||
|
||||
let shared_sp_client = Arc::new(Mutex::new(sp_client));
|
||||
let shared_daemon = Arc::new(Mutex::new(daemon));
|
||||
let shared_sp_client = Arc::new(Mutex::new(sp_client));
|
||||
let shared_sp_outputs = Arc::new(Mutex::new(sp_outputs));
|
||||
|
||||
if last_scan < current_tip {
|
||||
log::info!("Scanning for our outputs");
|
||||
match scan_blocks(
|
||||
shared_sp_client.clone(),
|
||||
shared_daemon.clone(),
|
||||
shared_sp_outputs.clone(),
|
||||
current_tip - last_scan,
|
||||
) {
|
||||
Ok(()) => {
|
||||
let updated = shared_sp_outputs
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e)))?;
|
||||
sp_outputs_file.save(updated.deref())?;
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
}
|
||||
|
||||
// Subscribe to Bitcoin Core
|
||||
tokio::spawn(handle_zmq(state.clone(), shared_daemon.clone()));
|
||||
@ -381,7 +753,14 @@ 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, shared_sp_client.clone(), shared_daemon.clone()));
|
||||
tokio::spawn(handle_connection(
|
||||
state.clone(),
|
||||
stream,
|
||||
addr,
|
||||
shared_sp_client.clone(),
|
||||
shared_sp_outputs.clone(),
|
||||
shared_daemon.clone(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
302
src/scan.rs
Normal file
302
src/scan.rs
Normal file
@ -0,0 +1,302 @@
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::{Error, Result};
|
||||
use electrum_client::ElectrumApi;
|
||||
use hex::FromHex;
|
||||
use sp_backend::bitcoin::bip158::BlockFilter;
|
||||
use sp_backend::bitcoin::hex::DisplayHex;
|
||||
use sp_backend::bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey};
|
||||
use sp_backend::bitcoin::{BlockHash, OutPoint, Transaction, TxOut, XOnlyPublicKey};
|
||||
use sp_backend::silentpayments::receiving::Receiver;
|
||||
use sp_backend::spclient::{OutputList, OutputSpendStatus, OwnedOutput, SpClient};
|
||||
use tokio::time::Instant;
|
||||
|
||||
use crate::{electrumclient, Daemon};
|
||||
|
||||
fn get_script_to_secret_map(
|
||||
sp_receiver: &Receiver,
|
||||
tweak_data_vec: Vec<String>,
|
||||
scan_key_scalar: Scalar,
|
||||
secp: &Secp256k1<All>,
|
||||
) -> Result<HashMap<[u8; 34], PublicKey>> {
|
||||
let mut res = HashMap::new();
|
||||
let shared_secrets: Result<Vec<PublicKey>> = tweak_data_vec
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
let x = PublicKey::from_str(&s).map_err(Error::new)?;
|
||||
x.mul_tweak(secp, &scan_key_scalar).map_err(Error::new)
|
||||
})
|
||||
.collect();
|
||||
let shared_secrets = shared_secrets?;
|
||||
|
||||
for shared_secret in shared_secrets {
|
||||
let spks = sp_receiver.get_spks_from_shared_secret(&shared_secret)?;
|
||||
|
||||
for spk in spks.into_values() {
|
||||
res.insert(spk, shared_secret);
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn check_block(
|
||||
blkfilter: BlockFilter,
|
||||
blkhash: BlockHash,
|
||||
candidate_spks: Vec<&[u8; 34]>,
|
||||
owned_spks: Vec<Vec<u8>>,
|
||||
) -> Result<bool> {
|
||||
// check output scripts
|
||||
let mut scripts_to_match: Vec<_> = candidate_spks.into_iter().map(|spk| spk.as_ref()).collect();
|
||||
|
||||
// check input scripts
|
||||
scripts_to_match.extend(owned_spks.iter().map(|spk| spk.as_slice()));
|
||||
|
||||
// note: match will always return true for an empty query!
|
||||
if !scripts_to_match.is_empty() {
|
||||
Ok(blkfilter.match_any(&blkhash, &mut scripts_to_match.into_iter())?)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn scan_block_outputs(
|
||||
sp_receiver: &Receiver,
|
||||
txdata: &Vec<Transaction>,
|
||||
blkheight: u64,
|
||||
spk2secret: HashMap<[u8; 34], PublicKey>,
|
||||
) -> Result<HashMap<OutPoint, OwnedOutput>> {
|
||||
let mut res: HashMap<OutPoint, OwnedOutput> = HashMap::new();
|
||||
|
||||
// loop over outputs
|
||||
for tx in txdata {
|
||||
let txid = tx.txid();
|
||||
|
||||
// collect all taproot outputs from transaction
|
||||
let p2tr_outs: Vec<(usize, &TxOut)> = tx
|
||||
.output
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, o)| o.script_pubkey.is_p2tr())
|
||||
.collect();
|
||||
|
||||
if p2tr_outs.is_empty() {
|
||||
continue;
|
||||
}; // no taproot output
|
||||
|
||||
let mut secret: Option<PublicKey> = None;
|
||||
// Does this transaction contains one of the outputs we already found?
|
||||
for spk in p2tr_outs.iter().map(|(_, o)| &o.script_pubkey) {
|
||||
if let Some(s) = spk2secret.get(spk.as_bytes()) {
|
||||
// we might have at least one output in this transaction
|
||||
secret = Some(*s);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if secret.is_none() {
|
||||
continue;
|
||||
}; // we don't have a secret that matches any of the keys
|
||||
|
||||
// Now we can just run sp_receiver on all the p2tr outputs
|
||||
let xonlykeys: Result<Vec<XOnlyPublicKey>> = p2tr_outs
|
||||
.iter()
|
||||
.map(|(_, o)| {
|
||||
XOnlyPublicKey::from_slice(&o.script_pubkey.as_bytes()[2..]).map_err(Error::new)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let ours = sp_receiver.scan_transaction(&secret.unwrap(), xonlykeys?)?;
|
||||
for (label, map) in ours {
|
||||
res.extend(p2tr_outs.iter().filter_map(|(i, o)| {
|
||||
match XOnlyPublicKey::from_slice(&o.script_pubkey.as_bytes()[2..]) {
|
||||
Ok(key) => {
|
||||
if let Some(scalar) = map.get(&key) {
|
||||
match SecretKey::from_slice(&scalar.to_be_bytes()) {
|
||||
Ok(tweak) => {
|
||||
let outpoint = OutPoint {
|
||||
txid,
|
||||
vout: *i as u32,
|
||||
};
|
||||
let label_str: Option<String>;
|
||||
if let Some(l) = &label {
|
||||
label_str =
|
||||
Some(l.as_inner().to_be_bytes().to_lower_hex_string());
|
||||
} else {
|
||||
label_str = None;
|
||||
}
|
||||
return Some((
|
||||
outpoint,
|
||||
OwnedOutput {
|
||||
blockheight: blkheight as u32,
|
||||
tweak: hex::encode(tweak.secret_bytes()),
|
||||
amount: o.value,
|
||||
script: hex::encode(o.script_pubkey.as_bytes()),
|
||||
label: label_str,
|
||||
spend_status: OutputSpendStatus::Unspent,
|
||||
},
|
||||
));
|
||||
}
|
||||
Err(_) => {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn scan_block_inputs(
|
||||
our_outputs: HashMap<OutPoint, OwnedOutput>,
|
||||
txdata: Vec<Transaction>,
|
||||
) -> Result<Vec<OutPoint>> {
|
||||
let mut found = vec![];
|
||||
|
||||
for tx in txdata {
|
||||
for input in tx.input {
|
||||
let prevout = input.previous_output;
|
||||
|
||||
if our_outputs.contains_key(&prevout) {
|
||||
found.push(prevout);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
pub fn scan_blocks(
|
||||
sp_client: Arc<Mutex<SpClient>>,
|
||||
daemon: Arc<Mutex<Daemon>>,
|
||||
sp_outputs: Arc<Mutex<OutputList>>,
|
||||
mut n_blocks_to_scan: u32,
|
||||
) -> anyhow::Result<()> {
|
||||
log::info!("Starting a rescan");
|
||||
let electrum_client = electrumclient::create_electrum_client()?;
|
||||
|
||||
let core = daemon
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e.to_string())))?;
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
let scan_height = sp_outputs
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e.to_string())))?
|
||||
.get_last_scan();
|
||||
let tip_height: u32 = core.get_current_height()?.try_into()?;
|
||||
|
||||
// 0 means scan to tip
|
||||
if n_blocks_to_scan == 0 {
|
||||
n_blocks_to_scan = tip_height - scan_height;
|
||||
}
|
||||
|
||||
let start = scan_height + 1;
|
||||
let end = if scan_height + n_blocks_to_scan <= tip_height {
|
||||
scan_height + n_blocks_to_scan
|
||||
} else {
|
||||
tip_height
|
||||
};
|
||||
|
||||
if start > end {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::info!("start: {} end: {}", start, end);
|
||||
let mut filters: Vec<(u32, BlockHash, BlockFilter)> = vec![];
|
||||
for blkheight in start..=end {
|
||||
filters.push(core.get_filters(blkheight)?);
|
||||
}
|
||||
|
||||
let mut tweak_data_map = electrum_client.sp_tweaks(start as usize)?;
|
||||
|
||||
let scan_sk = sp_client
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e.to_string())))?
|
||||
.get_scan_key();
|
||||
|
||||
let sp_receiver = sp_client
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e.to_string())))?
|
||||
.sp_receiver
|
||||
.clone();
|
||||
let start_time = Instant::now();
|
||||
|
||||
for (blkheight, blkhash, blkfilter) in filters {
|
||||
let spk2secret = match tweak_data_map.remove(&(&blkheight)) {
|
||||
Some(tweak_data_vec) => {
|
||||
get_script_to_secret_map(&sp_receiver, tweak_data_vec, scan_sk.into(), &secp)?
|
||||
}
|
||||
None => HashMap::new(),
|
||||
};
|
||||
|
||||
// check if new possible outputs are payments to us
|
||||
let candidate_spks: Vec<&[u8; 34]> = spk2secret.keys().collect();
|
||||
|
||||
// check if owned inputs are spent
|
||||
let our_outputs: HashMap<OutPoint, OwnedOutput> = sp_outputs
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e.to_string())))?
|
||||
.to_outpoints_list();
|
||||
|
||||
let owned_spks: Result<Vec<Vec<u8>>> = our_outputs
|
||||
.iter()
|
||||
.map(|(_, output)| {
|
||||
let script = Vec::from_hex(&output.script).map_err(|e| Error::new(e));
|
||||
script
|
||||
})
|
||||
.collect();
|
||||
|
||||
let matched = check_block(blkfilter, blkhash, candidate_spks, owned_spks?)?;
|
||||
|
||||
if matched {
|
||||
let blk = core.get_block(blkhash)?;
|
||||
|
||||
// scan block for new outputs, and add them to our list
|
||||
let utxo_created_in_block =
|
||||
scan_block_outputs(&sp_receiver, &blk.txdata, blkheight.into(), spk2secret)?;
|
||||
if !utxo_created_in_block.is_empty() {
|
||||
sp_outputs
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e)))?
|
||||
.extend_from(utxo_created_in_block);
|
||||
}
|
||||
|
||||
// update the list of outputs just in case
|
||||
// utxos may be created and destroyed in the same block
|
||||
let updated_outputs: HashMap<OutPoint, OwnedOutput> = sp_outputs
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e.to_string())))?
|
||||
.to_outpoints_list();
|
||||
|
||||
// search inputs and mark as mined
|
||||
let utxo_destroyed_in_block = scan_block_inputs(updated_outputs, blk.txdata)?;
|
||||
if !utxo_destroyed_in_block.is_empty() {
|
||||
let mut outputs = sp_outputs
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e)))?;
|
||||
for outpoint in utxo_destroyed_in_block {
|
||||
outputs.mark_mined(outpoint, blkhash)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// time elapsed for the scan
|
||||
log::info!(
|
||||
"Scan complete in {} seconds",
|
||||
start_time.elapsed().as_secs()
|
||||
);
|
||||
|
||||
// update last_scan height
|
||||
sp_outputs
|
||||
.lock()
|
||||
.map_err(|e| Error::msg(format!("Failed to lock daemon: {}", e)))?
|
||||
.update_last_scan(end);
|
||||
Ok(())
|
||||
}
|
155
src/sp.rs
155
src/sp.rs
@ -1,155 +0,0 @@
|
||||
use bitcoin::secp256k1::{Parity::Even, PublicKey, XOnlyPublicKey};
|
||||
use bitcoin::hashes::{hash160, Hash};
|
||||
|
||||
use anyhow::Error;
|
||||
|
||||
// ** Putting all the pubkey extraction logic in the test utils for now. **
|
||||
// NUMS_H (defined in BIP340)
|
||||
const NUMS_H: [u8; 32] = [
|
||||
0x50, 0x92, 0x9b, 0x74, 0xc1, 0xa0, 0x49, 0x54, 0xb7, 0x8b, 0x4b, 0x60, 0x35, 0xe9, 0x7a, 0x5e,
|
||||
0x07, 0x8a, 0x5a, 0x0f, 0x28, 0xec, 0x96, 0xd5, 0x47, 0xbf, 0xee, 0x9a, 0xce, 0x80, 0x3a, 0xc0,
|
||||
];
|
||||
|
||||
// Define OP_CODES used in script template matching for readability
|
||||
const OP_1: u8 = 0x51;
|
||||
const OP_0: u8 = 0x00;
|
||||
const OP_PUSHBYTES_20: u8 = 0x14;
|
||||
const OP_PUSHBYTES_32: u8 = 0x20;
|
||||
const OP_HASH160: u8 = 0xA9;
|
||||
const OP_EQUAL: u8 = 0x87;
|
||||
const OP_DUP: u8 = 0x76;
|
||||
const OP_EQUALVERIFY: u8 = 0x88;
|
||||
const OP_CHECKSIG: u8 = 0xAC;
|
||||
|
||||
// Only compressed pubkeys are supported for silent payments
|
||||
const COMPRESSED_PUBKEY_SIZE: usize = 33;
|
||||
|
||||
pub struct VinData {
|
||||
pub script_sig: Vec<u8>,
|
||||
pub txinwitness: Vec<Vec<u8>>,
|
||||
pub script_pub_key: Vec<u8>,
|
||||
}
|
||||
|
||||
// script templates for inputs allowed in BIP352 shared secret derivation
|
||||
pub fn is_p2tr(spk: &[u8]) -> bool {
|
||||
matches!(spk, [OP_1, OP_PUSHBYTES_32, ..] if spk.len() == 34)
|
||||
}
|
||||
|
||||
fn is_p2wpkh(spk: &[u8]) -> bool {
|
||||
matches!(spk, [OP_0, OP_PUSHBYTES_20, ..] if spk.len() == 22)
|
||||
}
|
||||
|
||||
fn is_p2sh(spk: &[u8]) -> bool {
|
||||
matches!(spk, [OP_HASH160, OP_PUSHBYTES_20, .., OP_EQUAL] if spk.len() == 23)
|
||||
}
|
||||
|
||||
fn is_p2pkh(spk: &[u8]) -> bool {
|
||||
matches!(spk, [OP_DUP, OP_HASH160, OP_PUSHBYTES_20, .., OP_EQUALVERIFY, OP_CHECKSIG] if spk.len() == 25)
|
||||
}
|
||||
|
||||
pub fn get_pubkey_from_input(vin: &VinData) -> Result<Option<PublicKey>, Error> {
|
||||
if is_p2pkh(&vin.script_pub_key) {
|
||||
match (&vin.txinwitness.is_empty(), &vin.script_sig.is_empty()) {
|
||||
(true, false) => {
|
||||
let spk_hash = &vin.script_pub_key[3..23];
|
||||
for i in (COMPRESSED_PUBKEY_SIZE..=vin.script_sig.len()).rev() {
|
||||
if let Some(pubkey_bytes) = &vin.script_sig.get(i - COMPRESSED_PUBKEY_SIZE..i) {
|
||||
let pubkey_hash = hash160::Hash::hash(pubkey_bytes);
|
||||
if pubkey_hash.to_byte_array() == spk_hash {
|
||||
return Ok(Some(PublicKey::from_slice(pubkey_bytes)?));
|
||||
}
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
(_, true) => return Err(Error::msg("Empty script_sig for spending a p2pkh")),
|
||||
(false, _) => return Err(Error::msg("non empty witness for spending a p2pkh")),
|
||||
}
|
||||
} else if is_p2sh(&vin.script_pub_key) {
|
||||
match (&vin.txinwitness.is_empty(), &vin.script_sig.is_empty()) {
|
||||
(false, false) => {
|
||||
let redeem_script = &vin.script_sig[1..];
|
||||
if is_p2wpkh(redeem_script) {
|
||||
if let Some(value) = vin.txinwitness.last() {
|
||||
if let Ok(pubkey) = PublicKey::from_slice(value) {
|
||||
return Ok(Some(pubkey));
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(_, true) => {
|
||||
return Err(Error::msg(
|
||||
"Empty script_sig for spending a p2sh".to_owned(),
|
||||
))
|
||||
}
|
||||
(true, false) => {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
} else if is_p2wpkh(&vin.script_pub_key) {
|
||||
match (&vin.txinwitness.is_empty(), &vin.script_sig.is_empty()) {
|
||||
(false, true) => {
|
||||
if let Some(value) = vin.txinwitness.last() {
|
||||
if let Ok(pubkey) = PublicKey::from_slice(value) {
|
||||
return Ok(Some(pubkey));
|
||||
} else {
|
||||
return Ok(None);
|
||||
}
|
||||
} else {
|
||||
return Err(Error::msg("Empty witness".to_owned()));
|
||||
}
|
||||
}
|
||||
(_, false) => {
|
||||
return Err(Error::msg(
|
||||
"Non empty script sig for spending a segwit output".to_owned(),
|
||||
))
|
||||
}
|
||||
(true, _) => {
|
||||
return Err(Error::msg(
|
||||
"Empty witness for spending a segwit output".to_owned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
} else if is_p2tr(&vin.script_pub_key) {
|
||||
match (&vin.txinwitness.is_empty(), &vin.script_sig.is_empty()) {
|
||||
(false, true) => {
|
||||
// check for the optional annex
|
||||
let annex = match vin.txinwitness.last().and_then(|value| value.get(0)) {
|
||||
Some(&0x50) => 1,
|
||||
Some(_) => 0,
|
||||
None => return Err(Error::msg("Empty or invalid witness".to_owned())),
|
||||
};
|
||||
|
||||
// Check for script path
|
||||
let stack_size = vin.txinwitness.len();
|
||||
if stack_size > annex && vin.txinwitness[stack_size - annex - 1][1..33] == NUMS_H {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Return the pubkey from the script pubkey
|
||||
return XOnlyPublicKey::from_slice(&vin.script_pub_key[2..34])
|
||||
.map_err(|e| Error::new(e))
|
||||
.map(|x_only_public_key| {
|
||||
Some(PublicKey::from_x_only_public_key(x_only_public_key, Even))
|
||||
});
|
||||
}
|
||||
(_, false) => {
|
||||
return Err(Error::msg(
|
||||
"Non empty script sig for spending a segwit output".to_owned(),
|
||||
))
|
||||
}
|
||||
(true, _) => {
|
||||
return Err(Error::msg(
|
||||
"Empty witness for spending a segwit output".to_owned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// We don't support this kind of output
|
||||
return Err(Error::msg("Unsupported script pubkey type"));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
787
src/spclient.rs
787
src/spclient.rs
@ -1,787 +0,0 @@
|
||||
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: &[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
|
||||
}
|
||||
|
||||
pub(crate) 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))
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user