From c2b23af823b0721d31e56a8d5359821f6bb4d249 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Fri, 1 Dec 2023 14:24:31 -0600 Subject: [PATCH] DLCs --- Cargo.lock | 551 +++++++++++++++---------- mutiny-core/Cargo.toml | 6 + mutiny-core/src/dlc/mod.rs | 393 ++++++++++++++++++ mutiny-core/src/dlc/storage.rs | 379 +++++++++++++++++ mutiny-core/src/error.rs | 12 + mutiny-core/src/lib.rs | 319 +++++++++++++- mutiny-core/src/node.rs | 2 +- mutiny-core/src/nodemanager.rs | 2 +- mutiny-core/src/nostr/dlc.rs | 242 +++++++++++ mutiny-core/src/nostr/mod.rs | 42 +- mutiny-core/src/nostr/nwc.rs | 29 +- mutiny-core/src/onchain.rs | 4 +- mutiny-core/src/storage.rs | 10 + mutiny-core/src/test_utils.rs | 30 ++ mutiny-core/src/utils.rs | 20 +- mutiny-core/test_inputs/dlc_offer.json | 163 ++++++++ mutiny-wasm/src/indexed_db.rs | 73 ++-- mutiny-wasm/src/lib.rs | 105 ++++- mutiny-wasm/src/models.rs | 260 ++++++++++++ 19 files changed, 2358 insertions(+), 284 deletions(-) create mode 100644 mutiny-core/src/dlc/mod.rs create mode 100644 mutiny-core/src/dlc/storage.rs create mode 100644 mutiny-core/src/nostr/dlc.rs create mode 100644 mutiny-core/test_inputs/dlc_offer.json diff --git a/Cargo.lock b/Cargo.lock index bb98f335d..7550f75b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,15 +95,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355" [[package]] name = "aquamarine" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df752953c49ce90719c7bf1fc587bc8227aed04732ea0c0f85e5397d7fdbd1a1" +checksum = "d1da02abba9f9063d786eab1509833ebb2fac0f966862ca59439c76b9c566760" dependencies = [ "include_dir", "itertools 0.10.5", @@ -159,7 +159,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -181,18 +181,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "fdf6721fb0140e4f897002dd086c06f6c27775df19cfe1fccb21181a48fd2c98" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -311,6 +311,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bdk_coin_select" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0320167c3655e83f0415d52f39618902e449186ffc7dfb090f922f79675c316" + [[package]] name = "bdk_esplora" version = "0.3.0" @@ -367,7 +373,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b9532c632b068e45a478f5e309126b6e2ec1dbf0bbd327b73836f33d9a43ede" dependencies = [ - "bitcoin 0.30.1", + "bitcoin 0.30.2", "percent-encoding-rfc3986", ] @@ -401,9 +407,9 @@ dependencies = [ [[package]] name = "bitcoin" -version = "0.30.1" +version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e99ff7289b20a7385f66a0feda78af2fc119d28fb56aea8886a9cd0a4abdd75" +checksum = "1945a5048598e4189e239d3f809b19bdad4845c4b2ba400d304d2dcf26d2c462" dependencies = [ "base64 0.13.1", "bech32", @@ -549,9 +555,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9897ef0f1bd2362169de6d7e436ea2237dc1085d7d1e4db75f4be34d86f309d1" +checksum = "26d4d6dafc1a3bb54687538972158f07b2c948bc57d5890df22c0739098b3028" dependencies = [ "borsh-derive", "cfg_aliases", @@ -559,15 +565,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478b41ff04256c5c8330f3dfdaaae2a5cc976a8e75088bafa4625b0d0208de8c" +checksum = "bf4918709cc4dd777ad2b6303ed03cb37f3ca0ccede8c1b0d28ac6db8f4710e0" dependencies = [ "once_cell", - "proc-macro-crate 2.0.0", + "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", "syn_derive", ] @@ -670,7 +676,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -731,9 +737,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -741,9 +747,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "core2" @@ -823,9 +829,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "derive_utils" @@ -846,7 +852,7 @@ checksum = "9abcad25e9720609ccb3dcdb795d845e37d8ce34183330a9f48b03a1a71c8e21" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -875,6 +881,57 @@ dependencies = [ "subtle", ] +[[package]] +name = "dlc" +version = "0.4.0" +source = "git+https://github.com/benthecarman/rust-dlc?branch=mutiny#09904107517c2b5c55bfab2fb14f702841565aa3" +dependencies = [ + "bitcoin 0.29.2", + "miniscript", + "secp256k1-sys 0.6.1", + "secp256k1-zkp", + "serde", +] + +[[package]] +name = "dlc-manager" +version = "0.4.0" +source = "git+https://github.com/benthecarman/rust-dlc?branch=mutiny#09904107517c2b5c55bfab2fb14f702841565aa3" +dependencies = [ + "async-trait", + "bitcoin 0.29.2", + "dlc", + "dlc-messages", + "dlc-trie", + "lightning", + "log", + "secp256k1-zkp", + "serde", +] + +[[package]] +name = "dlc-messages" +version = "0.4.0" +source = "git+https://github.com/benthecarman/rust-dlc?branch=mutiny#09904107517c2b5c55bfab2fb14f702841565aa3" +dependencies = [ + "bitcoin 0.29.2", + "dlc", + "lightning", + "secp256k1-zkp", + "serde", +] + +[[package]] +name = "dlc-trie" +version = "0.4.0" +source = "git+https://github.com/benthecarman/rust-dlc?branch=mutiny#09904107517c2b5c55bfab2fb14f702841565aa3" +dependencies = [ + "bitcoin 0.29.2", + "dlc", + "secp256k1-zkp", + "serde", +] + [[package]] name = "downcast" version = "0.11.0" @@ -922,12 +979,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -984,7 +1041,7 @@ dependencies = [ "argon2", "hex", "rand", - "ring 0.17.5", + "ring 0.17.7", ] [[package]] @@ -1045,7 +1102,7 @@ dependencies = [ "futures", "itertools 0.10.5", "rand", - "ring 0.17.5", + "ring 0.17.7", "secp256k1-zkp", "serde", "serde_json", @@ -1069,7 +1126,7 @@ dependencies = [ "bech32", "bincode", "bitcoin 0.29.2", - "bitcoin 0.30.1", + "bitcoin 0.30.2", "bitcoin_hashes 0.11.0", "bitvec", "erased-serde", @@ -1127,7 +1184,7 @@ dependencies = [ "fedimint-core", "fedimint-hkdf", "fedimint-tbs", - "ring 0.17.5", + "ring 0.17.7", "secp256k1-zkp", ] @@ -1415,9 +1472,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -1502,7 +1559,7 @@ checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -1557,9 +1614,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "js-sys", @@ -1580,9 +1637,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gloo-net" @@ -1739,9 +1796,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ "bytes", "fnv", @@ -1749,7 +1806,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap 2.1.0", "slab", "tokio", "tokio-util", @@ -1831,9 +1888,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -1842,9 +1899,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -1865,9 +1922,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -1880,7 +1937,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2", "tokio", "tower-service", "tracing", @@ -1896,7 +1953,7 @@ dependencies = [ "futures-util", "http", "hyper", - "rustls 0.21.8", + "rustls 0.21.10", "tokio", "tokio-rustls 0.24.1", ] @@ -1975,6 +2032,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "if_chain" version = "1.0.2" @@ -2105,7 +2172,7 @@ checksum = "83abfc4e5361c9cb087172c7f6d46a4db47e3c4fccf5d9168599451f230765cd" dependencies = [ "derive_utils 0.13.2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -2128,15 +2195,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" -version = "0.3.65" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" dependencies = [ "wasm-bindgen", ] @@ -2274,9 +2341,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "libm" @@ -2356,9 +2423,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.10" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "lnurl-rs" @@ -2473,13 +2540,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2531,6 +2598,7 @@ dependencies = [ "bdk", "bdk-macros", "bdk_chain", + "bdk_coin_select", "bdk_esplora", "bincode", "bip39", @@ -2538,6 +2606,10 @@ dependencies = [ "cbc", "cfg-if", "chrono", + "dlc", + "dlc-manager", + "dlc-messages", + "dlc-trie", "esplora-client", "fedimint-bip39", "fedimint-client", @@ -2658,7 +2730,7 @@ dependencies = [ "aes", "base64 0.21.5", "bip39", - "bitcoin 0.30.1", + "bitcoin 0.30.2", "cbc", "chacha20", "getrandom", @@ -2779,9 +2851,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -2791,9 +2863,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.59" +version = "0.10.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33" +checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45" dependencies = [ "bitflags 2.4.1", "cfg-if", @@ -2812,7 +2884,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -2823,9 +2895,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.95" +version = "0.9.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9" +checksum = "c3eaad34cdd97d81de97964fc7f29e2d104f483840d906ef56daa1912338460b" dependencies = [ "cc", "libc", @@ -2861,9 +2933,9 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.6.5" +version = "3.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dec8a8073036902368c2cdc0387e85ff9a37054d7e7c98e592145e0c92cd4fb" +checksum = "881331e34fa842a2fb61cc2db9643a8fedc615e47cfcc52597d1af0db9a7e8fe" dependencies = [ "arrayvec", "bitvec", @@ -2875,11 +2947,11 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.6.5" +version = "3.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312270ee71e1cd70289dacf597cab7b207aa107d2f28191c2ae45b2ece18a260" +checksum = "be30eaf4b0a9fba5336683b38de57bb86d179a35862ba6bfcf57625d006bde5b" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate", "proc-macro2", "quote", "syn 1.0.109", @@ -2911,7 +2983,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -2949,7 +3021,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fac532e6caa3a192dd6017a88446c2a1014d31b66cc68f04c584a846a4cb0373" dependencies = [ "bip21", - "bitcoin 0.30.1", + "bitcoin 0.30.2", "log", "url", ] @@ -2968,9 +3040,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "percent-encoding-rfc3986" @@ -3005,7 +3077,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -3022,9 +3094,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" [[package]] name = "polyval" @@ -3076,21 +3148,12 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - -[[package]] -name = "proc-macro-crate" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a" dependencies = [ - "toml_edit 0.20.7", + "toml_datetime", + "toml_edit", ] [[package]] @@ -3119,9 +3182,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] @@ -3286,9 +3349,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.22" +version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ "base64 0.21.5", "bytes", @@ -3309,7 +3372,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.21.8", + "rustls 0.21.10", "rustls-pemfile", "serde", "serde_json", @@ -3360,26 +3423,27 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.5" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", "getrandom", "libc", "spin 0.9.8", "untrusted 0.9.0", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "rkyv" -version = "0.7.42" +version = "0.7.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58" +checksum = "527a97cdfef66f65998b5f3b637c26f5a5ec09cc52a3f9932313ac645f4190f5" dependencies = [ "bitvec", "bytecheck", + "bytes", "hashbrown 0.12.3", "ptr_meta", "rend", @@ -3391,9 +3455,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.42" +version = "0.7.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e06b915b5c230a17d7a736d1e2e63ee753c256a8614ef3f5147b13a4f5541d" +checksum = "b5c462a1328c8e67e4d6dbad1eb0355dd43e8ab432c6e227a43657f16ade5033" dependencies = [ "proc-macro2", "quote", @@ -3439,15 +3503,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.21" +version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -3464,12 +3528,12 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.8" +version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", - "ring 0.17.5", + "ring 0.17.7", "rustls-webpki", "sct", ] @@ -3489,7 +3553,7 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.5", + "ring 0.17.7", "untrusted 0.9.0", ] @@ -3501,9 +3565,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "schannel" @@ -3511,7 +3575,7 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -3532,7 +3596,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.5", + "ring 0.17.7", "untrusted 0.9.0", ] @@ -3667,9 +3731,9 @@ checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" [[package]] name = "serde" -version = "1.0.192" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] @@ -3696,13 +3760,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -3818,19 +3882,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" - -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", -] +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "socket2" @@ -3839,7 +3893,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -3918,9 +3972,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "5b7d0a2c048d661a1a59fcd7355baa232f7ed34e0ee4df2eef3c1c1c0d3852d8" dependencies = [ "proc-macro2", "quote", @@ -3936,7 +3990,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -3976,7 +4030,7 @@ dependencies = [ "fastrand", "redox_syscall", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -3987,22 +4041,22 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -4041,9 +4095,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.33.0" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ "backtrace", "bytes", @@ -4053,21 +4107,21 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.5", + "socket2", "tokio-macros", "tracing", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -4097,7 +4151,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.21.8", + "rustls 0.21.10", "tokio", ] @@ -4135,7 +4189,7 @@ checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", - "rustls 0.21.8", + "rustls 0.21.10", "tokio", "tokio-rustls 0.24.1", "tungstenite 0.20.1", @@ -4168,26 +4222,15 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" - -[[package]] -name = "toml_edit" -version = "0.19.15" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.1.0", - "toml_datetime", - "winnow", -] +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" [[package]] name = "toml_edit" -version = "0.20.7" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ "indexmap 2.1.0", "toml_datetime", @@ -4219,7 +4262,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -4263,9 +4306,9 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" @@ -4300,7 +4343,7 @@ dependencies = [ "httparse", "log", "rand", - "rustls 0.21.8", + "rustls 0.21.10", "sha1", "thiserror", "url", @@ -4315,9 +4358,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" [[package]] name = "unicode-ident" @@ -4358,12 +4401,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", - "idna", + "idna 0.5.0", "percent-encoding", "serde", ] @@ -4395,9 +4438,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.5.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ "getrandom", "wasm-bindgen", @@ -4409,7 +4452,7 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" dependencies = [ - "idna", + "idna 0.4.0", "lazy_static", "regex", "serde", @@ -4480,9 +4523,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -4490,24 +4533,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" dependencies = [ "cfg-if", "js-sys", @@ -4517,9 +4560,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4527,28 +4570,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "wasm-bindgen-test" -version = "0.3.38" +version = "0.3.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6433b7c56db97397842c46b67e11873eda263170afeb3a2dc74a7cb370fee0d" +checksum = "2cf9242c0d27999b831eae4767b2a146feb0b27d332d553e605864acd2afd403" dependencies = [ "console_error_panic_hook", "js-sys", @@ -4560,13 +4603,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.38" +version = "0.3.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "493fcbab756bb764fa37e6bee8cec2dd709eb4273d06d0c282a5e74275ded735" +checksum = "794645f5408c9a039fd09f4d113cdfb2e7eba5ff1956b07bcf701cf4b394fe89" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] [[package]] @@ -4599,9 +4642,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.65" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" dependencies = [ "js-sys", "wasm-bindgen", @@ -4613,15 +4656,15 @@ version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" dependencies = [ - "ring 0.17.5", + "ring 0.17.7", "untrusted 0.9.0", ] [[package]] name = "webpki-roots" -version = "0.25.2" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" [[package]] name = "winapi" @@ -4651,7 +4694,7 @@ version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -4660,7 +4703,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -4669,13 +4721,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -4684,47 +4751,89 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winnow" -version = "0.5.28" +version = "0.5.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c830786f7720c2fd27a1a0e27a709dbd3c4d009b56d098fc742d4f4eab91fe2" +checksum = "9b5c3db89721d50d0e2a673f5043fc4722f76dcc352d7b1ab8b8288bed4ed2c5" dependencies = [ "memchr", ] @@ -4736,7 +4845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -4750,9 +4859,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" dependencies = [ "zeroize_derive", ] @@ -4765,5 +4874,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.42", ] diff --git a/mutiny-core/Cargo.toml b/mutiny-core/Cargo.toml index 100b1f132..1be365981 100644 --- a/mutiny-core/Cargo.toml +++ b/mutiny-core/Cargo.toml @@ -20,6 +20,7 @@ bitcoin = { version = "0.29.2", default-features = false, features = ["std", "se bdk = { version = "=1.0.0-alpha.1" } bdk_esplora = { version = "=0.3.0", default-features = false, features = ["std", "async-https"] } bdk_chain = { version = "=0.5.0", features = ["std"] } +bdk_coin_select = "0.1.1" bdk-macros = "0.6.0" getrandom = { version = "0.2" } itertools = "0.11.0" @@ -68,6 +69,11 @@ futures = "0.3.25" thiserror = "1.0" anyhow = "1.0" +dlc = { git = "https://github.com/benthecarman/rust-dlc", branch = "mutiny", features = ["use-serde"] } +dlc-manager = { git = "https://github.com/benthecarman/rust-dlc", branch = "mutiny", features = ["use-serde"] } +dlc-messages = { git = "https://github.com/benthecarman/rust-dlc", branch = "mutiny", features = [ "use-serde"] } +dlc-trie = { git = "https://github.com/benthecarman/rust-dlc", branch = "mutiny", features = ["use-serde"] } + [dev-dependencies] wasm-bindgen-test = "0.3.33" mockall = "0.11.2" diff --git a/mutiny-core/src/dlc/mod.rs b/mutiny-core/src/dlc/mod.rs new file mode 100644 index 000000000..440d16694 --- /dev/null +++ b/mutiny-core/src/dlc/mod.rs @@ -0,0 +1,393 @@ +use crate::dlc::storage::DlcStorage; +use crate::error::MutinyError; +use crate::fees::MutinyFeeEstimator; +use crate::logging::MutinyLogger; +use crate::onchain::OnChainWallet; +use crate::storage::MutinyStorage; +use crate::utils; +use bdk::wallet::AddressIndex; +use bdk::SignOptions; +use bdk_chain::ConfirmationTime; +use bdk_coin_select::{Candidate, CoinSelector, Drain, FeeRate, Target}; +use bitcoin::psbt::PartiallySignedTransaction; +use bitcoin::secp256k1::{All, PublicKey, Secp256k1, SecretKey}; +use bitcoin::util::bip32::ChildNumber; +use bitcoin::{Address, Block, Network, OutPoint, Script, Transaction, Txid, XOnlyPublicKey}; +use dlc_manager::contract::signed_contract::SignedContract; +use dlc_manager::error::Error; +use dlc_manager::{Oracle, Signer, Storage, Time, Utxo}; +use dlc_messages::oracle_msgs::{OracleAnnouncement, OracleAttestation}; +use futures_util::lock::Mutex; +use lightning::log_error; +use lightning::util::logger::Logger; +use std::collections::HashMap; +use std::sync::Arc; + +mod storage; + +pub use storage::{DLC_CONTRACT_KEY_PREFIX, DLC_KEY_INDEX_KEY}; + +pub(crate) type DlcManager = dlc_manager::manager::Manager< + Arc>, + Arc>, + Arc>, + Arc, + Arc, + Arc>, +>; + +/// Handles DLC functionality in Mutiny. +#[derive(Clone)] +pub struct DlcHandler { + pub manager: Arc>>, + pub store: Arc>, + pub logger: Arc, +} + +impl DlcHandler { + pub fn new( + wallet: Arc>, + logger: Arc, + ) -> Result { + let store = Arc::new(DlcStorage::new(wallet.storage.clone())); + + let dlc_wallet = DlcWallet { + wallet: wallet.clone(), + storage: store.clone(), + logger: logger.clone(), + secp: Secp256k1::new(), + }; + + let manager = DlcManager::new( + Arc::new(dlc_wallet), + Arc::new(DlcBlockchain(wallet.clone())), + store.clone(), + HashMap::new(), + Arc::new(MutinyTimeProvider {}), + wallet.fees.clone(), + ) + .map_err(|e| anyhow::anyhow!("Failed to create dlc manager: {e}"))?; + + Ok(Self { + manager: Arc::new(Mutex::new(manager)), + store, + logger, + }) + } + + /// Outputs to watch for on the blockchain. This is used to detect when a contract is closed + /// by our counter party. + /// + /// If a contract is closed by our counter party, the [`on_counterparty_close`] method should + /// be called. + pub fn outputs_to_watch(&self) -> Result, Error> { + let contracts: Vec = self.store.get_confirmed_contracts()?; + + let outpoints = contracts + .into_iter() + .map(|c| (c.accepted_contract.dlc_transactions.get_fund_outpoint(), c)) + .collect(); + + Ok(outpoints) + } +} + +/// Converts a bdk error to a manager error +fn bdk_err_to_manager_err(e: bdk::Error) -> Error { + create_wallet_error(&format!("{:?}", e)) +} + +/// Creates a wallet error from a string +fn create_wallet_error(error: &str) -> Error { + Error::WalletError(Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + error, + ))) +} + +/// A wrapper around a bdk wallet that implements the different traits needed by the dlc manager +#[derive(Clone)] +pub struct DlcWallet { + pub wallet: Arc>, + pub storage: Arc>, + pub logger: Arc, + pub secp: Secp256k1, +} + +impl DlcWallet { + pub fn get_secret_key_for_index(&self, index: u32) -> SecretKey { + let network_index = if self.wallet.network == Network::Bitcoin { + ChildNumber::from_hardened_idx(0).expect("infallible") + } else { + ChildNumber::from_hardened_idx(1).expect("infallible") + }; + + let path = [ + ChildNumber::from_hardened_idx(586).expect("infallible"), + network_index, + ChildNumber::from_hardened_idx(index).unwrap(), + ]; + + self.wallet + .xprivkey + .derive_priv(&self.secp, &path) + .unwrap() + .private_key + } +} + +impl Signer for DlcWallet { + fn sign_psbt_input( + &self, + psbt: &mut PartiallySignedTransaction, + input_index: usize, + ) -> Result<(), Error> { + let Ok(wallet) = self.wallet.wallet.try_read() else { + log_error!(self.logger, "Could not get wallet lock to sign tx input"); + return Err(create_wallet_error( + "Failed to get wallet lock to sign tx input", + )); + }; + + let sig_options = SignOptions { + trust_witness_utxo: true, + ..Default::default() + }; + + let mut to_sign = psbt.clone(); + wallet.sign(&mut to_sign, sig_options).map_err(|e| { + log_error!(self.logger, "Failed to sign tx input: {e:?}"); + bdk_err_to_manager_err(e) + })?; + + // Since we can only sign the whole PSBT, we need to just copy over + // the one input we signed. + // https://github.com/bitcoindevkit/bdk/issues/1219 + psbt.inputs[input_index] = to_sign.inputs[input_index].clone(); + + Ok(()) + } + + fn get_secret_key_for_pubkey(&self, pk: &PublicKey) -> Result { + let index = self + .storage + .get_index_for_key(pk) + .map_err(|e| Error::WalletError(Box::new(e)))?; + + Ok(self.get_secret_key_for_index(index)) + } +} + +impl dlc_manager::Wallet for DlcWallet { + fn get_new_address(&self) -> Result { + let Ok(mut wallet) = self.wallet.wallet.try_write() else { + log_error!(self.logger, "Could not get wallet lock to get new address"); + return Err(create_wallet_error( + "Failed to get wallet lock to get new address", + )); + }; + + let address = wallet.get_address(AddressIndex::New).address; + Ok(address) + } + + fn get_new_change_address(&self) -> Result { + let Ok(mut wallet) = self.wallet.wallet.try_write() else { + log_error!( + self.logger, + "Could not get wallet lock to get new change address" + ); + return Err(create_wallet_error( + "Failed to get wallet lock to get new change address", + )); + }; + + let address = wallet.get_internal_address(AddressIndex::New).address; + Ok(address) + } + + fn get_new_secret_key(&self) -> Result { + let index = self.storage.get_next_key_index(); + let key = self.get_secret_key_for_index(index); + let pk = PublicKey::from_secret_key(&self.secp, &key); + self.storage + .add_new_key(pk, index) + .map_err(|e| Error::WalletError(Box::new(e)))?; + + Ok(key) + } + + fn get_utxos_for_amount( + &self, + amount: u64, + fee_rate: Option, + _lock_utxos: bool, + ) -> Result, Error> { + let utxos = self + .wallet + .list_utxos() + .map_err(|e| Error::WalletError(Box::new(e)))? + .into_iter() + // only use confirmed utxos + .filter(|u| matches!(u.confirmation_time, ConfirmationTime::Confirmed { .. })) + .collect::>(); + + let candidates = utxos + .iter() + .map(|u| Candidate::new_tr_keyspend(u.txout.value)) + .collect::>(); + + let target = Target { + feerate: FeeRate::from_sat_per_vb(fee_rate.unwrap_or(10) as f32), + min_fee: 0, + value: amount, + }; + + // base weight of 212 is standard for DLC transaction + let mut coin_selector = CoinSelector::new(&candidates, 212); + coin_selector + .select_until_target_met(target, Drain::none()) + .map_err(|e| { + log_error!(self.logger, "Failed to select coins: {e:?}"); + Error::WalletError(Box::new(e)) + })?; + + // Check that selection is finished! + debug_assert!(coin_selector.is_target_met(target, Drain::none())); + + // get indices of selected coins + let indices = coin_selector.selected_indices(); + + let mut selection: Vec = Vec::with_capacity(indices.len()); + for index in indices { + let utxo = &utxos[*index]; + + let address = + Address::from_script(&utxo.txout.script_pubkey, self.wallet.network).unwrap(); + let u = Utxo { + tx_out: utxo.txout.clone(), + outpoint: utxo.outpoint, + address, + redeem_script: Script::new(), + reserved: false, + }; + + selection.push(u); + } + + Ok(selection) + } + + fn import_address(&self, _address: &Address) -> Result<(), Error> { + // BDK does not support importing addresses which is fine. + // We will always see the funding tx spending our funds and we will be able to track the + // closing tx as well. + Ok(()) + } +} + +pub struct MutinyTimeProvider {} +impl Time for MutinyTimeProvider { + fn unix_time_now(&self) -> u64 { + utils::now().as_secs() + } +} + +pub struct DlcBlockchain(Arc>); + +impl dlc_manager::Blockchain for DlcBlockchain { + fn send_transaction(&self, transaction: &Transaction) -> Result<(), Error> { + let tx = transaction.clone(); + let wallet = self.0.clone(); + utils::spawn(async move { + if let Err(e) = wallet.broadcast_transaction(tx).await { + log_error!(wallet.logger, "Failed to broadcast transaction: {e}"); + } + }); + Ok(()) + } + + fn get_network(&self) -> Result { + Ok(self.0.network) + } + + fn get_blockchain_height(&self) -> Result { + let Ok(wallet) = self.0.wallet.try_read() else { + log_error!( + self.0.logger, + "Could not get wallet lock to get blockchain height" + ); + return Err(create_wallet_error( + "Failed to get wallet lock to get blockchain height", + )); + }; + + Ok(wallet + .latest_checkpoint() + .map(|c| c.height as u64) + .unwrap_or(0)) // if no checkpoint, then we assume 0 + } + + fn get_block_at_height(&self, _: u64) -> Result { + unimplemented!("Only needed for channels") + } + + fn get_transaction(&self, tx_id: &Txid) -> Result { + let Ok(wallet) = self.0.wallet.try_read() else { + log_error!( + self.0.logger, + "Could not get wallet lock to get transaction" + ); + return Err(create_wallet_error( + "Failed to get wallet lock to get transaction", + )); + }; + + Ok(wallet.get_tx(*tx_id, true).unwrap().transaction.unwrap()) + } + + fn get_transaction_confirmations(&self, tx_id: &Txid) -> Result { + let Ok(wallet) = self.0.wallet.try_read() else { + log_error!( + self.0.logger, + "Could not get wallet lock to get tx confirmations" + ); + return Err(create_wallet_error( + "Failed to get wallet lock to get tx confirmations", + )); + }; + + let Some(tx) = wallet.get_tx(*tx_id, true) else { + // if we don't have the tx, then it is unconfirmed, so return 0 + return Ok(0); + }; + + match tx.confirmation_time { + ConfirmationTime::Confirmed { height, .. } => { + let cur = wallet + .latest_checkpoint() + .map(|c| c.height) + .ok_or(create_wallet_error("Failed to get latest checkpoint"))?; + + Ok(cur.saturating_sub(height) + 1) + } + ConfirmationTime::Unconfirmed { .. } => Ok(0), + } + } +} + +pub struct DummyOracleClient {} + +impl Oracle for DummyOracleClient { + fn get_public_key(&self) -> XOnlyPublicKey { + unimplemented!("Unused") + } + + fn get_announcement(&self, _: &str) -> Result { + unimplemented!("Unused") + } + + fn get_attestation(&self, _: &str) -> Result { + unimplemented!("Unused") + } +} diff --git a/mutiny-core/src/dlc/storage.rs b/mutiny-core/src/dlc/storage.rs new file mode 100644 index 000000000..8983194c8 --- /dev/null +++ b/mutiny-core/src/dlc/storage.rs @@ -0,0 +1,379 @@ +use crate::error::MutinyError; +use crate::storage::{MutinyStorage, VersionedValue}; +use bitcoin::consensus::ReadExt; +use bitcoin::hashes::hex::ToHex; +use bitcoin::secp256k1::PublicKey; +use dlc_manager::chain_monitor::ChainMonitor; +use dlc_manager::channel::offered_channel::OfferedChannel; +use dlc_manager::channel::signed_channel::{SignedChannel, SignedChannelStateType}; +use dlc_manager::channel::Channel; +use dlc_manager::contract::accepted_contract::AcceptedContract; +use dlc_manager::contract::offered_contract::OfferedContract; +use dlc_manager::contract::ser::Serializable; +use dlc_manager::contract::signed_contract::SignedContract; +use dlc_manager::contract::{ + ClosedContract, Contract, FailedAcceptContract, FailedSignContract, PreClosedContract, +}; +use dlc_manager::ChannelId; +use dlc_manager::{error::Error, ContractId}; +use lightning::io::Cursor; +use serde_json::Value; +use std::collections::HashMap; +use std::convert::TryInto; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::Arc; + +// copied from rust-dlc +macro_rules! convertible_enum { + (enum $name:ident { + $($vname:ident $(= $val:expr)?,)*; + $($tname:ident $(= $tval:expr)?,)* + }, $input:ident) => { + #[derive(Debug)] + enum $name { + $($vname $(= $val)?,)* + $($tname $(= $tval)?,)* + } + + impl From<$name> for u8 { + fn from(prefix: $name) -> u8 { + prefix as u8 + } + } + + impl std::convert::TryFrom for $name { + type Error = Error; + + fn try_from(v: u8) -> Result { + match v { + $(x if x == u8::from($name::$vname) => Ok($name::$vname),)* + $(x if x == u8::from($name::$tname) => Ok($name::$tname),)* + _ => Err(Error::StorageError("Unknown prefix".to_string())), + } + } + } + + impl $name { + #[allow(dead_code)] + fn get_prefix(input: &$input) -> u8 { + let prefix = match input { + $($input::$vname(_) => $name::$vname,)* + $($input::$tname{..} => $name::$tname,)* + }; + prefix.into() + } + } + } +} + +// copied from rust-dlc +convertible_enum!( + enum ContractPrefix { + Offered = 1, + Accepted, + Signed, + Confirmed, + PreClosed, + Closed, + FailedAccept, + FailedSign, + Refunded, + Rejected,; + }, + Contract +); + +fn to_storage_error(e: T) -> Error +where + T: std::fmt::Display, +{ + Error::StorageError(e.to_string()) +} + +pub const DLC_CONTRACT_KEY_PREFIX: &str = "dlc_contract/"; +pub const DLC_KEY_INDEX_KEY: &str = "dlc_key_index"; + +#[derive(Clone)] +pub struct DlcStorage { + pub(crate) storage: S, + key_index_counter: Arc, +} + +impl DlcStorage { + pub fn new(storage: S) -> Self { + Self { + storage, + key_index_counter: Arc::new(AtomicU32::new(0)), + } + } + + /// Get the next key index to use for a new contract. Saves the index to storage. + /// This is used to generate unique keys for contracts + pub(crate) fn get_next_key_index(&self) -> u32 { + self.key_index_counter.fetch_add(1, Ordering::SeqCst) + } + + pub(crate) fn add_new_key(&self, pk: PublicKey, index: u32) -> Result<(), MutinyError> { + let mut current: HashMap = + match self.storage.get_data::(DLC_KEY_INDEX_KEY)? { + Some(value) => value.get_value()?, + None => HashMap::with_capacity(1), + }; + + current.insert(pk, index); + + // Save the new key index map and set the version to the current index + // this way it is stored in VSS with the latest version + let value = VersionedValue { + value: serde_json::to_value(current)?, + version: index, + }; + self.storage + .set_data(DLC_KEY_INDEX_KEY.to_string(), value, Some(index))?; + + Ok(()) + } + + pub(crate) fn get_index_for_key(&self, pk: &PublicKey) -> Result { + let current: HashMap = + match self.storage.get_data::(DLC_KEY_INDEX_KEY)? { + Some(value) => value.get_value()?, + None => return Err(MutinyError::NotFound), + }; + current.get(pk).copied().ok_or(MutinyError::NotFound) + } +} + +impl dlc_manager::Storage for DlcStorage { + fn get_contract(&self, id: &ContractId) -> Result, Error> { + let key = format!("{DLC_CONTRACT_KEY_PREFIX}{}", id.to_hex()); + match self + .storage + .get_data::(&key) + .map_err(to_storage_error)? + { + None => Ok(None), + Some(value) => { + let string: String = value.get_value().map_err(to_storage_error)?; + let bytes: Vec = base64::decode(string).map_err(to_storage_error)?; + Ok(Some(deserialize_contract(&bytes)?)) + } + } + } + + fn get_contracts(&self) -> Result, Error> { + self.storage + .scan::(DLC_CONTRACT_KEY_PREFIX, None) + .map_err(to_storage_error)? + .into_values() + .map(|value| { + let string: String = value.get_value().map_err(to_storage_error)?; + base64::decode(string) + .map_err(to_storage_error) + .and_then(|b| deserialize_contract(&b)) + }) + .collect() + } + + fn create_contract(&self, contract: &OfferedContract) -> Result<(), Error> { + let serialized = serialize_contract(&Contract::Offered(contract.clone()))?; + let key = format!("{DLC_CONTRACT_KEY_PREFIX}{}", contract.id.to_hex()); + + let value = VersionedValue { + value: Value::String(base64::encode(serialized)), + version: 0, + }; + + self.storage + .set_data(key, value, None) + .map_err(to_storage_error) + } + + fn delete_contract(&self, id: &ContractId) -> Result<(), Error> { + let key = format!("{DLC_CONTRACT_KEY_PREFIX}{}", id.to_hex()); + self.storage.delete(&[key]).map_err(to_storage_error) + } + + fn update_contract(&self, contract: &Contract) -> Result<(), Error> { + let serialized = serialize_contract(contract)?; + let key = format!("{DLC_CONTRACT_KEY_PREFIX}{}", contract.get_id().to_hex()); + + let version = get_version(contract); + let value = VersionedValue { + value: Value::String(base64::encode(serialized)), + version: version.unwrap_or(0), + }; + + self.storage + .set_data(key, value, version) + .map_err(to_storage_error)?; + + // if the contract was in the offer state, we can delete the version with the temporary id + match contract { + a @ Contract::Accepted(_) | a @ Contract::Signed(_) => { + let key = format!("{DLC_CONTRACT_KEY_PREFIX}{}", a.get_temporary_id().to_hex()); + self.storage.delete(&[key]).map_err(to_storage_error)?; + } + _ => {} + }; + + Ok(()) + } + + fn get_contract_offers(&self) -> Result, Error> { + Ok(self + .get_contracts()? + .into_iter() + .filter_map(|c| match c { + Contract::Offered(o) => Some(o), + _ => None, + }) + .collect()) + } + + fn get_signed_contracts(&self) -> Result, Error> { + Ok(self + .get_contracts()? + .into_iter() + .filter_map(|c| match c { + Contract::Signed(o) => Some(o), + _ => None, + }) + .collect()) + } + + fn get_confirmed_contracts(&self) -> Result, Error> { + Ok(self + .get_contracts()? + .into_iter() + .filter_map(|c| match c { + Contract::Confirmed(o) => Some(o), + _ => None, + }) + .collect()) + } + + fn get_preclosed_contracts(&self) -> Result, Error> { + Ok(self + .get_contracts()? + .into_iter() + .filter_map(|c| match c { + Contract::PreClosed(o) => Some(o), + _ => None, + }) + .collect()) + } + + fn upsert_channel(&self, _: Channel, _: Option) -> Result<(), Error> { + Ok(()) // Channels not supported + } + + fn delete_channel(&self, _: &ChannelId) -> Result<(), Error> { + Ok(()) // Channels not supported + } + + fn get_channel(&self, _: &ChannelId) -> Result, Error> { + Ok(None) // Channels not supported + } + + fn get_signed_channels( + &self, + _: Option, + ) -> Result, Error> { + Ok(vec![]) // Channels not supported + } + + fn get_offered_channels(&self) -> Result, Error> { + Ok(vec![]) // Channels not supported + } + + fn persist_chain_monitor(&self, _: &ChainMonitor) -> Result<(), Error> { + Ok(()) // Channels not supported + } + + fn get_chain_monitor(&self) -> Result, Error> { + Ok(None) // Channels not supported + } +} + +fn get_version(contract: &Contract) -> Option { + match contract { + Contract::Offered(_) => None, + Contract::Accepted(_) => Some(1), + Contract::Signed(_) => Some(2), + Contract::Confirmed(_) => Some(3), + Contract::PreClosed(_) => Some(4), + Contract::Closed(_) => Some(5), + Contract::Refunded(_) => Some(5), + Contract::FailedAccept(_) => None, + Contract::FailedSign(_) => Some(2), + Contract::Rejected(_) => None, + } +} + +fn serialize_contract(contract: &Contract) -> Result, lightning::io::Error> { + let mut serialized = match contract { + Contract::Offered(o) | Contract::Rejected(o) => o.serialize(), + Contract::Accepted(o) => o.serialize(), + Contract::Signed(o) | Contract::Confirmed(o) | Contract::Refunded(o) => o.serialize(), + Contract::FailedAccept(c) => c.serialize(), + Contract::FailedSign(c) => c.serialize(), + Contract::PreClosed(c) => c.serialize(), + Contract::Closed(c) => c.serialize(), + }?; + let mut res = Vec::with_capacity(serialized.len() + 1); + res.push(ContractPrefix::get_prefix(contract)); + res.append(&mut serialized); + Ok(res) +} + +fn deserialize_contract(buff: &Vec) -> Result { + let mut cursor = Cursor::new(buff); + let prefix = cursor.read_u8().map_err(to_storage_error)?; + let contract_prefix: ContractPrefix = prefix.try_into()?; + let contract = match contract_prefix { + ContractPrefix::Offered => { + Contract::Offered(OfferedContract::deserialize(&mut cursor).map_err(to_storage_error)?) + } + ContractPrefix::Accepted => Contract::Accepted( + AcceptedContract::deserialize(&mut cursor).map_err(to_storage_error)?, + ), + ContractPrefix::Signed => { + Contract::Signed(SignedContract::deserialize(&mut cursor).map_err(to_storage_error)?) + } + ContractPrefix::Confirmed => { + Contract::Confirmed(SignedContract::deserialize(&mut cursor).map_err(to_storage_error)?) + } + ContractPrefix::PreClosed => Contract::PreClosed( + PreClosedContract::deserialize(&mut cursor).map_err(to_storage_error)?, + ), + ContractPrefix::Closed => { + Contract::Closed(ClosedContract::deserialize(&mut cursor).map_err(to_storage_error)?) + } + ContractPrefix::FailedAccept => Contract::FailedAccept( + FailedAcceptContract::deserialize(&mut cursor).map_err(to_storage_error)?, + ), + ContractPrefix::FailedSign => Contract::FailedSign( + FailedSignContract::deserialize(&mut cursor).map_err(to_storage_error)?, + ), + ContractPrefix::Refunded => { + Contract::Refunded(SignedContract::deserialize(&mut cursor).map_err(to_storage_error)?) + } + ContractPrefix::Rejected => { + Contract::Rejected(OfferedContract::deserialize(&mut cursor).map_err(to_storage_error)?) + } + }; + Ok(contract) +} + +#[cfg(test)] +mod test { + const CONTRACT: &str = "AQ68ldR1g4+lMEK8Thnn6YVBQSPCmMSDE3yhTr94i6aYAQEAAwVIZWFkcwAAAAAAAE4gAAAAAAAAAAAFVGFpbHMAAAAAAAAAAAAAAAAAAE4gBU90aGVyAAAAAAAATiAAAAAAAAAAAAH+fbKa07eOeLmFEq7s+oEqLvvEezDGts8b+nde8ioDPiv/AAmOFo7qr+02HLDDWRTfbSGG+EbV2SUopvkHf28A6BSt/rJCA1591v1sZk7s0jYALK6l+FfRcP0z89JxR6X92CJWAAGlLn5AA0hFODKxAnrWjypuhXUbvz9Zfou91hPgK3LcB2Owp4D92AYUAAMFSGVhZHMFVGFpbHMFT3RoZXIXQ29pbiBGbGlwOiBwcm9tcHRfaWQ6IDMAAAAAAAAAAQINsC8f/fzu4kZIQ3fRBsLcMQgYKR37cUg0DuSghIszUAAiUSAVRsZLDbs7nb3h0iD1dJq1hJSb/75ai1bcBsnjiMoB7AH4SBYY6DbDACJRIB+HX+NTITKZXVG8cwyw/msjOzQevUrfsvK/MCxSJGXGQc9CZTYW+vgCLCil9Bsdm2wLqWMjeS/0njJx9b1u2jwd9FR0Q4h8cEMAAAABAAAAAAAAAGsAANFyTIr3AbIwfl/BWtUZ7ggHvQYTdfpsBKzXZ90VekKxdvq1Pl2UIBMAAAABAAAAAAAAAGsAAMu8nsx8bof0AAAAAAANujEAAAAAAAAnEAAAAAAAAE4gAtFyTIr3AbIw/QFlAgAAAAABAX5fwVrVGe4IB70GE3X6bASs12fdFXpCsXb6tT5dlCATAAAAAAD/////AqGGAQAAAAAAIlEggwVyU+kC22wCN614y2wCtnTvuF/Ln92cptOwzBl2j4gwNAwAAAAAACJRIPzpzEQqLkazw4rRnglUQIk+dpmmshqGTfDyc5yk5fvDBABHMEQCIGyaSYj/vfO7ODRKV/xulr3Ee58R0i1+OufMlxUa+njcAiA8OTf/Feg/IIlnA4qEtFGdm7jksIZrOTijVWBjKTFv8QFHMEQCIBvNV6dMdh9SlT5XglwK66hnod7zopHT4RVkVhfx0AHnAiAdc2mk3uMEyfMN805E4jnSN2PHywLpqihWvwTFMc6EKQFHUiECMA2WZ/AHrCQrohTqaAwu6aWJhUAfUzgDRLDlKlDyzOEhAuwZwjj5yAd/DKBptU26oE4WjHZX9wBrpKwVHyT0U7SgUq4AAAAAAAAAAf////8AawAAAQAiUSD86cxEKi5Gs8OK0Z4JVECJPnaZprIahk3w8nOcpOX7wwLLvJ7MfG6H9M0BAAAAAAEBeeE5jZDiWmQr024298DGmNniTu2umnSNjqmgMDbq5yABAAAAAP3///8CoLsNAAAAAAAiACAgWddflXslHvYkNG0voUEqe8u5S/YMpI9Ub1TTzF2RwgGGAQAAAAAAIlEgL13S33W6Alqltjr+uyrWxPdpLFGRVIhcrFzMHz0KelMBQE7cNkv7asK8qIEtMTZHbxPdpmUAuYQcygWYmquLYJkFZs5f6mJslvsiNHjEteW9yyBbgZPmoPYvM9puFHLJ4kmMGQgAAAAAAf////8AawAAAQAiUSAvXdLfdboCWqW2Ov67KtbE92ksUZFUiFysXMwfPQp6UwJp7uXSKrCglwAAAAAAAAACZVLPy2O54gACgdG50ZdBvVN5GiSylSn5wqojFmEVMlhgFBqgcRBTLXU="; + + #[test] + fn test_parse_contract() { + let bytes = base64::decode(CONTRACT).unwrap(); + let contract = super::deserialize_contract(&bytes).unwrap(); + assert!(matches!(contract, super::Contract::Offered(_))); + } +} diff --git a/mutiny-core/src/error.rs b/mutiny-core/src/error.rs index 3cb0a6ee0..e4a2ba34a 100644 --- a/mutiny-core/src/error.rs +++ b/mutiny-core/src/error.rs @@ -470,3 +470,15 @@ impl From for MutinyError { Self::PayjoinValidateResponse(e) } } + +impl From for MutinyError { + fn from(_e: dlc::Error) -> Self { + Self::DLCManagerError + } +} + +impl From for MutinyError { + fn from(_e: dlc_manager::error::Error) -> Self { + Self::DLCManagerError + } +} diff --git a/mutiny-core/src/lib.rs b/mutiny-core/src/lib.rs index 4ca89d2ad..06d2cc876 100644 --- a/mutiny-core/src/lib.rs +++ b/mutiny-core/src/lib.rs @@ -11,6 +11,7 @@ extern crate core; pub mod auth; mod chain; +mod dlc; pub mod encrypt; pub mod error; mod event; @@ -41,14 +42,19 @@ pub mod vss; #[cfg(test)] mod test_utils; +pub use crate::dlc::{DLC_CONTRACT_KEY_PREFIX, DLC_KEY_INDEX_KEY}; pub use crate::event::HTLCStatus; pub use crate::gossip::{GOSSIP_SYNC_TIME_KEY, NETWORK_GRAPH_KEY, PROB_SCORER_KEY}; pub use crate::keymanager::generate_seed; pub use crate::ldkstorage::{CHANNEL_MANAGER_KEY, MONITORS_PREFIX_KEY}; +pub use dlc_messages::oracle_msgs::{OracleAnnouncement, OracleAttestation}; +use crate::dlc::*; +use crate::logging::LOGGING_KEY; use crate::nodemanager::{ ChannelClosure, MutinyBip21RawMaterials, MutinyInvoice, TransactionDetails, }; +use crate::nostr::dlc::DlcMessageType; use crate::nostr::nwc::{ BudgetPeriod, BudgetedSpendingConditions, NwcProfileTag, SpendingConditions, }; @@ -62,16 +68,27 @@ use crate::{ nodemanager::NodeBalance, sql::glue::GlueDB, }; -use crate::{logging::LOGGING_KEY, nodemanager::NodeManagerBuilder}; -use crate::{nodemanager::NodeManager, nostr::ProfileType}; +use crate::{ + nodemanager::{NodeManager, NodeManagerBuilder}, + nostr::ProfileType, +}; use crate::{nostr::NostrManager, utils::sleep}; use ::nostr::key::XOnlyPublicKey; +use ::nostr::secp256k1::Parity; use ::nostr::{Event, JsonUtil, Kind, Metadata}; use async_lock::RwLock; use bdk_chain::ConfirmationTime; use bip39::Mnemonic; +use bitcoin::secp256k1::PublicKey; use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::{hashes::sha256, Network}; +use dlc_manager::contract::contract_input::{ContractInput, ContractInputInfo, OracleInput}; +use dlc_manager::contract::enum_descriptor::EnumDescriptor; +use dlc_manager::contract::{Contract, ContractDescriptor}; +use dlc_manager::{ContractId, Storage}; +use dlc_messages::oracle_msgs::EventDescriptor; +use dlc_messages::Message; +use esplora_client::OutputStatus; use fedimint_core::{api::InviteCode, config::FederationId, BitcoinHash}; use futures::{pin_mut, select, FutureExt}; use lightning::{log_debug, util::logger::Logger}; @@ -85,6 +102,10 @@ use std::sync::Arc; use std::{collections::HashMap, sync::atomic::AtomicBool}; use uuid::Uuid; +pub use ::dlc as rust_dlc; +pub use ::dlc_manager; +pub use ::dlc_messages; + #[cfg(test)] use mockall::{automock, predicate::*}; @@ -352,6 +373,50 @@ impl MutinyWalletConfigBuilder { } } +pub fn create_contract_input( + collateral: u64, + descriptor: EnumDescriptor, + announcement: OracleAnnouncement, + fee_rate: u64, +) -> Result { + match announcement.oracle_event.event_descriptor { + EventDescriptor::EnumEvent(e) => { + if e.outcomes + != descriptor + .outcome_payouts + .iter() + .map(|x| x.outcome.clone()) + .collect::>() + { + return Err(MutinyError::InvalidArgumentsError); + } + } + EventDescriptor::DigitDecompositionEvent(_) => unimplemented!("digit decomposition"), + } + let contract_info = ContractInputInfo { + contract_descriptor: ContractDescriptor::Enum(descriptor), + oracles: OracleInput { + public_keys: vec![announcement.oracle_public_key], + event_id: announcement.oracle_event.event_id, + threshold: 1, + }, + }; + + let input = ContractInput { + offer_collateral: collateral, + accept_collateral: collateral, + fee_rate, + contract_infos: vec![contract_info], + }; + + input.validate().map_err(|e| { + log::error!("Error validating contract input: {e}"); + MutinyError::DLCManagerError + })?; + + Ok(input) +} + #[derive(Clone)] pub struct MutinyWalletConfig { xprivkey: ExtendedPrivKey, @@ -475,11 +540,59 @@ impl MutinyWalletBuilder { nm_builder.with_logger(logger.clone()); let node_manager = Arc::new(nm_builder.build().await?); + let dlc = Arc::new(DlcHandler::new( + node_manager.wallet.clone(), + node_manager.logger.clone(), + )?); + NodeManager::start_sync(node_manager.clone()); + // DLC syncing + let esplora = node_manager.esplora.clone(); + let dlc_clone = dlc.clone(); + let dlc_stop = node_manager.stop.clone(); + utils::spawn(async move { + loop { + if dlc_stop.load(Ordering::Relaxed) { + break; + }; + + let mut dlc = dlc_clone.manager.lock().await; + if let Err(e) = dlc.periodic_check(false) { + log_error!(dlc_clone.logger, "Error checking DLCs: {e:?}"); + } else { + log_info!(dlc_clone.logger, "DLCs synced!"); + } + drop(dlc); + + // check if any of the contracts have been closed + let to_watch = dlc_clone.outputs_to_watch().unwrap_or_default(); + for (outpoint, contract) in to_watch { + // if it has been spent, find the close tx and process it + if let Ok(Some(OutputStatus { + txid: Some(txid), .. + })) = esplora + .get_output_status(&outpoint.txid, outpoint.vout as u64) + .await + { + if let Ok(Some(tx)) = esplora.get_tx(&txid).await { + let mut dlc = dlc_clone.manager.lock().await; + // for now just put 6 confirmations + if let Err(e) = dlc.on_counterparty_close(&contract, tx, 6) { + log_error!(dlc_clone.logger, "Error processing close tx: {e:?}"); + } + } + } + } + + sleep(60_000).await; + } + }); + // create nostr manager let nostr = Arc::new(NostrManager::from_mnemonic( node_manager.xprivkey, + dlc.clone(), self.storage.clone(), node_manager.logger.clone(), stop.clone(), @@ -524,6 +637,7 @@ impl MutinyWalletBuilder { nostr, federation_storage: Arc::new(RwLock::new(federation_storage)), federations, + dlc, stop, logger, network, @@ -555,7 +669,7 @@ impl MutinyWalletBuilder { }; // start the nostr wallet connect background process - mw.start_nostr_wallet_connect().await; + mw.start_nostr().await; Ok(mw) } @@ -574,6 +688,7 @@ pub struct MutinyWallet { pub nostr: Arc>, pub federation_storage: Arc>, pub(crate) federations: Arc>>>, + pub dlc: Arc>, pub stop: Arc, pub logger: Arc, network: Network, @@ -599,8 +714,156 @@ impl MutinyWallet { Ok(()) } - /// Starts a background process that will watch for nostr wallet connect events - pub(crate) async fn start_nostr_wallet_connect(&self) { + /// Sends a DLC offer to the given pubkey over Nostr. + pub async fn send_dlc_offer( + &self, + contract_input: &ContractInput, + oracle_announcement: OracleAnnouncement, + pubkey: XOnlyPublicKey, + ) -> Result { + // make sure we aren't sending an offer to ourselves + if pubkey == self.nostr.dlc_handler.public_key() { + return Err(MutinyError::InvalidArgumentsError); + } + + let mut dlc = self.dlc.manager.lock().await; + let counter_party = PublicKey::from_slice(&pubkey.public_key(Parity::Even).serialize()) + .expect("converting pubkey between crates should not fail"); + let msg = dlc + .send_offer_with_announcements( + contract_input, + counter_party, + vec![vec![oracle_announcement]], + ) + .map_err(|e| { + log_error!(self.node_manager.logger, "Error sending DLC offer: {e}"); + e + })?; + + let client = Client::new(&self.nostr.primary_key); + let relay = self.nostr.dlc_handler.relay.clone(); + #[cfg(target_arch = "wasm32")] + let add_relay_res = client.add_relay(relay).await; + + #[cfg(not(target_arch = "wasm32"))] + let add_relay_res = client.add_relay(relay, None).await; + + add_relay_res.expect("Failed to add relays"); + client.connect().await; + + let contract_id = ContractId::from(msg.temporary_contract_id); + + let event = self.nostr.dlc_handler.create_wire_msg_event( + pubkey, + None, + DlcMessageType::Normal(Message::Offer(msg)), + )?; + client.send_event(event).await?; + + client.disconnect().await?; + + Ok(contract_id) + } + + /// Accepts a DLC offer with the given contract id. This in irrevocable and will in lock in the DLC unless it fails. + /// + /// This only sends the accept message, it does not guarantee that the counterparty will also sign the DLC. + pub async fn accept_dlc_offer(&self, contract_id: [u8; 32]) -> Result<(), MutinyError> { + let contract_id = ContractId::from(contract_id); + let mut dlc = self.dlc.manager.lock().await; + let (_, pubkey, msg) = dlc.accept_contract_offer(&contract_id)?; + + let client = Client::new(&self.nostr.primary_key); + let relay = self.nostr.dlc_handler.relay.clone(); + #[cfg(target_arch = "wasm32")] + let add_relay_res = client.add_relay(relay).await; + + #[cfg(not(target_arch = "wasm32"))] + let add_relay_res = client.add_relay(relay, None).await; + + add_relay_res.expect("Failed to add relays"); + client.connect().await; + + let xonly = XOnlyPublicKey::from_slice(&pubkey.x_only_public_key().0.serialize()) + .expect("converting pubkey between crates should not fail"); + let event = self.nostr.dlc_handler.create_wire_msg_event( + xonly, + None, + DlcMessageType::Normal(Message::Accept(msg)), + )?; + client.send_event(event).await?; + + client.disconnect().await?; + + Ok(()) + } + + /// Rejects a DLC offer with the given contract id. This will delete the DLC from the wallet. + /// This is only possible if the DLC is in the Offered state or in a failed state, otherwise it will return an error. + pub async fn reject_dlc_offer(&self, contract_id: [u8; 32]) -> Result<(), MutinyError> { + let contract_id = ContractId::from(contract_id); + let dlc = self.dlc.manager.lock().await; + if let Some(contract) = dlc.get_store().get_contract(&contract_id)? { + // Only delete the contract if it's an offer or failed, + // otherwise we can't reject it without risking losing funds. + match contract { + Contract::Offered(_) => { + dlc.get_store().delete_contract(&contract_id)?; + return Ok(()); + } + Contract::FailedAccept(_) | Contract::FailedSign(_) => { + // if we failed to accept or sign, we can delete the contract + dlc.get_store().delete_contract(&contract_id)?; + return Ok(()); + } + _ => { + log_error!( + self.node_manager.logger, + "Cannot reject a contract that is active" + ); + // todo probably want a more explicit error + return Err(MutinyError::DLCManagerError); + } + } + } + + Err(MutinyError::NotFound) + } + + /// Closes the DLC with the given contract id. If the oracle attestations are valid, this will broadcast the + /// corresponding closing transaction. If the oracle attestations are not valid, this will return an error. + pub async fn close_dlc( + &self, + contract_id: [u8; 32], + attestation: OracleAttestation, + ) -> Result { + let contract_id = ContractId::from(contract_id); + let mut dlc = self.dlc.manager.lock().await; + let contract = dlc + .close_confirmed_contract(&contract_id, vec![(0, attestation)]) + .map_err(|e| { + log_error!(self.node_manager.logger, "Error closing DLC: {e}"); + e + })?; + + Ok(contract) + } + + /// Lists all of the DLCs in the wallet, including offered, active, and failed. + pub async fn list_dlcs(&self) -> Result, MutinyError> { + let dlc = self.dlc.manager.lock().await; + let mut contracts = dlc.get_store().get_contracts()?; + contracts.sort_by_key(|c| c.get_id()); + Ok(contracts) + } + + /// The wallet's nostr key it uses to send and receive DLC offers. + pub fn get_dlc_key(&self) -> XOnlyPublicKey { + self.nostr.dlc_handler.public_key() + } + + /// Starts a background process that will watch for nostr wallet connect & dlc events + pub(crate) async fn start_nostr(&self) { let nostr = self.nostr.clone(); let logger = self.logger.clone(); let stop = self.stop.clone(); @@ -611,14 +874,6 @@ impl MutinyWallet { break; }; - // if we have no relays, then there are no nwc profiles enabled - // wait 10 seconds and see if we do again - let relays = nostr.get_relays(); - if relays.is_empty() { - utils::sleep(10_000).await; - continue; - } - // clear in-active profiles, we used to have disabled and archived profiles // but now we just delete profiles if let Err(e) = nostr.remove_inactive_profiles() { @@ -646,9 +901,15 @@ impl MutinyWallet { .expect("Failed to add relays"); client.connect().await; + // subscribe to NWC events let mut last_filters = nostr.get_nwc_filters(); client.subscribe(last_filters.clone()).await; + // subscribe to DLC wire messages + client + .subscribe(vec![nostr.dlc_handler.create_wire_msg_filter()]) + .await; + // handle NWC requests let mut notifications = client.notifications(); @@ -672,17 +933,33 @@ impl MutinyWallet { notification = read_fut => { match notification { Ok(RelayPoolNotification::Event { event, .. }) => { - if event.kind == Kind::WalletConnectRequest && event.verify().is_ok() { - match nostr.handle_nwc_request(event, &self_clone).await { - Ok(Some(event)) => { - if let Err(e) = client.send_event(event).await { - log_warn!(logger, "Error sending NWC event: {e}"); + if event.verify().is_ok() { + match event.kind { + Kind::WalletConnectRequest => { + match nostr.handle_nwc_request(event, &self_clone).await { + Ok(Some(event)) => { + if let Err(e) = client.send_event(event).await { + log_warn!(logger, "Error sending NWC event: {e}"); + } + } + Ok(None) => {} // no response + Err(e) => { + log_error!(logger, "Error handling NWC request: {e}"); + } } } - Ok(None) => {} // no response - Err(e) => { - log_error!(logger, "Error handling NWC request: {e}"); + Kind::Ephemeral(28_888) => { + match nostr.dlc_handler.handle_dlc_wire_event(event).await { + Err(e) => log_error!(logger, "Error handling DLC wire event: {e}"), + Ok(None) => {}, + Ok(Some(event)) => { + if let Err(e) = client.send_event(event).await { + log_warn!(logger, "Error sending NWC event: {e}"); + } + } + } } + _ => log_warn!(logger, "Received unexpected Nostr event: {event:?}"), } } }, diff --git a/mutiny-core/src/node.rs b/mutiny-core/src/node.rs index d49f000f3..5f74b7072 100644 --- a/mutiny-core/src/node.rs +++ b/mutiny-core/src/node.rs @@ -843,7 +843,7 @@ pub(crate) struct Node { pub fee_estimator: Arc>, network: Network, pub persister: Arc>, - wallet: Arc>, + pub(crate) wallet: Arc>, pub(crate) logger: Arc, pub(crate) lsp_client: Option>, pub(crate) sync_lock: Arc>, diff --git a/mutiny-core/src/nodemanager.rs b/mutiny-core/src/nodemanager.rs index 9583027c7..e61ee5ad4 100644 --- a/mutiny-core/src/nodemanager.rs +++ b/mutiny-core/src/nodemanager.rs @@ -664,7 +664,7 @@ pub struct NodeManager { #[cfg(target_arch = "wasm32")] websocket_proxy_addr: String, user_rgs_url: Option, - esplora: Arc, + pub(crate) esplora: Arc, pub(crate) wallet: Arc>, gossip_sync: Arc, scorer: Arc>, diff --git a/mutiny-core/src/nostr/dlc.rs b/mutiny-core/src/nostr/dlc.rs new file mode 100644 index 000000000..32fe5e1a7 --- /dev/null +++ b/mutiny-core/src/nostr/dlc.rs @@ -0,0 +1,242 @@ +use crate::dlc::DlcHandler; +use crate::error::MutinyError; +use crate::logging::MutinyLogger; +use crate::storage::MutinyStorage; +use bitcoin::hashes::hex::ToHex; +use dlc::secp256k1_zkp::PublicKey; +use dlc_manager::Storage; +use dlc_messages::message_handler::read_dlc_message; +use dlc_messages::{Message, WireMessage}; +use lightning::ln::wire::Type; +use lightning::util::logger::Logger; +use lightning::util::ser::{Readable, Writeable}; +use lightning::{log_info, log_warn}; +use nostr::key::{Keys, XOnlyPublicKey}; +use nostr::prelude::{decrypt, encrypt, Parity}; +use nostr::Url; +use nostr::{Event, EventBuilder, EventId, Filter, Kind, Tag}; +use std::io::Read; +use std::sync::Arc; + +pub const DLC_WIRE_MESSAGE_KIND: Kind = Kind::Ephemeral(28_888); + +/// A wrapper around a DLC message that indicates if it is an error +pub(crate) enum DlcMessageType { + Normal(Message), + Error([u8; 32]), +} + +/// Handles Nostr DLC events +pub struct NostrDlcHandler { + key: Keys, + pub relay: Url, + dlc: Arc>, + logger: Arc, +} + +impl NostrDlcHandler { + pub fn new(key: Keys, relay: Url, dlc: Arc>, logger: Arc) -> Self { + Self { + key, + relay, + dlc, + logger, + } + } + + /// Returns the public key of the handler, this is the key that is used to send and receive events + pub fn public_key(&self) -> XOnlyPublicKey { + self.key.public_key() + } + + /// A nostr filter that can be used to subscribe to events for this handler, this should be used to subscribe to events + pub fn create_wire_msg_filter(&self) -> Filter { + Filter::new() + .kind(DLC_WIRE_MESSAGE_KIND) + .pubkey(self.key.public_key()) + } + + /// Turns an DLC message into a Nostr event + pub(crate) fn create_wire_msg_event( + &self, + to: XOnlyPublicKey, + event_id: Option, + msg: DlcMessageType, + ) -> Result { + let bytes = match msg { + DlcMessageType::Normal(msg) => { + let mut bytes = msg.type_id().encode(); + bytes.extend(msg.encode()); + bytes + } + DlcMessageType::Error(err) => { + let mut bytes = 0u16.encode(); + bytes.extend(err); + bytes + } + }; + let content = encrypt(&self.key.secret_key().unwrap(), &to, base64::encode(bytes))?; + let p_tag = Tag::PublicKey { + public_key: to, + relay_url: None, + alias: None, + }; + let e_tag = event_id.map(|event_id| Tag::Event { + event_id, + relay_url: None, + marker: None, + }); + let tags = [Some(p_tag), e_tag] + .into_iter() + .flatten() + .collect::>(); + let event = EventBuilder::new(DLC_WIRE_MESSAGE_KIND, content, tags).to_event(&self.key)?; + + Ok(event) + } + + /// Parses a Nostr event into a DLC message + pub(crate) fn parse_wire_msg_event( + &self, + event: &Event, + ) -> Result { + // Decrypt the message and parse to bytes + let content = decrypt( + &self.key.secret_key().unwrap(), + &event.pubkey, + &event.content, + )?; + let bytes = base64::decode(content)?; + let mut cursor = lightning::io::Cursor::new(&bytes); + + // Parse the message + let msg_type: u16 = Readable::read(&mut cursor)?; + + // If the message is an error, return it + if msg_type == 0 { + let mut err = [0u8; 32]; + cursor.read_exact(&mut err)?; + return Ok(DlcMessageType::Error(err)); + } + + let Some(wire) = read_dlc_message(msg_type, &mut cursor)? else { + log_warn!(self.logger, "Error reading message {}", bytes.to_hex()); + return Err(MutinyError::DLCManagerError); + }; + + match wire { + WireMessage::Message(msg) => Ok(DlcMessageType::Normal(msg)), + WireMessage::SegmentStart(_) | WireMessage::SegmentChunk(_) => { + Err(MutinyError::InvalidArgumentsError) + } + } + } + + /// Handles a DLC wire event, returns an event to reply with if needed + pub async fn handle_dlc_wire_event(&self, event: Event) -> Result, MutinyError> { + // Only handle DLC wire messages + if event.kind != DLC_WIRE_MESSAGE_KIND { + return Ok(None); + } + log_info!(self.logger, "Received DLC wire message"); + + let msg = self.parse_wire_msg_event(&event).map_err(|e| { + log_warn!(self.logger, "Error parsing DLC wire message: {e:?}"); + e + })?; + + match msg { + DlcMessageType::Normal(msg) => { + let pubkey = + PublicKey::from_slice(&event.pubkey.public_key(Parity::Even).serialize()) + .expect("converting pubkey between crates should not fail"); + let mut dlc = self.dlc.manager.lock().await; + + match dlc.on_dlc_message(&msg, pubkey) { + Err(e) => { + log_warn!(self.logger, "Error handling DLC message: {e:?}"); + let id = match msg { + Message::Offer(o) => o.temporary_contract_id, + Message::Accept(a) => a.temporary_contract_id, + Message::Sign(s) => s.contract_id, + _ => [0u8; 32], + }; + let err = DlcMessageType::Error(id); + let event = + self.create_wire_msg_event(event.pubkey, Some(event.id), err)?; + Ok(Some(event)) + } + Ok(Some(msg)) => { + let event = self.create_wire_msg_event( + event.pubkey, + Some(event.id), + DlcMessageType::Normal(msg), + )?; + Ok(Some(event)) + } + Ok(None) => Ok(None), + } + } + DlcMessageType::Error(id) => { + // delete contract since it failed + let dlc = self.dlc.manager.lock().await; + dlc.get_store().delete_contract(&id)?; + + Ok(None) + } + } + } +} + +#[cfg(test)] +#[cfg(target_arch = "wasm32")] +mod wasm_test { + use super::*; + use crate::storage::MemoryStorage; + use crate::test_utils::create_node; + use dlc_messages::OfferDlc; + use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; + + wasm_bindgen_test_configure!(run_in_browser); + + #[test] + async fn test_dlc_serialization() { + let storage = MemoryStorage::default(); + let node = create_node(storage.clone()).await; + let dlc = Arc::new(DlcHandler::new(node.wallet.clone(), node.logger.clone()).unwrap()); + let handler = NostrDlcHandler::new( + Keys::generate(), + Url::parse("https://nostr.mutinywallet.com").unwrap(), + dlc, + node.logger.clone(), + ); + + let input = include_str!("../../test_inputs/dlc_offer.json"); + let offer: OfferDlc = serde_json::from_str(input).unwrap(); + let msg = DlcMessageType::Normal(Message::Offer(offer.clone())); + + let event = handler + .create_wire_msg_event(handler.public_key(), None, msg) + .unwrap(); + let parsed = handler.parse_wire_msg_event(&event).unwrap(); + + match parsed { + DlcMessageType::Normal(Message::Offer(parsed_offer)) => assert_eq!(offer, parsed_offer), + _ => panic!("Wrong message type"), + } + + // test error parsing + let id = [3u8; 32]; + let msg = DlcMessageType::Error(id); + + let event = handler + .create_wire_msg_event(handler.public_key(), None, msg) + .unwrap(); + let parsed = handler.parse_wire_msg_event(&event).unwrap(); + + match parsed { + DlcMessageType::Error(error_id) => assert_eq!(id, error_id), + _ => panic!("Wrong message type"), + } + } +} diff --git a/mutiny-core/src/nostr/mod.rs b/mutiny-core/src/nostr/mod.rs index 5fe87bed8..cfc75096a 100644 --- a/mutiny-core/src/nostr/mod.rs +++ b/mutiny-core/src/nostr/mod.rs @@ -1,4 +1,6 @@ +use crate::dlc::DlcHandler; use crate::logging::MutinyLogger; +use crate::nostr::dlc::NostrDlcHandler; use crate::nostr::nip49::{NIP49BudgetPeriod, NIP49URI}; use crate::nostr::nwc::{ BudgetPeriod, BudgetedSpendingConditions, NostrWalletConnect, NwcProfile, NwcProfileTag, @@ -23,16 +25,19 @@ use lightning::{log_error, log_warn}; use nostr::key::SecretKey; use nostr::nips::nip47::*; use nostr::prelude::{decrypt, encrypt}; +use nostr::Url; use nostr::{Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind, Tag}; use nostr_sdk::{Client, RelayPoolNotification}; use std::sync::{atomic::Ordering, Arc, RwLock}; use std::time::Duration; use std::{str::FromStr, sync::atomic::AtomicBool}; +pub mod dlc; pub mod nip49; pub mod nwc; const NWC_ACCOUNT_INDEX: u32 = 1; +const DLC_ACCOUNT_INDEX: u32 = 1; const USER_NWC_PROFILE_START_INDEX: u32 = 1000; const NWC_STORAGE_KEY: &str = "nwc_profiles"; @@ -71,6 +76,9 @@ pub struct NostrManager { pub primary_key: Keys, /// Separate profiles for each nostr wallet connect string pub(crate) nwc: Arc>>, + /// Handler for DLC messages + pub(crate) dlc_handler: Arc>, + /// Storage pub storage: S, /// Lock for pending nwc invoices pending_nwc_lock: Arc>, @@ -91,6 +99,8 @@ impl NostrManager { .map(|x| x.profile.relay.clone()) .collect(); + relays.push(self.dlc_handler.relay.to_string()); + // remove duplicates relays.sort(); relays.dedup(); @@ -1008,6 +1018,7 @@ impl NostrManager { /// Creates a new NostrManager pub fn from_mnemonic( xprivkey: ExtendedPrivKey, + dlc: Arc>, storage: S, logger: Arc, stop: Arc, @@ -1026,10 +1037,15 @@ impl NostrManager { .map(|profile| NostrWalletConnect::new(&context, xprivkey, profile).unwrap()) .collect(); + let dlc_key = Self::derive_nostr_key(&context, xprivkey, DLC_ACCOUNT_INDEX, None, None)?; + let relay = Url::parse("wss://relay.damus.io").unwrap(); + let dlc_handler = NostrDlcHandler::new(dlc_key, relay, dlc, logger.clone()); + Ok(Self { xprivkey, primary_key, nwc: Arc::new(RwLock::new(nwc)), + dlc_handler: Arc::new(dlc_handler), storage, pending_nwc_lock: Arc::new(Mutex::new(())), logger, @@ -1068,20 +1084,24 @@ fn get_next_nwc_index( #[cfg(test)] mod test { use super::*; + use crate::fees::MutinyFeeEstimator; + use crate::onchain::OnChainWallet; use crate::storage::MemoryStorage; use bip39::Mnemonic; use bitcoin::util::bip32::ExtendedPrivKey; use bitcoin::Network; + use esplora_client::Builder; use futures::executor::block_on; use lightning_invoice::Bolt11Invoice; use nostr::key::XOnlyPublicKey; use std::str::FromStr; + use std::sync::atomic::AtomicBool; fn create_nostr_manager() -> NostrManager { let mnemonic = Mnemonic::from_str("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").expect("could not generate"); let xprivkey = - ExtendedPrivKey::new_master(Network::Bitcoin, &mnemonic.to_seed("")).unwrap(); + ExtendedPrivKey::new_master(Network::Regtest, &mnemonic.to_seed("")).unwrap(); let storage = MemoryStorage::new(None, None, None); @@ -1089,7 +1109,25 @@ mod test { let stop = Arc::new(AtomicBool::new(false)); - NostrManager::from_mnemonic(xprivkey, storage, logger, stop).unwrap() + let esplora = Arc::new( + Builder::new("https://mutinynet.com/api") + .build_async() + .unwrap(), + ); + let fees = MutinyFeeEstimator::new(storage.clone(), esplora.clone(), logger.clone()); + let wallet = OnChainWallet::new( + xprivkey, + storage.clone(), + Network::Regtest, + esplora.clone(), + Arc::new(fees), + Arc::new(AtomicBool::default()), + logger.clone(), + ) + .unwrap(); + let dlc = DlcHandler::new(Arc::new(wallet), logger.clone()).unwrap(); + + NostrManager::from_mnemonic(xprivkey, Arc::new(dlc), storage, logger, stop).unwrap() } #[test] diff --git a/mutiny-core/src/nostr/nwc.rs b/mutiny-core/src/nostr/nwc.rs index f9a02d35f..fc783f092 100644 --- a/mutiny-core/src/nostr/nwc.rs +++ b/mutiny-core/src/nostr/nwc.rs @@ -1172,11 +1172,15 @@ mod test { #[cfg(target_arch = "wasm32")] mod wasm_test { use super::*; + use crate::dlc::DlcHandler; use crate::logging::MutinyLogger; use crate::nodemanager::MutinyInvoice; use crate::nostr::ProfileType; use crate::storage::MemoryStorage; - use crate::test_utils::{create_dummy_invoice, create_mutiny_wallet, create_nwc_request}; + use crate::test_utils::{ + create_dummy_invoice, create_mutiny_wallet, create_node, create_nwc_request, + create_onchain_wallet, + }; use crate::MockInvoiceHandler; use bitcoin::secp256k1::ONE_KEY; use bitcoin::Network; @@ -1219,8 +1223,10 @@ mod wasm_test { let xprivkey = ExtendedPrivKey::new_master(Network::Regtest, &[0; 64]).unwrap(); let stop = Arc::new(AtomicBool::new(false)); + let dlc = + Arc::new(DlcHandler::new(mw.node_manager.wallet.clone(), mw.logger.clone()).unwrap()); let nostr_manager = - NostrManager::from_mnemonic(xprivkey, storage.clone(), mw.logger.clone(), stop) + NostrManager::from_mnemonic(xprivkey, dlc, storage.clone(), mw.logger.clone(), stop) .unwrap(); let profile = nostr_manager @@ -1268,8 +1274,11 @@ mod wasm_test { let xprivkey = ExtendedPrivKey::new_master(Network::Regtest, &[0; 64]).unwrap(); let stop = Arc::new(AtomicBool::new(false)); + let wallet = Arc::new(create_onchain_wallet(storage.clone())); + let dlc = Arc::new(DlcHandler::new(wallet, logger.clone()).unwrap()); let nostr_manager = - NostrManager::from_mnemonic(xprivkey, storage.clone(), logger.clone(), stop).unwrap(); + NostrManager::from_mnemonic(xprivkey, dlc, storage.clone(), logger.clone(), stop) + .unwrap(); let profile = nostr_manager .create_new_profile( @@ -1443,10 +1452,13 @@ mod wasm_test { #[test] async fn test_clear_expired_pending_invoices() { let storage = MemoryStorage::default(); + let node = create_node(storage.clone()).await; let xprivkey = ExtendedPrivKey::new_master(Network::Regtest, &[0; 64]).unwrap(); + let dlc = Arc::new(DlcHandler::new(node.wallet.clone(), node.logger.clone()).unwrap()); let stop = Arc::new(AtomicBool::new(false)); let nostr_manager = NostrManager::from_mnemonic( xprivkey, + dlc, storage.clone(), Arc::new(MutinyLogger::default()), stop, @@ -1492,13 +1504,15 @@ mod wasm_test { #[test] async fn test_failed_process_nwc_event_budget() { let storage = MemoryStorage::default(); + let logger = Arc::new(MutinyLogger::default()); let mw = create_mutiny_wallet(storage.clone()).await; let xprivkey = ExtendedPrivKey::new_master(Network::Regtest, &[0; 64]).unwrap(); let stop = Arc::new(AtomicBool::new(false)); + let wallet = Arc::new(create_onchain_wallet(storage.clone())); + let dlc = Arc::new(DlcHandler::new(wallet, logger.clone()).unwrap()); let nostr_manager = - NostrManager::from_mnemonic(xprivkey, storage.clone(), mw.logger.clone(), stop) - .unwrap(); + NostrManager::from_mnemonic(xprivkey, dlc, storage.clone(), logger, stop).unwrap(); let budget = 10_000; let profile = nostr_manager @@ -1578,8 +1592,11 @@ mod wasm_test { let xprivkey = ExtendedPrivKey::new_master(Network::Regtest, &[0; 64]).unwrap(); let stop = Arc::new(AtomicBool::new(false)); + let wallet = Arc::new(create_onchain_wallet(storage.clone())); + let dlc = Arc::new(DlcHandler::new(wallet, logger.clone()).unwrap()); let nostr_manager = - NostrManager::from_mnemonic(xprivkey, storage.clone(), logger, stop).unwrap(); + NostrManager::from_mnemonic(xprivkey, dlc, storage.clone(), logger.clone(), stop) + .unwrap(); let budget = 10_000; let profile = nostr_manager diff --git a/mutiny-core/src/onchain.rs b/mutiny-core/src/onchain.rs index 5947719d3..a34871036 100644 --- a/mutiny-core/src/onchain.rs +++ b/mutiny-core/src/onchain.rs @@ -32,13 +32,14 @@ const FULL_SYNC_STOP_GAP: usize = 150; #[derive(Clone)] pub struct OnChainWallet { + pub(crate) xprivkey: ExtendedPrivKey, pub wallet: Arc>>>, pub(crate) storage: S, pub network: Network, pub blockchain: Arc, pub fees: Arc>, pub(crate) stop: Arc, - logger: Arc, + pub(crate) logger: Arc, } impl OnChainWallet { @@ -63,6 +64,7 @@ impl OnChainWallet { )?; Ok(OnChainWallet { + xprivkey, wallet: Arc::new(RwLock::new(wallet)), storage: db, network, diff --git a/mutiny-core/src/storage.rs b/mutiny-core/src/storage.rs index 67f215c0f..e5d366c2e 100644 --- a/mutiny-core/src/storage.rs +++ b/mutiny-core/src/storage.rs @@ -80,6 +80,16 @@ pub struct VersionedValue { pub value: Value, } +impl VersionedValue { + pub fn get_value(&self) -> Result + where + T: for<'de> Deserialize<'de>, + { + let value: T = serde_json::from_value(self.value.clone())?; + Ok(value) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct DeviceLock { pub time: u32, diff --git a/mutiny-core/src/test_utils.rs b/mutiny-core/src/test_utils.rs index 8ecaa8e3a..96a66c4d1 100644 --- a/mutiny-core/src/test_utils.rs +++ b/mutiny-core/src/test_utils.rs @@ -68,6 +68,36 @@ pub(crate) async fn create_mutiny_wallet(storage: S) -> Mutiny .expect("mutiny wallet should initialize") } +pub(crate) fn create_onchain_wallet(storage: S) -> OnChainWallet { + let logger = Arc::new(MutinyLogger::default()); + let network = Network::Regtest; + let seed = generate_seed(12).unwrap(); + let xprivkey = ExtendedPrivKey::new_master(network, &seed.to_seed("")).unwrap(); + + let esplora_server_url = get_esplora_url(network, None); + let esplora = Arc::new( + esplora_client::Builder::new(&esplora_server_url) + .build_async() + .unwrap(), + ); + let fee_estimator = Arc::new(MutinyFeeEstimator::new( + storage.clone(), + esplora.clone(), + logger.clone(), + )); + + OnChainWallet::new( + xprivkey, + storage, + network, + esplora, + fee_estimator, + Arc::new(AtomicBool::new(false)), + logger, + ) + .unwrap() +} + pub(crate) async fn create_node(storage: S) -> Node { // mark first sync as done so we can execute node functions storage.set_done_first_sync().unwrap(); diff --git a/mutiny-core/src/utils.rs b/mutiny-core/src/utils.rs index 247f51bb5..bad7ea4f1 100644 --- a/mutiny-core/src/utils.rs +++ b/mutiny-core/src/utils.rs @@ -1,15 +1,17 @@ use crate::error::MutinyError; +use bitcoin::hashes::hex::FromHex; use bitcoin::Network; use core::cell::{RefCell, RefMut}; use core::ops::{Deref, DerefMut}; use core::time::Duration; +use dlc_messages::oracle_msgs::{OracleAnnouncement, OracleAttestation}; use futures::{ future::{self, Either}, pin_mut, }; use lightning::routing::scoring::{LockableScore, ScoreLookUp, ScoreUpdate}; -use lightning::util::ser::Writeable; use lightning::util::ser::Writer; +use lightning::util::ser::{Readable, Writeable}; use reqwest::Client; pub const FETCH_TIMEOUT: i32 = 30_000; @@ -185,6 +187,22 @@ pub fn get_monitor_version(bytes: &[u8]) -> u64 { u64::from_be_bytes(bytes[2..10].try_into().unwrap()) } +/// Parses a hex string into an oracle announcement. +pub fn oracle_announcement_from_hex(hex: &str) -> Result { + let bytes: Vec = FromHex::from_hex(hex).map_err(|_| MutinyError::InvalidArgumentsError)?; + let mut cursor = lightning::io::Cursor::new(bytes); + + OracleAnnouncement::read(&mut cursor).map_err(|_| MutinyError::InvalidArgumentsError) +} + +/// Parses a hex string into an oracle attestation. +pub fn oracle_attestation_from_hex(hex: &str) -> Result { + let bytes: Vec = FromHex::from_hex(hex).map_err(|_| MutinyError::InvalidArgumentsError)?; + let mut cursor = lightning::io::Cursor::new(bytes); + + OracleAttestation::read(&mut cursor).map_err(|_| MutinyError::InvalidArgumentsError) +} + #[cfg(not(test))] pub const HODL_INVOICE_NODES: [&str; 1] = ["031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581"]; diff --git a/mutiny-core/test_inputs/dlc_offer.json b/mutiny-core/test_inputs/dlc_offer.json new file mode 100644 index 000000000..53e4a97e3 --- /dev/null +++ b/mutiny-core/test_inputs/dlc_offer.json @@ -0,0 +1,163 @@ +{ + "protocolVersion": 1, + "contractFlags": 0, + "chainHash": "06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f", + "temporaryContractId": "1212121212121212121212121212121212121212121212121212121212121212", + "contractInfo": { + "singleContractInfo": { + "totalCollateral": 200000000, + "contractInfo": { + "contractDescriptor": { + "numericOutcomeContractDescriptor": { + "numDigits": 13, + "payoutFunction": { + "payoutFunctionPieces": [ + { + "endPoint": { + "eventOutcome": 0, + "outcomePayout": 0, + "extraPrecision": 0 + }, + "payoutCurvePiece": { + "polynomialPayoutCurvePiece": { + "payoutPoints": [ + { + "eventOutcome": 3, + "outcomePayout": 100000000, + "extraPrecision": 0 + } + ] + } + } + }, + { + "endPoint": { + "eventOutcome": 5, + "outcomePayout": 200000000, + "extraPrecision": 0 + }, + "payoutCurvePiece": { + "polynomialPayoutCurvePiece": { + "payoutPoints": [] + } + } + } + ], + "lastEndpoint": { + "eventOutcome": 8191, + "outcomePayout": 200000000, + "extraPrecision": 0 + } + }, + "roundingIntervals": { + "intervals": [ + { + "beginInterval": 0, + "roundingMod": 1 + } + ] + } + } + }, + "oracleInfo": { + "multi": { + "threshold": 2, + "oracleAnnouncements": [ + { + "announcementSignature": "b6a9f79a3c352ffda73ef8db9c37dca6b7310ea4ad96094c7d29f276d72e89c8caebb8b80daa1a69360a5366280e519a58e328d1e3ee89f32716e4ce336607e2", + "oraclePublicKey": "66c05e5845f330791028c62aa2cb5cc9b88145f8295f7ae9e5f044a537b2a560", + "oracleEvent": { + "oracleNonces": [ + "3b584a4049fb2e4f51a0a55e371bc72a55ece6678c89414450f26543bba800bf", + "cd9433d185d08452302e24019134089c38c5b35ce2709398fb2793079ba3be4a", + "e9f506c24e86106ca7e67fa7b38576e868fba87b805386cca622ec7fc67cc781", + "1d23bda4d41bc3829a5dbbacdb94395cf95700ad3e9d84c2ccdbd016699b5aad", + "7631072764bce0db2de17273d48b084e67a5e47531bcc8da4444dd5a41f42c15", + "0876f61639e0cb6845008a43a24eb5110482096de6db5d1e9f03712fa92a0eec", + "cc669fc02f8b1005b92d5a292bde27488fad4d38f61087ed3e2731a62b65b801", + "768145f4edea846adc2a519add9f52b343f31afed366074ef7140b5d272a1a04", + "ae4278283bb7a27c841ad2083a9af1d8e6de0734c756319558bbe18878e01f65", + "df37b1b84d03bc4addf0c902c3d25311a55ce8cca1bdb298292e57e62eb3a51b", + "3afb1b0bdca1e8caff93f984f2ee640c8f766007cf843fbea49386cd97fa27b7", + "b597125db6d01b899ad1b383ba26741766754befd0ed7ff18ed9e2dcf84ed757", + "2bb73e1389ca2bbcb360d52b303d54968b9b614e5e730294fab13cbe39872733", + "2bfbdd9d12e9d7b98c34664bd87d53ca80cf21abfd2e0abc18d9ecaa6a1311d6", + "e586d07ff98f47d1d4e871b7dd2df84c91bd84d352d0fc357cd49b54476bf155" + ], + "eventMaturityEpoch": 1623133104, + "eventDescriptor": { + "digitDecompositionEvent": { + "base": 2, + "isSigned": false, + "unit": "sats/sec", + "precision": 0, + "nbDigits": 15 + } + }, + "eventId": "Test" + } + }, + { + "announcementSignature": "e3b1d3c8c874c339141fe0209dd48c01c62a2fad5668093e2e96be0a88be37a5e7163f7d4e0c9f9111fba60503b775e464d1c798d843ab80a9f1ceacf52a2aeb", + "oraclePublicKey": "97e61039bfdaf898391b8b9825934802b7960a01162d9b4f8a000ae8ce9e1fa6", + "oracleEvent": { + "oracleNonces": [ + "baaa6464e8cfede79012a02723301d7595cb506ea846ac2f5da45c726b003766", + "5970ae9ec1b131ff157da44c204ce1fdd70ad670ece8c704cea32ef7b0ebf462", + "62b5a76b68629d963bb4718ca713e8171365ce51c29941befe0e3dca99cea126", + "5dc0318d4195801ed9b5b86afa12e744ccf7e15e68bc46ef7a30ddb5652d8264", + "18346b71e012f91ac41f222817722717e33c351e19ccf7c9f4378d8e3fd56881", + "4254d6e692e620c080fb29a4e9c67d35e62feb7629d964bed71ac6679414bdd5", + "36d610e88e1fe491fc1055497017e5babc45f17f7aadf9c35800f529a93fa2a6", + "60ff8f86ff75d191bc7cc74dbc31d9f031c54654406fe8ef5f802c9622afcdc4", + "890bd95edc36570b61d3750f89bbb0b6e5a10f12692431e57d7f9362c3f31436", + "3ef80333f95f0277206dd693d5c9362b6326b991c44b7d21f3a63dd8931314a2", + "4b209c8b2e0fdb1b910a57135e146bc0cef46d21915e0bf17cfedb5c7c2296a3", + "49ce4c0b32fdc717d941c87137b9c7b26726236f256a2abf28de9e4870e79d08", + "a9ded37ffee3d0d35d9fb3ae8e0984c250fbbc1db32c08baafee3154bb6e0f51" + ], + "eventMaturityEpoch": 1623133104, + "eventDescriptor": { + "digitDecompositionEvent": { + "base": 2, + "isSigned": false, + "unit": "sats/sec", + "precision": 0, + "nbDigits": 13 + } + }, + "eventId": "Test" + } + } + ], + "oracleParams": { + "maxErrorExp": 2, + "minFailExp": 1, + "maximizeCoverage": false + } + } + } + } + } + }, + "fundingPubkey": "03c12e81303c79abf90a81b900aa3bd3ba8f47ef84c860cb314fc58d531a4d37c6", + "payoutSpk": "0014f65cd6349437fe1f35cb27628f112d7885a5c644", + "payoutSerialId": 4752179201940702056, + "offerCollateral": 100000000, + "fundingInputs": [ + { + "inputSerialId": 3784123604127642354, + "prevTx": "020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff03520101ffffffff0200f2052a010000001600143d7834074191c93d7fc2c0a54a6d40efbbfe76430000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000", + "prevTxVout": 0, + "sequence": 4294967295, + "maxWitnessLen": 107, + "redeemScript": "" + } + ], + "changeSpk": "0014bf1b2161a681add692a326ace320bc4d8451ee81", + "changeSerialId": 11805380369894479502, + "fundOutputSerialId": 17245645112901355593, + "feeRatePerVb": 2, + "cetLocktime": 1623133104, + "refundLocktime": 1623737904 +} diff --git a/mutiny-wasm/src/indexed_db.rs b/mutiny-wasm/src/indexed_db.rs index eba1c8fdf..f3839fa7b 100644 --- a/mutiny-wasm/src/indexed_db.rs +++ b/mutiny-wasm/src/indexed_db.rs @@ -9,6 +9,7 @@ use mutiny_core::logging::MutinyLogger; use mutiny_core::nodemanager::NodeStorage; use mutiny_core::storage::*; use mutiny_core::vss::*; +use mutiny_core::DLC_CONTRACT_KEY_PREFIX; use mutiny_core::*; use mutiny_core::{ encrypt::Cipher, @@ -353,6 +354,9 @@ impl IndexedDbStorage { } } } + DLC_KEY_INDEX_KEY => { + return Self::handle_versioned_value(kv, vss, current, logger).await; + } key => { if key.starts_with(MONITORS_PREFIX_KEY) { // we can get versions from monitors, so we should compare @@ -379,35 +383,10 @@ impl IndexedDbStorage { return Ok(Some((kv.key, obj.value))); } } - } else if key.starts_with(CHANNEL_MANAGER_KEY) { - // we can get versions from channel manager, so we should compare - match current.get_data::(&kv.key)? { - Some(local) => { - if local.version < kv.version { - let obj = vss.get_object(&kv.key).await?; - if serde_json::from_value::(obj.value.clone()) - .is_ok() - { - return Ok(Some((kv.key, obj.value))); - } - } else { - log_debug!( - logger, - "Skipping vss key {} with version {}, current version is {}", - kv.key, - kv.version, - local.version - ); - return Ok(None); - } - } - None => { - let obj = vss.get_object(&kv.key).await?; - if serde_json::from_value::(obj.value.clone()).is_ok() { - return Ok(Some((kv.key, obj.value))); - } - } - } + } else if key.starts_with(CHANNEL_MANAGER_KEY) + || key.starts_with(DLC_CONTRACT_KEY_PREFIX) + { + return Self::handle_versioned_value(kv, vss, current, logger).await; } } } @@ -422,6 +401,42 @@ impl IndexedDbStorage { Ok(None) } + async fn handle_versioned_value( + kv: KeyVersion, + vss: &MutinyVssClient, + current: &MemoryStorage, + logger: &MutinyLogger, + ) -> Result, MutinyError> { + // we can get versions from VersionedValue so we should compare + match current.get_data::(&kv.key)? { + Some(local) => { + if local.version < kv.version { + let obj = vss.get_object(&kv.key).await?; + if serde_json::from_value::(obj.value.clone()).is_ok() { + return Ok(Some((kv.key, obj.value))); + } + } else { + log_debug!( + logger, + "Skipping vss key {} with version {}, current version is {}", + kv.key, + kv.version, + local.version + ); + return Ok(None); + } + } + None => { + let obj = vss.get_object(&kv.key).await?; + if serde_json::from_value::(obj.value.clone()).is_ok() { + return Ok(Some((kv.key, obj.value))); + } + } + } + + Ok(None) + } + async fn build_indexed_db_database() -> Result { let rexie = Rexie::builder(WALLET_DATABASE_NAME) .version(1) diff --git a/mutiny-wasm/src/lib.rs b/mutiny-wasm/src/lib.rs index d7a85f8d0..2a829df93 100644 --- a/mutiny-wasm/src/lib.rs +++ b/mutiny-wasm/src/lib.rs @@ -34,7 +34,7 @@ use mutiny_core::lnurlauth::AuthManager; use mutiny_core::nostr::nip49::NIP49URI; use mutiny_core::nostr::nwc::{BudgetedSpendingConditions, NwcProfileTag, SpendingConditions}; use mutiny_core::storage::{DeviceLock, MutinyStorage, DEVICE_LOCK_KEY}; -use mutiny_core::utils::{now, sleep}; +use mutiny_core::utils::{now, oracle_announcement_from_hex, oracle_attestation_from_hex, sleep}; use mutiny_core::vss::MutinyVssClient; use mutiny_core::{encrypt::encryption_key_from_pass, MutinyWalletConfigBuilder}; use mutiny_core::{labels::Contact, MutinyWalletBuilder}; @@ -45,6 +45,7 @@ use mutiny_core::{ use mutiny_core::{logging::MutinyLogger, nostr::ProfileType}; use nostr::key::XOnlyPublicKey; use nostr::prelude::FromBech32; +use nostr::ToBech32; use payjoin::UriExt; use std::str::FromStr; use std::sync::Arc; @@ -1591,6 +1592,90 @@ impl MutinyWallet { Ok(()) } + /// Sends a DLC offer to the given pubkey over Nostr. + #[wasm_bindgen] + pub async fn send_dlc_offer( + &self, + collateral: u64, + descriptor: &JsValue, /* EnumDescriptor */ + oracle_announcement: String, + npub_str: String, + ) -> Result { + let oracle_announcement = oracle_announcement_from_hex(&oracle_announcement)?; + let descriptor: mutiny_core::dlc_manager::contract::enum_descriptor::EnumDescriptor = + descriptor.into_serde().map_err(|e| { + log::error!("Error: {e:?}"); + log::error!("Descriptor: {descriptor:?}"); + MutinyJsError::InvalidArgumentsError + })?; + + let fee_rate = self.inner.node_manager.estimate_fee_normal(); + let contract_input = mutiny_core::create_contract_input( + collateral, + descriptor, + oracle_announcement.clone(), + fee_rate as u64, + )?; + let pubkey = XOnlyPublicKey::from_bech32(&npub_str)?; + let res = self + .inner + .send_dlc_offer(&contract_input, oracle_announcement, pubkey) + .await?; + + Ok(res.to_hex()) + } + + /// Accepts a DLC offer with the given contract id. This in irrevocable and will in lock in the DLC unless it fails. + /// + /// This only sends the accept message, it does not guarantee that the counterparty will also sign the DLC. + #[wasm_bindgen] + pub async fn accept_dlc_offer(&self, contract_id: String) -> Result<(), MutinyJsError> { + let contract_id: [u8; 32] = FromHex::from_hex(&contract_id)?; + self.inner.accept_dlc_offer(contract_id).await?; + + Ok(()) + } + + /// Rejects a DLC offer with the given contract id. This will delete the DLC from the wallet. + /// This is only possible if the DLC is in the Offered state or in a failed state, otherwise it will return an error. + #[wasm_bindgen] + pub async fn reject_dlc_offer(&self, contract_id: String) -> Result<(), MutinyJsError> { + let contract_id: [u8; 32] = FromHex::from_hex(&contract_id)?; + self.inner.reject_dlc_offer(contract_id).await?; + + Ok(()) + } + + /// Closes the DLC with the given contract id. If the oracle attestations are valid, this will broadcast the + /// corresponding closing transaction. If the oracle attestations are not valid, this will return an error. + #[wasm_bindgen] + pub async fn close_dlc( + &self, + contract_id: String, + attestation: String, + ) -> Result<(), MutinyJsError> { + let contract_id: [u8; 32] = FromHex::from_hex(&contract_id)?; + let attestation = oracle_attestation_from_hex(&attestation)?; + self.inner.close_dlc(contract_id, attestation).await?; + + Ok(()) + } + + /// Lists all of the DLCs in the wallet, including offered, active, and failed. + #[wasm_bindgen] + pub async fn list_dlcs(&self) -> Result { + let dlcs = self.inner.list_dlcs().await?; + + let ret: Vec = dlcs.into_iter().map(|d| d.into()).collect(); + Ok(JsValue::from_serde(&ret)?) + } + + /// The wallet's nostr key it uses to send and receive DLC offers. + #[wasm_bindgen] + pub fn get_dlc_key(&self) -> String { + self.inner.get_dlc_key().to_bech32().unwrap() + } + /// Resets the scorer and network graph. This can be useful if you get stuck in a bad state. #[wasm_bindgen] pub async fn reset_router(&self) -> Result<(), MutinyJsError> { @@ -1708,6 +1793,24 @@ impl MutinyWallet { let npub = XOnlyPublicKey::from_bech32(npub)?; Ok(npub.to_hex()) } + + /// Decodes an oracle announcement from hex into a wasm object + #[wasm_bindgen] + pub fn decode_oracle_announcement( + announcement: String, + ) -> Result { + let announcement = oracle_announcement_from_hex(&announcement)?; + announcement.try_into() + } + + /// Decodes an oracle attestation from hex into a wasm object + #[wasm_bindgen] + pub fn decode_oracle_attestation( + attestation: String, + ) -> Result { + let attestation = oracle_attestation_from_hex(&attestation)?; + Ok(attestation.into()) + } } #[cfg(test)] diff --git a/mutiny-wasm/src/models.rs b/mutiny-wasm/src/models.rs index 72ff5c658..1e4a8db8c 100644 --- a/mutiny-wasm/src/models.rs +++ b/mutiny-wasm/src/models.rs @@ -1,4 +1,6 @@ +use crate::MutinyJsError; use ::nostr::key::XOnlyPublicKey; +use ::nostr::ToBech32; use bitcoin::hashes::hex::ToHex; use bitcoin::secp256k1::PublicKey; use bitcoin::OutPoint; @@ -6,6 +8,8 @@ use gloo_utils::format::JsValueSerdeExt; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription}; use lnurl::lightning_address::LightningAddress; use lnurl::lnurl::LnUrl; +use mutiny_core::dlc_manager::contract; +use mutiny_core::dlc_messages::oracle_msgs::EventDescriptor; use mutiny_core::labels::Contact as MutinyContact; use mutiny_core::nostr::nwc::SpendingConditions; use mutiny_core::*; @@ -1096,3 +1100,259 @@ impl TryFrom for BudgetPeriod { } } } + +/// An announcement from an oracle of what are the possible outcomes of an event +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[wasm_bindgen] +pub struct OracleAnnouncement { + public_key: String, + event_id: String, + pub event_maturity_epoch: u32, + outcomes: Vec, // todo this only works with Enum Events, fine for now +} + +#[wasm_bindgen] +impl OracleAnnouncement { + #[wasm_bindgen(getter)] + pub fn value(&self) -> JsValue { + JsValue::from_serde(&serde_json::to_value(self).unwrap()).unwrap() + } + + #[wasm_bindgen(getter)] + pub fn public_key(&self) -> String { + self.public_key.clone() + } + + #[wasm_bindgen(getter)] + pub fn event_id(&self) -> String { + self.event_id.clone() + } + + #[wasm_bindgen(getter)] + pub fn outcomes(&self) -> Vec { + self.outcomes.clone() + } +} + +impl TryFrom for OracleAnnouncement { + type Error = MutinyJsError; + + fn try_from(a: mutiny_core::OracleAnnouncement) -> Result { + let outcomes = match a.oracle_event.event_descriptor { + EventDescriptor::EnumEvent(e) => e.outcomes, + EventDescriptor::DigitDecompositionEvent(_) => { + return Err(MutinyJsError::InvalidArgumentsError) + } + }; + + Ok(OracleAnnouncement { + public_key: a.oracle_public_key.to_hex(), + event_id: a.oracle_event.event_id, + event_maturity_epoch: a.oracle_event.event_maturity_epoch, + outcomes, + }) + } +} + +/// An attestation from an oracle of what the outcome of an event was +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[wasm_bindgen] +pub struct OracleAttestation { + public_key: String, + outcomes: Vec, +} + +#[wasm_bindgen] +impl OracleAttestation { + #[wasm_bindgen(getter)] + pub fn value(&self) -> JsValue { + JsValue::from_serde(&serde_json::to_value(self).unwrap()).unwrap() + } + + #[wasm_bindgen(getter)] + pub fn public_key(&self) -> String { + self.public_key.clone() + } + + #[wasm_bindgen(getter)] + pub fn outcomes(&self) -> Vec { + self.outcomes.clone() + } +} + +impl From for OracleAttestation { + fn from(a: mutiny_core::OracleAttestation) -> Self { + OracleAttestation { + public_key: a.oracle_public_key.to_hex(), + outcomes: a.outcomes, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[wasm_bindgen] +pub struct Contract { + id: String, + state: String, + counter_party: String, + pub cet_locktime: u32, + pub is_offer_party: Option, + funding_txid: Option, + closing_txid: Option, +} + +impl From for Contract { + fn from(value: contract::Contract) -> Self { + let state = match value { + contract::Contract::Offered(_) => "Offered", + contract::Contract::Accepted(_) => "Accepted", + contract::Contract::Signed(_) => "Signed", + contract::Contract::Confirmed(_) => "Confirmed", + contract::Contract::PreClosed(_) => "PreClosed", + contract::Contract::Closed(_) => "Closed", + contract::Contract::Refunded(_) => "Refunded", + contract::Contract::FailedAccept(_) => "FailedAccept", + contract::Contract::FailedSign(_) => "FailedSign", + contract::Contract::Rejected(_) => "Rejected", + } + .to_string(); + + let cet_locktime = match &value { + contract::Contract::Offered(x) => x.cet_locktime, + contract::Contract::Accepted(x) => x.offered_contract.cet_locktime, + contract::Contract::Signed(x) => x.accepted_contract.offered_contract.cet_locktime, + contract::Contract::Confirmed(x) => x.accepted_contract.offered_contract.cet_locktime, + contract::Contract::PreClosed(x) => { + x.signed_contract + .accepted_contract + .offered_contract + .cet_locktime + } + contract::Contract::Closed(x) => x + .signed_cet + .as_ref() + .map(|x| x.lock_time.0) + .unwrap_or_default(), + contract::Contract::Refunded(x) => x.accepted_contract.offered_contract.cet_locktime, + contract::Contract::FailedAccept(x) => x.offered_contract.cet_locktime, + contract::Contract::FailedSign(x) => x.accepted_contract.offered_contract.cet_locktime, + contract::Contract::Rejected(x) => x.cet_locktime, + }; + + let is_offer_party = match &value { + contract::Contract::Offered(x) => Some(x.is_offer_party), + contract::Contract::Accepted(x) => Some(x.offered_contract.is_offer_party), + contract::Contract::Signed(x) => { + Some(x.accepted_contract.offered_contract.is_offer_party) + } + contract::Contract::Confirmed(x) => { + Some(x.accepted_contract.offered_contract.is_offer_party) + } + contract::Contract::PreClosed(x) => Some( + x.signed_contract + .accepted_contract + .offered_contract + .is_offer_party, + ), + contract::Contract::Closed(_) => None, + contract::Contract::Refunded(x) => { + Some(x.accepted_contract.offered_contract.is_offer_party) + } + contract::Contract::FailedAccept(x) => Some(x.offered_contract.is_offer_party), + contract::Contract::FailedSign(x) => { + Some(x.accepted_contract.offered_contract.is_offer_party) + } + contract::Contract::Rejected(x) => Some(x.is_offer_party), + }; + + let funding_txid: Option = match &value { + contract::Contract::Offered(_) => None, + contract::Contract::Accepted(_) => None, + contract::Contract::Signed(x) => { + Some(x.accepted_contract.dlc_transactions.fund.txid().to_hex()) + } + contract::Contract::Confirmed(x) => { + Some(x.accepted_contract.dlc_transactions.fund.txid().to_hex()) + } + contract::Contract::PreClosed(x) => Some( + x.signed_contract + .accepted_contract + .dlc_transactions + .fund + .txid() + .to_hex(), + ), + contract::Contract::Closed(x) => x + .signed_cet + .as_ref() + .map(|t| t.input[0].previous_output.txid.to_hex()), + contract::Contract::Refunded(x) => { + Some(x.accepted_contract.dlc_transactions.fund.txid().to_hex()) + } + contract::Contract::FailedAccept(_) => None, + contract::Contract::FailedSign(_) => None, + contract::Contract::Rejected(_) => None, + }; + + let closing_txid: Option = match &value { + contract::Contract::Offered(_) => None, + contract::Contract::Accepted(_) => None, + contract::Contract::Signed(_) => None, + contract::Contract::Confirmed(_) => None, + contract::Contract::PreClosed(x) => Some(x.signed_cet.txid().to_hex()), + contract::Contract::Closed(x) => x.signed_cet.as_ref().map(|t| t.txid().to_hex()), + contract::Contract::Refunded(x) => { + Some(x.accepted_contract.dlc_transactions.refund.txid().to_hex()) + } + contract::Contract::FailedAccept(_) => None, + contract::Contract::FailedSign(_) => None, + contract::Contract::Rejected(_) => None, + }; + + let counter_party = { + let xonly = value.get_counter_party_id().x_only_public_key().0; + XOnlyPublicKey::from_slice(&xonly.serialize()) + .unwrap() + .to_bech32() + .unwrap() + }; + + Contract { + id: value.get_id().to_hex(), + state, + counter_party, + cet_locktime, + is_offer_party, + funding_txid, + closing_txid, + } + } +} + +#[wasm_bindgen] +impl Contract { + #[wasm_bindgen(getter)] + pub fn value(&self) -> JsValue { + JsValue::from_serde(&serde_json::to_value(self).unwrap()).unwrap() + } + + #[wasm_bindgen(getter)] + pub fn id(&self) -> String { + self.id.clone() + } + + #[wasm_bindgen(getter)] + pub fn counter_party(&self) -> String { + self.counter_party.clone() + } + + #[wasm_bindgen(getter)] + pub fn funding_txid(&self) -> Option { + self.funding_txid.clone() + } + + #[wasm_bindgen(getter)] + pub fn closing_txid(&self) -> Option { + self.closing_txid.clone() + } +}