diff --git a/Cargo.lock b/Cargo.lock index 059e7535..ba52bb17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,49 @@ dependencies = [ "subtle", ] +[[package]] +name = "age" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fc171f4874fa10887e47088f81a55fcf030cd421aa31ec2b370cafebcc608a" +dependencies = [ + "age-core", + "base64 0.21.7", + "bech32 0.9.1", + "chacha20poly1305", + "cookie-factory", + "hmac", + "i18n-embed", + "i18n-embed-fl", + "lazy_static", + "nom", + "pin-project", + "rand", + "rust-embed", + "scrypt", + "sha2", + "subtle", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "age-core" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2bf6a89c984ca9d850913ece2da39e1d200563b0a94b002b253beee4c5acf99" +dependencies = [ + "base64 0.21.7", + "chacha20poly1305", + "cookie-factory", + "hkdf", + "io_tee", + "nom", + "rand", + "secrecy", + "sha2", +] + [[package]] name = "ahash" version = "0.8.11" @@ -75,7 +118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -98,9 +141,9 @@ checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-activity" @@ -109,7 +152,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.6.0", + "bitflags 2.8.0", "cc", "cesu8", "jni", @@ -120,7 +163,7 @@ dependencies = [ "ndk-context", "ndk-sys 0.6.0+11769913", "num_enum", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -146,9 +189,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.93" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "approx" @@ -168,6 +211,12 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "arrayref" version = "0.3.9" @@ -219,9 +268,9 @@ dependencies = [ [[package]] name = "async-broadcast" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ "event-listener", "event-listener-strategy", @@ -358,7 +407,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -387,13 +436,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -484,6 +533,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + [[package]] name = "bdk_chain" version = "0.16.0" @@ -508,12 +566,32 @@ dependencies = [ "electrum-client", ] +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + [[package]] name = "bech32" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" +[[package]] +name = "bip329" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8351c1cf438ae5814ad2a696f06fd07d7be4a1a133d889b1785260fa8797798c" +dependencies = [ + "age", + "bitcoin", + "hex", + "serde", + "serde_json", + "thiserror 2.0.12", +] + [[package]] name = "bip39" version = "2.1.0" @@ -557,31 +635,31 @@ dependencies = [ "bitcoin", "byteorder", "chrono", - "getrandom", + "getrandom 0.2.15", "hex", "hidapi", "noise-protocol", "noise-rust-crypto", "num-bigint", - "prost 0.13.3", + "prost 0.13.4", "prost-build", "semver", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", "tokio", "zeroize", ] [[package]] name = "bitcoin" -version = "0.32.4" +version = "0.32.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788902099d47c8682efe6a7afb01c8d58b9794ba66c06affd81c3d6b560743eb" +checksum = "ce6bc65742dea50536e35ad42492b234c27904a27f0abdcbce605015cb4ea026" dependencies = [ "base58ck", "base64 0.21.7", - "bech32", + "bech32 0.11.0", "bitcoin-internals 0.3.0", "bitcoin-io", "bitcoin-units", @@ -667,9 +745,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" [[package]] name = "blake2" @@ -719,9 +797,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "by_address" @@ -731,22 +809,22 @@ checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" [[package]] name = "bytemuck" -version = "1.19.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec" +checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -757,9 +835,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "bzip2" @@ -788,12 +866,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "log", "polling", "rustix", "slab", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -810,9 +888,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.36" +version = "1.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baee610e9452a8f6f0a1b6194ec09ff9e2d85dea54432acdae41aa0761c95d70" +checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2" dependencies = [ "jobserver", "libc", @@ -869,9 +947,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -927,7 +1005,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4274ea815e013e0f9f04a2633423e14194e408a0576c943ce3d14ca56c50031c" dependencies = [ - "thiserror", + "thiserror 1.0.69", "x11rb", ] @@ -1033,11 +1111,20 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom", + "getrandom 0.2.15", "once_cell", "tiny-keccak", ] +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1083,7 +1170,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "core-foundation 0.10.0", "core-graphics-types 0.2.0", "foreign-types", @@ -1107,7 +1194,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "core-foundation 0.10.0", "libc", ] @@ -1118,14 +1205,14 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59fd57d82eb4bfe7ffa9b1cec0c05e2fd378155b47f255a67983cb4afe0e80c2" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "fontdb 0.16.2", "log", "rangemap", "rayon", "rustc-hash 1.1.0", "rustybuzz", - "self_cell", + "self_cell 1.1.0", "swash", "sys-locale", "ttf-parser 0.21.1", @@ -1137,9 +1224,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -1155,9 +1242,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -1174,15 +1261,15 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "crypto-bigint" @@ -1250,7 +1337,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -1259,11 +1346,25 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e3d747f100290a1ca24b752186f61f6637e1deffe3bf6320de6fcb29510a307" dependencies = [ - "bitflags 2.6.0", - "libloading 0.8.5", + "bitflags 2.8.0", + "libloading 0.8.6", "winapi", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.10", +] + [[package]] name = "data-url" version = "0.3.1" @@ -1288,7 +1389,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -1358,7 +1459,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -1367,7 +1468,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading 0.8.5", + "libloading 0.8.6", ] [[package]] @@ -1403,7 +1504,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "bytemuck", "drm-ffi", "drm-fourcc", @@ -1466,7 +1567,7 @@ dependencies = [ "byteorder", "libc", "log", - "rustls 0.23.21", + "rustls 0.23.22", "serde", "serde_json", "webpki-roots", @@ -1518,9 +1619,9 @@ checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" [[package]] name = "enumflags2" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +checksum = "ba2f4b465f5318854c6f8dd686ede6c0a9dc67d4b1ac241cf0eb51521a309147" dependencies = [ "enumflags2_derive", "serde", @@ -1528,13 +1629,13 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -1545,12 +1646,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1561,9 +1662,9 @@ checksum = "a5d9305ccc6942a704f4335694ecd3de2ea531b114ac2d51f5f843750787a92f" [[package]] name = "etagere" -version = "0.2.13" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e2f1e3be19fb10f549be8c1bf013e8675b4066c445e36eb76d2ebb2f54ee495" +checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" dependencies = [ "euclid", "svg_fmt", @@ -1580,9 +1681,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", @@ -1634,15 +1735,15 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c6f4c64c1d33a3111c4466f7365ebdcc37c5bd1ea0d62aae2e3d722aacbedb" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" dependencies = [ "simd-adler32", ] @@ -1684,6 +1785,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "find-crate" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" +dependencies = [ + "toml", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -1692,9 +1802,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1712,6 +1822,50 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" +[[package]] +name = "fluent" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash 1.1.0", + "self_cell 0.10.3", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1782,7 +1936,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -1851,9 +2005,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ "fastrand", "futures-core", @@ -1870,7 +2024,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -1932,7 +2086,19 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", ] [[package]] @@ -2005,7 +2171,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "gpu-alloc-types", ] @@ -2015,7 +2181,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] @@ -2026,7 +2192,7 @@ checksum = "6f56f6318968d03c18e1bcf4857ff88c61157e9da8e47c5f29055d60e1228884" dependencies = [ "log", "presser", - "thiserror", + "thiserror 1.0.69", "winapi", "windows", ] @@ -2037,7 +2203,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc11df1ace8e7e564511f53af41f3e42ddc95b56fd07b3f4445d2a6048bc682c" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "gpu-descriptor-types", "hashbrown 0.14.5", ] @@ -2048,7 +2214,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bf0b36e6f090b7e1d8a4b49c0cb81c1f8376f72198c65dd3ad9ff3556b8b78c" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] @@ -2125,9 +2291,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "hashlink" @@ -2144,11 +2310,11 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "com", "libc", - "libloading 0.8.5", - "thiserror", + "libloading 0.8.6", + "thiserror 1.0.69", "widestring", "winapi", ] @@ -2217,6 +2383,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -2228,11 +2403,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2259,9 +2434,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" [[package]] name = "httpdate" @@ -2271,9 +2446,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.31" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -2307,6 +2482,73 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "i18n-config" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e88074831c0be5b89181b05e6748c4915f77769ecc9a4c372f88b169a8509c9" +dependencies = [ + "basic-toml", + "log", + "serde", + "serde_derive", + "thiserror 1.0.69", + "unic-langid", +] + +[[package]] +name = "i18n-embed" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0454970a5853f498e686cbd7bf9391aac2244928194780cb7a0af0f41937db6" +dependencies = [ + "arc-swap", + "fluent", + "fluent-langneg", + "fluent-syntax", + "i18n-embed-impl", + "intl-memoizer", + "log", + "parking_lot 0.12.3", + "rust-embed", + "thiserror 1.0.69", + "unic-langid", + "walkdir", +] + +[[package]] +name = "i18n-embed-fl" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7578cee2940492a648bd60fb49ca85ee8c821a63790e0ef5b604cfed353b2a" +dependencies = [ + "dashmap", + "find-crate", + "fluent", + "fluent-syntax", + "i18n-config", + "i18n-embed", + "proc-macro-error2", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.98", + "unic-langid", +] + +[[package]] +name = "i18n-embed-impl" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" +dependencies = [ + "find-crate", + "i18n-config", + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -2342,7 +2584,7 @@ dependencies = [ "iced_widget", "iced_winit", "image", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2351,16 +2593,16 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0013a238275494641bf8f1732a23a808196540dc67b22ff97099c044ae4c8a1c" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "bytes", "glam", "log", "num-traits", "once_cell", "palette", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "smol_str", - "thiserror", + "thiserror 1.0.69", "web-time", ] @@ -2373,7 +2615,7 @@ dependencies = [ "futures", "iced_core", "log", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "tokio", "wasm-bindgen-futures", "wasm-timer", @@ -2388,7 +2630,7 @@ dependencies = [ "cosmic-text", "etagere", "lru", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "wgpu", ] @@ -2398,7 +2640,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba25a18cfa6d5cc160aca7e1b34f73ccdff21680fa8702168c09739767b6c66f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "bytemuck", "cosmic-text", "half 2.4.1", @@ -2410,8 +2652,8 @@ dependencies = [ "lyon_path", "once_cell", "raw-window-handle", - "rustc-hash 2.1.0", - "thiserror", + "rustc-hash 2.1.1", + "thiserror 1.0.69", "unicode-segmentation", ] @@ -2425,7 +2667,7 @@ dependencies = [ "iced_tiny_skia", "iced_wgpu", "log", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2438,7 +2680,7 @@ dependencies = [ "iced_core", "iced_futures", "raw-window-handle", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2453,7 +2695,7 @@ dependencies = [ "kurbo 0.10.4", "log", "resvg", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "softbuffer", "tiny-skia", ] @@ -2464,7 +2706,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15708887133671d2bcc6c1d01d1f176f43a64d6cdc3b2bf893396c3ee498295f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "bytemuck", "futures", "glam", @@ -2475,8 +2717,8 @@ dependencies = [ "lyon", "once_cell", "resvg", - "rustc-hash 2.1.0", - "thiserror", + "rustc-hash 2.1.1", + "thiserror 1.0.69", "wgpu", ] @@ -2492,8 +2734,8 @@ dependencies = [ "once_cell", "ouroboros", "qrcode", - "rustc-hash 2.1.0", - "thiserror", + "rustc-hash 2.1.1", + "thiserror 1.0.69", "unicode-segmentation", ] @@ -2507,8 +2749,8 @@ dependencies = [ "iced_graphics", "iced_runtime", "log", - "rustc-hash 2.1.0", - "thiserror", + "rustc-hash 2.1.1", + "thiserror 1.0.69", "tracing", "wasm-bindgen-futures", "web-sys", @@ -2632,7 +2874,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -2682,12 +2924,12 @@ checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", - "hashbrown 0.15.1", + "hashbrown 0.15.2", ] [[package]] @@ -2708,6 +2950,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "intl-memoizer" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe22e020fce238ae18a6d5d8c502ee76a52a6e880d99477657e6acc30ec57bda" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + [[package]] name = "io-kit-sys" version = "0.4.1" @@ -2719,10 +2980,16 @@ dependencies = [ ] [[package]] -name = "ipnet" -version = "2.10.1" +name = "io_tee" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "itertools" @@ -2733,15 +3000,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -2753,9 +3011,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jni" @@ -2768,7 +3026,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] @@ -2861,7 +3119,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ "libc", - "libloading 0.8.5", + "libloading 0.8.6", "pkg-config", ] @@ -2937,7 +3195,7 @@ dependencies = [ "ledger-transport", "libc", "log", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -2957,7 +3215,7 @@ version = "9.0.0" dependencies = [ "bdk_coin_select", "bip39", - "getrandom", + "getrandom 0.2.15", "log", "miniscript", "rdrand", @@ -3026,6 +3284,7 @@ version = "9.0.0" dependencies = [ "backtrace", "bdk_electrum", + "bip329", "dirs 5.0.1", "fern", "jsonrpc 0.17.0", @@ -3040,19 +3299,18 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.162" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libfuzzer-sys" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" dependencies = [ "arbitrary", "cc", - "once_cell", ] [[package]] @@ -3067,9 +3325,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", "windows-targets 0.52.6", @@ -3087,9 +3345,9 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "libc", - "redox_syscall 0.5.7", + "redox_syscall 0.5.8", ] [[package]] @@ -3125,9 +3383,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" @@ -3137,9 +3395,9 @@ checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" [[package]] name = "litemap" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" @@ -3153,9 +3411,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "lru" @@ -3175,9 +3433,9 @@ dependencies = [ [[package]] name = "lyon_algorithms" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3bca95f9a4955b3e4a821fbbcd5edfbd9be2a9a50bb5758173e5358bfb4c623" +checksum = "f13c9be19d257c7d37e70608ed858e8eab4b2afcea2e3c9a622e892acbf43c08" dependencies = [ "lyon_path", "num-traits", @@ -3248,15 +3506,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - [[package]] name = "memoffset" version = "0.9.1" @@ -3272,7 +3521,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43f73953f8cbe511f021b58f18c3ce1c3d1ae13fe953293e13345bf83217f25" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block", "core-graphics-types 0.1.3", "foreign-types", @@ -3287,22 +3536,28 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniscript" version = "12.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bd3c9608217b0d6fa9c9c8ddd875b85ab72bd4311cfc8db35e1b5a08fc11f4d" dependencies = [ - "bech32", + "bech32 0.11.0", "bitcoin", "serde", ] [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" dependencies = [ "adler2", "simd-adler32", @@ -3310,9 +3565,9 @@ dependencies = [ [[package]] name = "minreq" -version = "2.12.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763d142cdff44aaadd9268bebddb156ef6c65a0e13486bb81673cf2d8739f9b0" +checksum = "da0c420feb01b9fb5061f8c8f452534361dd783756dcf38ec45191ce55e7a161" dependencies = [ "log", "serde", @@ -3321,37 +3576,25 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.11" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "log", - "wasi", - "windows-sys 0.48.0", -] - -[[package]] -name = "mio" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" -dependencies = [ - "hermit-abi 0.3.9", - "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] [[package]] name = "mio-serial" -version = "5.0.5" +version = "5.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20a4c60ca5c9c0e114b3bd66ff4aa5f9b2b175442be51ca6c4365d687a97a2ac" +checksum = "029e1f407e261176a983a6599c084efd322d9301028055c87174beac71397ba3" dependencies = [ "log", - "mio 0.8.11", - "nix 0.26.4", + "mio", + "nix 0.29.0", "serialport", "winapi", ] @@ -3375,7 +3618,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50e3524642f53d9af419ab5e8dd29d3ba155708267667c2f3f06c88c9e130843" dependencies = [ "bit-set", - "bitflags 2.6.0", + "bitflags 2.8.0", "codespan-reporting", "hexf-parse", "indexmap", @@ -3384,7 +3627,7 @@ dependencies = [ "rustc-hash 1.1.0", "spirv", "termcolor", - "thiserror", + "thiserror 1.0.69", "unicode-xid", ] @@ -3394,13 +3637,13 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "jni-sys", "log", "ndk-sys 0.6.0+11769913", "num_enum", "raw-window-handle", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -3436,8 +3679,6 @@ dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", - "memoffset 0.7.1", - "pin-utils", ] [[package]] @@ -3446,11 +3687,11 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", "cfg_aliases 0.2.1", "libc", - "memoffset 0.9.1", + "memoffset", ] [[package]] @@ -3483,6 +3724,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3550,7 +3801,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -3585,7 +3836,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "libc", "objc2", @@ -3601,7 +3852,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-core-location", @@ -3625,7 +3876,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-foundation", @@ -3657,9 +3908,9 @@ dependencies = [ [[package]] name = "objc2-encode" -version = "4.0.3" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" @@ -3667,7 +3918,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "dispatch", "libc", @@ -3692,7 +3943,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-foundation", @@ -3704,7 +3955,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-foundation", @@ -3727,7 +3978,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-cloud-kit", @@ -3759,7 +4010,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "objc2", "objc2-core-location", @@ -3777,9 +4028,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -3833,9 +4084,9 @@ dependencies = [ [[package]] name = "ouroboros" -version = "0.18.4" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "944fa20996a25aded6b4795c6d63f10014a7a83f8be9828a11860b08c5fc4a67" +checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" dependencies = [ "aliasable", "ouroboros_macro", @@ -3844,16 +4095,15 @@ dependencies = [ [[package]] name = "ouroboros_macro" -version = "0.18.4" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39b0deead1528fd0e5947a8546a9642a9777c25f6e1e26f34c97b204bbb465bd" +checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" dependencies = [ "heck", - "itertools 0.12.1", "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -3868,7 +4118,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" dependencies = [ - "ttf-parser 0.25.0", + "ttf-parser 0.25.1", ] [[package]] @@ -3892,7 +4142,7 @@ dependencies = [ "by_address", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -3944,7 +4194,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.7", + "redox_syscall 0.5.8", "smallvec", "windows-targets 0.52.6", ] @@ -3955,6 +4205,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -3973,9 +4233,9 @@ dependencies = [ [[package]] name = "phf" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_macros", "phf_shared", @@ -3983,9 +4243,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", "rand", @@ -3993,24 +4253,24 @@ dependencies = [ [[package]] name = "phf_macros" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" dependencies = [ "phf_generator", "phf_shared", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] name = "phf_shared" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 0.3.11", + "siphasher", ] [[package]] @@ -4021,29 +4281,29 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -4080,9 +4340,9 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "png" -version = "0.17.14" +version = "0.17.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -4108,9 +4368,9 @@ dependencies = [ [[package]] name = "pollster" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" [[package]] name = "poly1305" @@ -4170,10 +4430,32 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "1.0.89" +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.98", +] + +[[package]] +name = "proc-macro2" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -4186,7 +4468,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", "version_check", "yansi", ] @@ -4209,12 +4491,12 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0487d90e047de87f984913713b85c601c05609aad5b0df4b4573fbf69aa13f" +checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" dependencies = [ "bytes", - "prost-derive 0.13.3", + "prost-derive 0.13.4", ] [[package]] @@ -4254,15 +4536,15 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9552f850d5f0964a4e4d0bf306459ac29323ddfbae05e35a7c0d35cb0803cc5" +checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" dependencies = [ "anyhow", "itertools 0.13.0", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -4291,18 +4573,18 @@ checksum = "166f136dfdb199f98186f3649cf7a0536534a61417a1a30221b492b4fb60ce3f" [[package]] name = "quick-xml" -version = "0.36.2" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -4334,14 +4616,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] name = "range-alloc" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8a99fddc9f0ba0a85884b8d14e3592853e787d581ca1816c91349b10e4eeab" +checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" [[package]] name = "rangemap" @@ -4414,11 +4696,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] @@ -4427,9 +4709,9 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -4446,9 +4728,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -4538,12 +4820,14 @@ dependencies = [ [[package]] name = "rfd" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f6f80a9b882647d9014673ca9925d30ffc9750f2eed2b4490e189eaebd01e8" +checksum = "6a24763657bff09769a8ccf12c8b8a50416fb035fe199263b4c5071e4e3f006f" dependencies = [ "ashpd", "block2", + "core-foundation 0.10.0", + "core-foundation-sys", "js-sys", "log", "objc2", @@ -4555,7 +4839,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -4575,7 +4859,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.15", "libc", "spin", "untrusted", @@ -4594,7 +4878,7 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -4602,6 +4886,40 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-embed" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b3aba5104622db5c9fc61098de54708feb732e7763d7faa2fa625899f00bf6f" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f198c73be048d2c5aa8e12f7960ad08443e56fd39cc26336719fdb4ea0ebaae" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.98", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a2fcdc9f40c8dc2922842ca9add611ad19f332227fc651d015881ad1552bd9a" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "rust-ini" version = "0.19.0" @@ -4626,9 +4944,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc_version" @@ -4641,15 +4959,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.39" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "errno", "libc", - "linux-raw-sys 0.4.14", - "windows-sys 0.52.0", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", ] [[package]] @@ -4666,9 +4984,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.21" +version = "0.23.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7" dependencies = [ "log", "once_cell", @@ -4690,9 +5008,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" [[package]] name = "rustls-webpki" @@ -4727,7 +5045,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "bytemuck", "libm", "smallvec", @@ -4740,9 +5058,18 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] [[package]] name = "same-file" @@ -4765,6 +5092,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "sct" version = "0.7.1" @@ -4823,22 +5161,40 @@ dependencies = [ ] [[package]] -name = "self_cell" -version = "1.0.4" +name = "secrecy" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "self_cell" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" +dependencies = [ + "self_cell 1.1.0", +] + +[[package]] +name = "self_cell" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" [[package]] name = "semver" -version = "1.0.23" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" [[package]] name = "serde" -version = "1.0.214" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] @@ -4864,20 +5220,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", "memchr", @@ -4893,7 +5249,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -4910,11 +5266,11 @@ dependencies = [ [[package]] name = "serialport" -version = "4.6.0" +version = "4.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7331eefcaafaa382c0df95bcd84068f0b3e3c215c300750dde2316e9b8806ed5" +checksum = "5ecfc4858c2266c7695d8b8460bbd612fa81bd2e250f5f0dd16195e4b4f8b3d8" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", "core-foundation 0.10.0", "core-foundation-sys", @@ -4980,19 +5336,13 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "simplecss" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a11be7c62927d9427e9f40f3444d5499d868648e2edbc4e2116de69e7ec0e89d" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" dependencies = [ "log", ] -[[package]] -name = "siphasher" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" - [[package]] name = "siphasher" version = "1.0.1" @@ -5039,7 +5389,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "calloop", "calloop-wayland-source", "cursor-icon", @@ -5047,7 +5397,7 @@ dependencies = [ "log", "memmap2", "rustix", - "thiserror", + "thiserror 1.0.69", "wayland-backend", "wayland-client", "wayland-csd-frame", @@ -5102,9 +5452,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -5130,7 +5480,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "redox_syscall 0.5.7", + "redox_syscall 0.5.8", "rustix", "tiny-xlib", "wasm-bindgen", @@ -5154,7 +5504,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] @@ -5188,6 +5538,12 @@ dependencies = [ "float-cmp", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -5207,7 +5563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" dependencies = [ "kurbo 0.11.1", - "siphasher 1.0.1", + "siphasher", ] [[package]] @@ -5234,9 +5590,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.87" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -5257,7 +5613,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -5302,12 +5658,13 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.13.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" dependencies = [ "cfg-if", "fastrand", + "getrandom 0.3.1", "once_cell", "rustix", "windows-sys 0.59.0", @@ -5324,22 +5681,42 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", ] [[package]] name = "thiserror-impl" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", ] [[package]] @@ -5400,13 +5777,13 @@ dependencies = [ [[package]] name = "tiny-xlib" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d52f22673960ad13af14ff4025997312def1223bfa7c8e4949d099e6b3d5d1c" +checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e" dependencies = [ "as-raw-xcb-connection", "ctor-lite", - "libloading 0.8.5", + "libloading 0.8.6", "pkg-config", "tracing", ] @@ -5423,9 +5800,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -5438,14 +5815,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.41.1" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", "libc", - "mio 1.0.2", + "mio", "pin-project-lite", "signal-hook-registry", "socket2", @@ -5455,13 +5832,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -5476,22 +5853,23 @@ dependencies = [ [[package]] name = "tokio-serial" -version = "5.4.4" +version = "5.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa6e2e4cf0520a99c5f87d5abb24172b5bd220de57c3181baaaa5440540c64aa" +checksum = "aa1d5427f11ba7c5e6384521cfd76f2d64572ff29f3f4f7aa0f496282923fdc8" dependencies = [ "cfg-if", "futures", "log", "mio-serial", + "serialport", "tokio", ] [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -5517,9 +5895,9 @@ checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" dependencies = [ "indexmap", "toml_datetime", @@ -5534,9 +5912,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -5545,20 +5923,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -5577,9 +5955,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" dependencies = [ "nu-ansi-term", "sharded-slab", @@ -5609,9 +5987,18 @@ checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" [[package]] name = "ttf-parser" -version = "0.25.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5902c5d130972a0000f60860bfbf46f7ca3db5391eddfedd1b8728bd9dc96c0e" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "type-map" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb68604048ff8fa93347f02441e4487594adc20bb8a084f9e564d2b827a0a9f" +dependencies = [ + "rustc-hash 1.1.0", +] [[package]] name = "typenum" @@ -5625,7 +6012,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ - "memoffset 0.9.1", + "memoffset", "tempfile", "winapi", ] @@ -5636,14 +6023,33 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c878a167baa8afd137494101a688ef8c67125089ff2249284bd2b5f9bfedb815" dependencies = [ - "thiserror", + "thiserror 1.0.69", +] + +[[package]] +name = "unic-langid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dd9d1e72a73b25e07123a80776aae3e7b0ec461ef94f9151eed6ec88005a44" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5422c1f65949306c99240b81de9f3f15929f5a8bfe05bb44b034cc8bf593e5" +dependencies = [ + "serde", + "tinystr", ] [[package]] name = "unicode-bidi" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-bidi-mirroring" @@ -5659,9 +6065,9 @@ checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656" [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-linebreak" @@ -5732,9 +6138,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.3" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -5765,7 +6171,7 @@ dependencies = [ "roxmltree", "rustybuzz", "simplecss", - "siphasher 1.0.1", + "siphasher", "strict-num", "svgtypes", "tiny-skia-path", @@ -5789,9 +6195,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcpkg" @@ -5830,6 +6236,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -5852,18 +6267,19 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.45" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] @@ -5886,7 +6302,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5930,9 +6346,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" +checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf" dependencies = [ "cc", "downcast-rs", @@ -5944,11 +6360,11 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" +checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "rustix", "wayland-backend", "wayland-scanner", @@ -5960,16 +6376,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cursor-icon", "wayland-backend", ] [[package]] name = "wayland-cursor" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b08bc3aafdb0035e7fe0fdf17ba0c09c268732707dca4ae098f60cb28c9e4c" +checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d" dependencies = [ "rustix", "wayland-client", @@ -5978,11 +6394,11 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.5" +version = "0.32.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e" +checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -5990,11 +6406,11 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b31cab548ee68c7eb155517f2212049dc151f7cd7910c2b66abfd31c3ee12bd" +checksum = "7ccaacc76703fefd6763022ac565b590fcade92202492381c95b2edfdf7d46b3" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -6003,11 +6419,11 @@ dependencies = [ [[package]] name = "wayland-protocols-wlr" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "782e12f6cd923c3c316130d56205ebab53f55d6666b7faddfad36cecaeeb4022" +checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -6016,9 +6432,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.5" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" +checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" dependencies = [ "proc-macro2", "quick-xml", @@ -6027,9 +6443,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.5" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" +checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" dependencies = [ "dlib", "log", @@ -6102,7 +6518,7 @@ checksum = "28b94525fc99ba9e5c9a9e24764f2bc29bad0911a7446c12f446a8277369bf3a" dependencies = [ "arrayvec", "bit-vec", - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg_aliases 0.1.1", "codespan-reporting", "indexmap", @@ -6114,7 +6530,7 @@ dependencies = [ "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror", + "thiserror 1.0.69", "web-sys", "wgpu-hal", "wgpu-types", @@ -6130,7 +6546,7 @@ dependencies = [ "arrayvec", "ash", "bit-set", - "bitflags 2.6.0", + "bitflags 2.8.0", "block", "cfg_aliases 0.1.1", "core-graphics-types 0.1.3", @@ -6144,7 +6560,7 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading 0.8.5", + "libloading 0.8.6", "log", "metal", "naga", @@ -6158,7 +6574,7 @@ dependencies = [ "renderdoc-sys", "rustc-hash 1.1.0", "smallvec", - "thiserror", + "thiserror 1.0.69", "wasm-bindgen", "web-sys", "wgpu-types", @@ -6171,7 +6587,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b671ff9fb03f78b46ff176494ee1ebe7d603393f42664be55b64dc8d53969805" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "js-sys", "web-sys", ] @@ -6236,7 +6652,7 @@ dependencies = [ "clipboard_wayland", "clipboard_x11", "raw-window-handle", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -6481,7 +6897,7 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.6.0", + "bitflags 2.8.0", "block2", "bytemuck", "calloop", @@ -6526,9 +6942,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.20" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f" dependencies = [ "memchr", ] @@ -6543,6 +6959,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -6575,7 +7000,7 @@ dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", - "libloading 0.8.5", + "libloading 0.8.6", "once_cell", "rustix", "x11rb-protocol", @@ -6595,6 +7020,7 @@ checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", "rand_core", + "serde", "zeroize", ] @@ -6620,7 +7046,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "dlib", "log", "once_cell", @@ -6635,9 +7061,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.23" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af310deaae937e48a26602b730250b4949e125f468f11e6990be3e5304ddd96f" +checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" [[package]] name = "xmlwriter" @@ -6659,9 +7085,9 @@ checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" [[package]] name = "yoke" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" dependencies = [ "serde", "stable_deref_trait", @@ -6671,21 +7097,21 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", "synstructure", ] [[package]] name = "zbus" -version = "5.2.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb67eadba43784b6fb14857eba0d8fc518686d3ee537066eb6086dc318e2c8a1" +checksum = "cbddd8b6cb25d5d8ec1b23277b45299a98bfb220f1761ca11e186d5c702507f8" dependencies = [ "async-broadcast", "async-executor", @@ -6719,14 +7145,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.2.0" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d49ebc960ceb660f2abe40a5904da975de6986f2af0d7884b39eec6528c57" +checksum = "dac404d48b4e9cf193c8b49589f3280ceca5ff63519e7e64f55b4cf9c47ce146" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", "zbus_names", "zvariant", "zvariant_utils", @@ -6734,9 +7160,9 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856b7a38811f71846fd47856ceee8bccaec8399ff53fb370247e66081ace647b" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", @@ -6768,27 +7194,27 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] name = "zerofrom" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", "synstructure", ] @@ -6809,7 +7235,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -6831,7 +7257,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", ] [[package]] @@ -6858,9 +7284,9 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.1.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1200ee6ac32f1e5a312e455a949a4794855515d34f9909f4a3e082d14e1a56f" +checksum = "31c951c21879c6e1d46ac5adfc34f698fefb465d498cf4ac87545849bd71bb5a" dependencies = [ "endi", "enumflags2", @@ -6874,27 +7300,27 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.1.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687e3b97fae6c9104fbbd36c73d27d149abf04fb874e2efbd84838763daa8916" +checksum = "9eeb539471af098d9e63faf428c71ac4cd4efe0b5baa3c8a6b991c5f2543b70e" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.87", + "syn 2.0.98", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.0.2" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20d1d011a38f12360e5fcccceeff5e2c42a8eb7f27f0dcba97a0862ede05c9c6" +checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" dependencies = [ "proc-macro2", "quote", "serde", "static_assertions", - "syn 2.0.87", + "syn 2.0.98", "winnow", ] diff --git a/doc/API.md b/doc/API.md index 0be165b2..a7b0f9ff 100644 --- a/doc/API.md +++ b/doc/API.md @@ -9,8 +9,9 @@ Commands must be sent as valid JSONRPC 2.0 requests, ending with a `\n`. | ----------------------------------------------------------- | ---------------------------------------------------- | | [`stop`](#stop) | Stops liana daemon | | [`getinfo`](#getinfo) | Get general information about the daemon | +| [`updatederivationindexes`](#updatederivationindexes) | Update last generated addresses derivation indexes | | [`getnewaddress`](#getnewaddress) | Get a new receiving address | -| [`listaddresses`](#listaddresses) | List addresses given start_index and count | +| [`listaddresses`](#listaddresses) | List addresses given start_index and count | | [`listcoins`](#listcoins) | List all wallet transaction outputs. | | [`createspend`](#createspend) | Create a new Spend transaction | | [`updatespend`](#updatespend) | Store a created Spend transaction | @@ -24,6 +25,7 @@ Commands must be sent as valid JSONRPC 2.0 requests, ending with a `\n`. | [`createrecovery`](#createrecovery) | Create a recovery transaction to sweep expired coins | | [`updatelabels`](#updatelabels) | Update the labels | | [`getlabels`](#getlabels) | Get the labels for the given addresses, txids and outpoints | +| [`getlabelsbip329`](#getlabelsbip329) | Get the labels in BIP-0329 format | # Reference @@ -63,6 +65,38 @@ This command does not take any parameter for now. | `rescan_progress` | float or null | Progress of an ongoing rescan as a percentage (between 0 and 1) if there is any | | `timestamp` | integer | Unix timestamp of wallet creation date | | `last_poll_timestamp`| integer or null | Unix timestamp of last poll (if any) of the blockchain | +| `receive_index` | integer | Last index used to generate a receive address | +| `change_index` | integer | Last index used to generate a change address | + + +### `updatederivationindexes` + +Updates the last generated address derivation indexes in the wallet database. +At least one of the `receive` or `change` arguments is required. + +Derivation indexes **must be unhardened**. If a provided index is lower than +the one currently stored in the database, it will be ignored. + +**Note:** Each time a derivation index in the database is incremented, the +corresponding new addresses must be inserted into the database. To prevent +excessive increments, there is a limit: the derivation index can only be +incremented by a maximum of **1000** from its current value. + +The updated indexes will be returned in the response. + +#### Request + +| Field | Type | Description | +|-----------|-------------------|----------------------------------------------------------| +| `receive` | integer(optional) | The latest receive address derivation index to update | +| `change` | integer(optional) | The latest change address derivation index to update | + +#### Response + +| Field | Type | Description | +|-----------|---------|----------------------------------------------------------| +| `receive` | integer | The updated receive address derivation index | +| `change` | integer | The updated change address derivation index | ### `getnewaddress` @@ -427,3 +461,22 @@ Items without labels are not present in the response map. | Field | Type | Description | | -------- | ------ | -------------------------------------------------------------------------------- | | `labels` | object | A mapping of bitcoin addresses, txids and outpoints as keys, and string as values | + +### `getlabelsbip329` + +Retrieve a list of labels in [BIP-0329](https://github.com/bitcoin/bips/blob/master/bip-0329.mediawiki) +format, with pagination support. + +#### Request + +| Field | Type | Description | +| -------- | ------- | ------------------------------------------ | +| `offset` | integer | Index to start returning labels from | +| `limit` | integer | Maximum number of labels to return | + +#### Response + +| Field | Type | Description | +| -------- | ------ | ------------------------------------------------- | +| `labels` | array | A list of BIP-0329-formatted label objects | + diff --git a/liana-gui/src/app/error.rs b/liana-gui/src/app/error.rs index 4f279242..ce49658e 100644 --- a/liana-gui/src/app/error.rs +++ b/liana-gui/src/app/error.rs @@ -7,6 +7,7 @@ use lianad::config::ConfigError; use crate::{ app::{settings::SettingsError, wallet::WalletError}, daemon::DaemonError, + export::{self, RestoreBackupError}, }; #[derive(Debug)] @@ -18,6 +19,8 @@ pub enum Error { HardwareWallet(async_hwi::Error), Desc(LianaDescError), Spend(SpendCreationError), + ImportExport(export::Error), + RestoreBackup(RestoreBackupError), } impl std::fmt::Display for Error { @@ -53,10 +56,13 @@ impl std::fmt::Display for Error { write!(f, "[{:?}] {}", code, e) } DaemonError::CoinSelectionError => write!(f, "{}", e), + DaemonError::NotImplemented => write!(f, "{}", e), }, Self::Unexpected(e) => write!(f, "Unexpected error: {}", e), Self::HardwareWallet(e) => write!(f, "error: {}\nPlease check if the device is still connected and unlocked with the correct firmware open for the current network and no other application is accessing the device.", e), Self::Desc(e) => write!(f, "Liana descriptor error: {}", e), + Self::ImportExport(e) => write!(f, "{e}"), + Self::RestoreBackup(e) => write!(f, "{e}"), } } } diff --git a/liana-gui/src/app/message.rs b/liana-gui/src/app/message.rs index dad49c93..e7afe426 100644 --- a/liana-gui/src/app/message.rs +++ b/liana-gui/src/app/message.rs @@ -11,7 +11,7 @@ use lianad::config::Config as DaemonConfig; use crate::{ app::{cache::Cache, error::Error, view, wallet::Wallet}, daemon::model::*, - export::ExportMessage, + export::ImportExportMessage, hw::HardwareWalletMessage, }; @@ -47,5 +47,11 @@ pub enum Message { LabelsUpdated(Result>, Error>), BroadcastModal(Result, Error>), RbfModal(Box, bool, Result, Error>), - Export(ExportMessage), + Export(ImportExportMessage), +} + +impl From for Message { + fn from(value: ImportExportMessage) -> Self { + Message::View(view::Message::ImportExport(value)) + } } diff --git a/liana-gui/src/app/mod.rs b/liana-gui/src/app/mod.rs index 80680ebd..004a5193 100644 --- a/liana-gui/src/app/mod.rs +++ b/liana-gui/src/app/mod.rs @@ -62,6 +62,7 @@ impl Panels { data_dir: PathBuf, daemon_backend: DaemonBackend, internal_bitcoind: Option<&Bitcoind>, + config: Arc, ) -> Panels { Self { current: Menu::Home, @@ -93,6 +94,7 @@ impl Panels { wallet.clone(), daemon_backend, internal_bitcoind.is_some(), + config.clone(), ), } } @@ -132,7 +134,7 @@ impl Panels { pub struct App { cache: Cache, - config: Config, + config: Arc, wallet: Arc, daemon: Arc, internal_bitcoind: Option, @@ -149,12 +151,14 @@ impl App { data_dir: PathBuf, internal_bitcoind: Option, ) -> (App, Task) { + let config = Arc::new(config); let mut panels = Panels::new( &cache, wallet.clone(), data_dir, daemon.backend(), internal_bitcoind.as_ref(), + config.clone(), ); let cmd = panels.home.reload(daemon.clone(), wallet.clone()); ( diff --git a/liana-gui/src/app/settings.rs b/liana-gui/src/app/settings.rs index b4376154..b2b5aba7 100644 --- a/liana-gui/src/app/settings.rs +++ b/liana-gui/src/app/settings.rs @@ -6,9 +6,15 @@ use std::io::Write; use std::path::PathBuf; use liana::miniscript::bitcoin::{bip32::Fingerprint, Network}; +use liana_ui::component::form; use serde::{Deserialize, Serialize}; -use crate::{hw::HardwareWalletConfig, lianalite::client::backend, services}; +use crate::{ + backup::{Key, KeyRole, KeyType}, + hw::HardwareWalletConfig, + lianalite::client::backend, + services, +}; pub const DEFAULT_FILE_NAME: &str = "settings.json"; @@ -101,6 +107,25 @@ impl WalletSetting { } map } + + pub fn update_alias(&mut self, key: &Fingerprint, alias: &str) { + let key_aliases = self.keys_aliases(); + if key_aliases.contains_key(key) { + self.keys = self + .keys + .clone() + .into_iter() + .map(|mut ks| { + if ks.master_fingerprint == *key { + ks.name = alias.into(); + ks + } else { + ks + } + }) + .collect(); + } + } } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] @@ -151,6 +176,67 @@ pub struct KeySetting { pub provider_key: Option, } +impl KeySetting { + pub fn to_backup(&self) -> Key { + if let Some(provider_key) = &self.provider_key { + if let Ok(metadata) = serde_json::to_value(provider_key) { + return Key { + key: self.master_fingerprint, + alias: Some(self.name.clone()), + role: None, + key_type: Some(KeyType::ThirdParty), + proprietary: metadata, + }; + } + } + Key { + key: self.master_fingerprint, + alias: Some(self.name.clone()), + role: None, + key_type: None, + proprietary: serde_json::Value::Null, + } + } + + pub fn from_backup( + name: String, + fg: Fingerprint, + _role: Option, + key_type: Option, + metadata: serde_json::Value, + ) -> Option { + if let Some(KeyType::ThirdParty) = key_type { + let provider_key = serde_json::from_value(metadata).ok(); + Some(Self { + name, + master_fingerprint: fg, + provider_key, + }) + } else { + Some(Self { + name, + master_fingerprint: fg, + provider_key: None, + }) + } + } + + pub fn to_form(&self) -> form::Value { + form::Value { + value: self.name.clone(), + valid: true, + } + } + + pub fn name(&self) -> String { + self.name.clone() + } + + pub fn has_name(&self) -> bool { + !self.name.is_empty() + } +} + #[derive(PartialEq, Eq, Debug, Clone)] pub enum SettingsError { NotFound, diff --git a/liana-gui/src/app/state/export.rs b/liana-gui/src/app/state/export.rs index 193e6e3c..2dbee270 100644 --- a/liana-gui/src/app/state/export.rs +++ b/liana-gui/src/app/state/export.rs @@ -8,110 +8,231 @@ use liana_ui::{component::modal::Modal, widget::Element}; use tokio::task::JoinHandle; use crate::{ - app::{ - self, - view::{self, export::export_modal}, - }, + app::view::{export::export_modal, Close}, daemon::Daemon, - export::{self, get_path, ExportMessage, ExportProgress, ExportState}, + export::{self, get_path, ImportExportMessage, ImportExportState, ImportExportType, Progress}, }; #[derive(Debug)] pub struct ExportModal { path: Option, handle: Option>>>, - state: ExportState, + state: ImportExportState, error: Option, - daemon: Arc, + daemon: Option>, + import_export_type: ImportExportType, } impl ExportModal { #[allow(clippy::new_without_default)] - pub fn new(daemon: Arc) -> Self { + pub fn new( + daemon: Option>, + export_type: ImportExportType, + ) -> Self { Self { path: None, handle: None, - state: ExportState::Init, + state: ImportExportState::Init, error: None, daemon, + import_export_type: export_type, } } - pub fn launch(&self) -> Task { - Task::perform(get_path(), |m| { - app::message::Message::View(view::Message::Export(ExportMessage::Path(m))) + pub fn modal_title(&self) -> &'static str { + match self.import_export_type { + ImportExportType::Transactions => "Export Transactions", + ImportExportType::ExportPsbt(_) => "Export PSBT", + ImportExportType::ExportBackup(_) => "Export Backup", + ImportExportType::Descriptor(_) => "Export Descriptor", + ImportExportType::ExportLabels => "Export Labels", + ImportExportType::ImportPsbt => "Import PSBT", + ImportExportType::ImportDescriptor => "Import Descriptor", + ImportExportType::ImportBackup(..) => "Restore Backup", + ImportExportType::WalletFromBackup => "Import existing wallet from backup", + } + } + + pub fn default_filename(&self) -> String { + let date = chrono::Local::now().format("%Y-%m-%dT%H-%M-%S"); + match &self.import_export_type { + ImportExportType::Transactions => { + format!("liana-txs-{date}.csv") + } + ImportExportType::ExportPsbt(_) => "psbt.psbt".into(), + ImportExportType::Descriptor(descriptor) => { + let checksum = descriptor + .to_string() + .split_once('#') + .map(|(_, checksum)| checksum) + .expect("cannot fail") + .to_string(); + format!("liana-{}.descriptor", checksum) + } + ImportExportType::ImportPsbt => "psbt.psbt".into(), + ImportExportType::ImportDescriptor => "descriptor.descriptor".into(), + ImportExportType::ExportLabels => format!("liana-labels-{date}.jsonl"), + ImportExportType::ExportBackup(_) => { + format!("liana-backup-{date}.json") + } + ImportExportType::WalletFromBackup | ImportExportType::ImportBackup(_, _) => { + "liana-backup.json".to_string() + } + } + } + + pub fn launch + Send + 'static>(&self, write: bool) -> Task { + Task::perform(get_path(self.default_filename(), write), move |m| { + ImportExportMessage::Path(m).into() }) } - pub fn update(&mut self, message: ExportMessage) -> Task { + pub fn update + Send + 'static>( + &mut self, + message: ImportExportMessage, + ) -> Task { match message { - ExportMessage::ExportProgress(m) => match m { - ExportProgress::Started(handle) => { + ImportExportMessage::Progress(m) => match m { + Progress::Started(handle) => { self.handle = Some(handle); - self.state = ExportState::Progress(0.0); + self.state = ImportExportState::Progress(0.0); } - ExportProgress::Progress(p) => { - if let ExportState::Progress(_) = self.state { - self.state = ExportState::Progress(p); + Progress::Progress(p) => { + if let ImportExportState::Progress(_) = self.state { + self.state = ImportExportState::Progress(p); } } - ExportProgress::Finished | ExportProgress::Ended => self.state = ExportState::Ended, - ExportProgress::Error(e) => self.error = Some(e), - ExportProgress::None => {} + Progress::Finished | Progress::Ended => self.state = ImportExportState::Ended, + Progress::KeyAliasesConflict(ref sender) => { + if let ImportExportType::ImportBackup(_, None) = &self.import_export_type { + self.import_export_type = + ImportExportType::ImportBackup(None, Some(sender.clone())); + } + } + Progress::LabelsConflict(ref sender) => { + if let ImportExportType::ImportBackup(None, _) = &self.import_export_type { + self.import_export_type = + ImportExportType::ImportBackup(Some(sender.clone()), None); + } + } + Progress::Error(e) => { + self.error = Some(e.clone()); + } + Progress::None => {} + Progress::Psbt(_) => { + if self.import_export_type == ImportExportType::ImportPsbt { + self.state = ImportExportState::Ended; + } + // TODO: forward PSBT + } + Progress::Descriptor(_) => { + if self.import_export_type == ImportExportType::ImportDescriptor { + self.state = ImportExportState::Ended; + } + // TODO: forward Descriptor + } + Progress::UpdateAliases(map) => { + return Task::perform(async {}, move |_| { + ImportExportMessage::UpdateAliases(map.clone()).into() + }); + } + Progress::WalletFromBackup(_) => {} }, - ExportMessage::TimedOut => { - self.stop(ExportState::TimedOut); + ImportExportMessage::TimedOut => { + self.stop(ImportExportState::TimedOut); } - ExportMessage::UserStop => { - self.stop(ExportState::Aborted); + ImportExportMessage::UserStop => { + self.stop(ImportExportState::Aborted); } - ExportMessage::Path(p) => { + ImportExportMessage::Path(p) => { if let Some(path) = p { self.path = Some(path); self.start(); } else { - return Task::perform(async {}, |_| { - app::message::Message::View(view::Message::Export(ExportMessage::Close)) - }); + return Task::perform(async {}, |_| ImportExportMessage::Close.into()); } } - ExportMessage::Close | ExportMessage::Open => { /* unreachable */ } + ImportExportMessage::Close | ImportExportMessage::Open => { /* unreachable */ } + ImportExportMessage::Overwrite => { + if let ImportExportType::ImportBackup(labels, aliases) = + &mut self.import_export_type + { + if let Some(sender) = labels.take() { + if sender.send(true).is_err() { + tracing::error!("ExportModal.update(): fail to send labels ACK"); + } + } else if let Some(sender) = aliases.take() { + if sender.send(true).is_err() { + tracing::error!("ExportModal.update(): fail to send aliases ACK"); + } + } + } + } + ImportExportMessage::Ignore => { + if let ImportExportType::ImportBackup(labels, aliases) = + &mut self.import_export_type + { + if let Some(sender) = labels.take() { + if sender.send(false).is_err() { + tracing::error!("ExportModal.update(): fail to send labels NACK"); + } + } else if let Some(sender) = aliases.take() { + if sender.send(false).is_err() { + tracing::error!("ExportModal.update(): fail to send aliases NACK"); + } + } + } + } + ImportExportMessage::UpdateAliases(_) => { /* unexpected */ } } Task::none() } - pub fn view<'a>(&'a self, content: Element<'a, view::Message>) -> Element { + + pub fn view<'a, M>(&'a self, content: Element<'a, M>) -> Element + where + M: 'a + Close + Clone + From, + { let modal = Modal::new( content, - export_modal(&self.state, self.error.as_ref(), "Transactions"), + export_modal( + &self.state, + self.error.as_ref(), + self.modal_title(), + &self.import_export_type, + ), ); match self.state { - ExportState::TimedOut - | ExportState::Aborted - | ExportState::Ended - | ExportState::Closed => modal.on_blur(Some(view::Message::Close)), + ImportExportState::TimedOut + | ImportExportState::Aborted + | ImportExportState::Ended + | ImportExportState::Closed => modal.on_blur(Some(M::close())), _ => modal, } .into() } pub fn start(&mut self) { - self.state = ExportState::Started; + self.state = ImportExportState::Started; } - pub fn stop(&mut self, state: ExportState) { + pub fn stop(&mut self, state: ImportExportState) { if let Some(handle) = self.handle.take() { handle.lock().expect("poisoned").abort(); self.state = state; } } - pub fn subscription(&self) -> Option> { + pub fn subscription(&self) -> Option> { if let Some(path) = &self.path { match &self.state { - ExportState::Started | ExportState::Progress(_) => { + ImportExportState::Started | ImportExportState::Progress(_) => { Some(iced::Subscription::run_with_id( - "transactions", - export::export_subscription(self.daemon.clone(), path.to_path_buf()), + self.modal_title(), + export::export_subscription( + self.daemon.clone(), + path.to_path_buf(), + self.import_export_type.clone(), + ), )) } _ => None, diff --git a/liana-gui/src/app/state/mod.rs b/liana-gui/src/app/state/mod.rs index e76c274b..a54f0397 100644 --- a/liana-gui/src/app/state/mod.rs +++ b/liana-gui/src/app/state/mod.rs @@ -1,5 +1,5 @@ mod coins; -mod export; +pub mod export; mod label; mod psbt; mod psbts; diff --git a/liana-gui/src/app/state/psbt.rs b/liana-gui/src/app/state/psbt.rs index df074374..cc6f2a7e 100644 --- a/liana-gui/src/app/state/psbt.rs +++ b/liana-gui/src/app/state/psbt.rs @@ -754,6 +754,8 @@ mod tests { "block_height": 1000, "sync": 1.0, "descriptors": { "main": LianaDescriptor::from_str(DESC).unwrap() }, + "receive_index": 4, + "change_index": 3, "timestamp": 1000, })), ), diff --git a/liana-gui/src/app/state/settings/mod.rs b/liana-gui/src/app/state/settings/mod.rs index 64ec0b27..e8f625e4 100644 --- a/liana-gui/src/app/state/settings/mod.rs +++ b/liana-gui/src/app/state/settings/mod.rs @@ -10,7 +10,7 @@ use iced::Task; use liana_ui::{component::form, widget::Element}; use bitcoind::BitcoindSettingsState; -use wallet::WalletSettingsState; +use wallet::{app_backup, WalletSettingsState}; use crate::{ app::{ @@ -20,16 +20,21 @@ use crate::{ state::State, view::{self}, wallet::Wallet, + Config, }, daemon::{Daemon, DaemonBackend}, + export::{self, ImportExportMessage, ImportExportType}, }; +use super::export::ExportModal; + pub struct SettingsState { data_dir: PathBuf, wallet: Arc, setting: Option>, daemon_backend: DaemonBackend, internal_bitcoind: bool, + config: Arc, } impl SettingsState { @@ -38,6 +43,7 @@ impl SettingsState { wallet: Arc, daemon_backend: DaemonBackend, internal_bitcoind: bool, + config: Arc, ) -> Self { Self { data_dir, @@ -45,6 +51,7 @@ impl SettingsState { setting: None, daemon_backend, internal_bitcoind, + config, } } } @@ -79,6 +86,12 @@ impl State for SettingsState { self.setting = Some(BackendSettingsState::new().into()); Task::none() } + Message::View(view::Message::Settings(view::SettingsMessage::ImportExportSection)) => { + self.setting = Some( + ImportExportSettingsState::new(self.wallet.clone(), self.config.clone()).into(), + ); + Task::none() + } Message::View(view::Message::Settings(view::SettingsMessage::AboutSection)) => { self.setting = Some(AboutSettingsState::default().into()); let wallet = self.wallet.clone(); @@ -89,7 +102,12 @@ impl State for SettingsState { } Message::View(view::Message::Settings(view::SettingsMessage::EditWalletSettings)) => { self.setting = Some( - WalletSettingsState::new(self.data_dir.clone(), self.wallet.clone()).into(), + WalletSettingsState::new( + self.data_dir.clone(), + self.wallet.clone(), + self.config.clone(), + ) + .into(), ); let wallet = self.wallet.clone(); self.setting @@ -145,6 +163,144 @@ impl From for Box { } } +pub struct ImportExportSettingsState { + warning: Option, + modal: Option, + wallet: Arc, + config: Arc, +} + +impl ImportExportSettingsState { + pub fn new(wallet: Arc, config: Arc) -> Self { + Self { + warning: None, + modal: None, + wallet, + config, + } + } +} + +macro_rules! launch { + ($s:ident, $m: ident, $write:ident) => { + let launch = $m.launch($write); + $s.modal = Some($m); + return launch + }; +} + +impl State for ImportExportSettingsState { + fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> { + let content = view::settings::import_export(cache, self.warning.as_ref()); + if let Some(modal) = &self.modal { + modal.view(content) + } else { + content + } + } + + fn subscription(&self) -> iced::Subscription { + if let Some(modal) = &self.modal { + if let Some(sub) = modal.subscription() { + return sub.map(|m| { + Message::View(view::Message::Settings( + view::SettingsMessage::ImportExport(ImportExportMessage::Progress(m)), + )) + }); + } + } + iced::Subscription::none() + } + + fn update( + &mut self, + daemon: Arc, + cache: &Cache, + message: Message, + ) -> Task { + match message { + Message::View(view::Message::ImportExport(ImportExportMessage::Close)) => { + self.modal = None; + } + Message::View(view::Message::ImportExport(m)) => { + if let Some(modal) = self.modal.as_mut() { + return modal.update(m); + }; + } + Message::View(view::Message::Settings(view::SettingsMessage::ImportExport(m))) => { + if let Some(modal) = self.modal.as_mut() { + return modal.update(m); + }; + } + Message::View(view::Message::Settings(view::SettingsMessage::ExportDescriptor)) => { + if self.modal.is_none() { + let modal = ExportModal::new( + Some(daemon), + ImportExportType::Descriptor(self.wallet.main_descriptor.clone()), + ); + launch!(self, modal, true); + } + } + Message::View(view::Message::Settings(view::SettingsMessage::ExportTransactions)) => { + if self.modal.is_none() { + let modal = ExportModal::new(Some(daemon), ImportExportType::Transactions); + launch!(self, modal, true); + } + } + Message::View(view::Message::Settings(view::SettingsMessage::ExportLabels)) => { + if self.modal.is_none() { + let modal = ExportModal::new(Some(daemon), ImportExportType::ExportLabels); + launch!(self, modal, true); + } + } + Message::View(view::Message::Settings(view::SettingsMessage::ExportWallet)) => { + if self.modal.is_none() { + let datadir = cache.datadir_path.clone(); + let network = cache.network; + let config = self.config.clone(); + let wallet = self.wallet.clone(); + let daemon = daemon.clone(); + return Task::perform( + async move { app_backup(datadir, network, config, wallet, daemon).await }, + |s| { + Message::View(view::Message::Settings( + view::SettingsMessage::ExportBackup(s), + )) + }, + ); + } + } + Message::View(view::Message::Settings(view::SettingsMessage::ExportBackup(backup))) => { + let backup = match backup { + Ok(b) => b, + Err(e) => { + self.warning = Some(Error::ImportExport(export::Error::Backup(e))); + return Task::none(); + } + }; + let modal = ExportModal::new(Some(daemon), ImportExportType::ExportBackup(backup)); + launch!(self, modal, true); + } + Message::View(view::Message::Settings(view::SettingsMessage::ImportWallet)) => { + if self.modal.is_none() { + let modal = + ExportModal::new(Some(daemon), ImportExportType::ImportBackup(None, None)); + launch!(self, modal, false); + } + } + _ => {} + } + + Task::none() + } +} + +impl From for Box { + fn from(s: ImportExportSettingsState) -> Box { + Box::new(s) + } +} + #[derive(Default)] pub struct AboutSettingsState { daemon_version: Option, diff --git a/liana-gui/src/app/state/settings/wallet.rs b/liana-gui/src/app/state/settings/wallet.rs index f5281fae..1bf7c387 100644 --- a/liana-gui/src/app/state/settings/wallet.rs +++ b/liana-gui/src/app/state/settings/wallet.rs @@ -17,34 +17,57 @@ use liana_ui::{ use crate::{ app::{ - cache::Cache, error::Error, message::Message, settings, state::State, view, wallet::Wallet, + cache::Cache, + error::Error, + message::Message, + settings, + state::{export::ExportModal, State}, + view, + wallet::Wallet, + Config, }, + backup::{self, Backup}, daemon::{Daemon, DaemonBackend}, + export::{self, ImportExportMessage, ImportExportType}, hw::{HardwareWallet, HardwareWalletConfig, HardwareWallets}, }; +enum Modal { + None, + RegisterWallet(RegisterWalletModal), + ImportExport(ExportModal), +} + +impl Modal { + fn is_none(&self) -> bool { + matches!(self, Modal::None) + } +} + pub struct WalletSettingsState { data_dir: PathBuf, warning: Option, descriptor: LianaDescriptor, keys_aliases: Vec<(Fingerprint, form::Value)>, wallet: Arc, - modal: Option, + modal: Modal, processing: bool, updated: bool, + config: Arc, } impl WalletSettingsState { - pub fn new(data_dir: PathBuf, wallet: Arc) -> Self { + pub fn new(data_dir: PathBuf, wallet: Arc, config: Arc) -> Self { WalletSettingsState { data_dir, descriptor: wallet.main_descriptor.clone(), keys_aliases: Self::keys_aliases(&wallet), wallet, warning: None, - modal: None, + modal: Modal::None, processing: false, updated: false, + config, } } @@ -86,20 +109,31 @@ impl State for WalletSettingsState { self.processing, self.updated, ); - if let Some(m) = &self.modal { - modal::Modal::new(content, m.view()) + + match &self.modal { + Modal::None => content, + Modal::RegisterWallet(m) => modal::Modal::new(content, m.view()) .on_blur(Some(view::Message::Close)) - .into() - } else { - content + .into(), + Modal::ImportExport(m) => m.view(content), } } fn subscription(&self) -> Subscription { - if let Some(modal) = &self.modal { - modal.subscription() - } else { - Subscription::none() + match &self.modal { + Modal::None => Subscription::none(), + Modal::RegisterWallet(modal) => modal.subscription(), + Modal::ImportExport(modal) => { + if let Some(sub) = modal.subscription() { + sub.map(|m| { + Message::View(view::Message::Settings( + view::SettingsMessage::ImportExport(ImportExportMessage::Progress(m)), + )) + }) + } else { + Subscription::none() + } + } } } @@ -112,7 +146,7 @@ impl State for WalletSettingsState { match message { Message::WalletUpdated(res) => { self.processing = false; - if let Some(modal) = &mut self.modal { + if let Modal::RegisterWallet(modal) = &mut self.modal { modal.update(daemon, cache, Message::WalletUpdated(res)) } else { match res { @@ -139,7 +173,7 @@ impl State for WalletSettingsState { Task::none() } Message::View(view::Message::Settings(view::SettingsMessage::Save)) => { - self.modal = None; + self.modal = Modal::None; self.processing = true; self.updated = false; Task::perform( @@ -157,22 +191,106 @@ impl State for WalletSettingsState { ) } Message::View(view::Message::Close) => { - self.modal = None; + self.modal = Modal::None; Task::none() } Message::View(view::Message::Settings(view::SettingsMessage::RegisterWallet)) => { - self.modal = Some(RegisterWalletModal::new( + self.modal = Modal::RegisterWallet(RegisterWalletModal::new( self.data_dir.clone(), self.wallet.clone(), cache.network, )); Task::none() } - _ => self - .modal - .as_mut() - .map(|m| m.update(daemon, cache, message)) - .unwrap_or_else(Task::none), + + Message::View(view::Message::ImportExport(ImportExportMessage::UpdateAliases( + aliases, + ))) => { + self.processing = true; + self.updated = false; + Task::perform( + update_keys_aliases( + self.data_dir.clone(), + cache.network, + self.wallet.clone(), + aliases.into_iter().map(|(fg, ks)| (fg, ks.name)).collect(), + daemon, + ), + Message::WalletUpdated, + ) + } + Message::View(view::Message::ImportExport(ImportExportMessage::Close)) => { + if let Modal::ImportExport(_) = &self.modal { + self.modal = Modal::None; + } + Task::none() + } + Message::View(view::Message::ImportExport(m)) => { + if let Modal::ImportExport(modal) = &mut self.modal { + modal.update(m) + } else { + Task::none() + } + } + Message::View(view::Message::Settings(view::SettingsMessage::ImportExport(m))) => { + if let Modal::ImportExport(modal) = &mut self.modal { + modal.update(m) + } else { + Task::none() + } + } + Message::View(view::Message::Settings(view::SettingsMessage::ExportWallet)) => { + if self.modal.is_none() { + let datadir = cache.datadir_path.clone(); + let network = cache.network; + let config = self.config.clone(); + let wallet = self.wallet.clone(); + let daemon = daemon.clone(); + Task::perform( + async move { app_backup(datadir, network, config, wallet, daemon).await }, + |s| { + Message::View(view::Message::Settings( + view::SettingsMessage::ExportBackup(s), + )) + }, + ) + } else { + Task::none() + } + } + Message::View(view::Message::Settings(view::SettingsMessage::ExportBackup(backup))) => { + if self.modal.is_none() { + let backup = match backup { + Ok(b) => b, + Err(e) => { + self.warning = Some(Error::ImportExport(export::Error::Backup(e))); + return Task::none(); + } + }; + let modal = + ExportModal::new(Some(daemon), ImportExportType::ExportBackup(backup)); + let launch = modal.launch(true); + self.modal = Modal::ImportExport(modal); + launch + } else { + Task::none() + } + } + Message::View(view::Message::Settings(view::SettingsMessage::ImportWallet)) => { + if self.modal.is_none() { + let modal = + ExportModal::new(Some(daemon), ImportExportType::ImportBackup(None, None)); + let launch = modal.launch(false); + self.modal = Modal::ImportExport(modal); + launch + } else { + Task::none() + } + } + _ => match &mut self.modal { + Modal::RegisterWallet(m) => m.update(daemon, cache, message), + _ => Task::none(), + }, } } @@ -370,7 +488,7 @@ async fn register_wallet( Ok(wallet) } -async fn update_keys_aliases( +pub async fn update_keys_aliases( data_dir: PathBuf, network: Network, wallet: Arc, @@ -407,3 +525,14 @@ async fn update_keys_aliases( Ok(Arc::new(wallet)) } + +pub async fn app_backup( + datadir: PathBuf, + network: Network, + config: Arc, + wallet: Arc, + daemon: Arc, +) -> Result { + let backup = Backup::from_app(datadir, network, config, wallet, daemon).await?; + serde_json::to_string_pretty(&backup).map_err(|_| backup::Error::Json) +} diff --git a/liana-gui/src/app/state/transactions.rs b/liana-gui/src/app/state/transactions.rs index a8a0d4fd..3aca960f 100644 --- a/liana-gui/src/app/state/transactions.rs +++ b/liana-gui/src/app/state/transactions.rs @@ -28,7 +28,7 @@ use crate::{ wallet::Wallet, }, daemon::model::{self, LabelsLoader}, - export::ExportMessage, + export::{ImportExportMessage, ImportExportType}, }; use crate::daemon::{ @@ -266,15 +266,18 @@ impl State for TransactionsPanel { ); } } - Message::View(view::Message::Export(ExportMessage::Open)) => { + Message::View(view::Message::ImportExport(ImportExportMessage::Open)) => { if let TransactionsModal::None = &self.modal { - self.modal = TransactionsModal::Export(ExportModal::new(daemon)); + self.modal = TransactionsModal::Export(ExportModal::new( + Some(daemon), + ImportExportType::Transactions, + )); if let TransactionsModal::Export(m) = &self.modal { - return m.launch(); + return m.launch(true); } } } - Message::View(view::Message::Export(ExportMessage::Close)) => { + Message::View(view::Message::ImportExport(ImportExportMessage::Close)) => { if let TransactionsModal::Export(_) = &self.modal { self.modal = TransactionsModal::None; } @@ -283,8 +286,8 @@ impl State for TransactionsPanel { return match &mut self.modal { TransactionsModal::CreateRbf(modal) => modal.update(daemon, _cache, message), TransactionsModal::Export(modal) => { - if let Message::View(view::Message::Export(m)) = msg { - modal.update(m.clone()) + if let Message::View(view::Message::ImportExport(m)) = msg { + modal.update::(m.clone()) } else { Task::none() } @@ -327,7 +330,9 @@ impl State for TransactionsPanel { if let TransactionsModal::Export(modal) = &self.modal { if let Some(sub) = modal.subscription() { return sub.map(|m| { - Message::View(view::Message::Export(ExportMessage::ExportProgress(m))) + Message::View(view::Message::ImportExport(ImportExportMessage::Progress( + m, + ))) }); } } diff --git a/liana-gui/src/app/view/export.rs b/liana-gui/src/app/view/export.rs index af9e9b2d..6c707985 100644 --- a/liana-gui/src/app/view/export.rs +++ b/liana-gui/src/app/view/export.rs @@ -11,49 +11,102 @@ use liana_ui::{ widget::Element, }; -use crate::export::{Error, ExportMessage}; -use crate::{app::view::message::Message, export::ExportState}; +use crate::export::ImportExportState; +use crate::export::{Error, ImportExportMessage, ImportExportType}; /// Return the modal view for an export task -pub fn export_modal<'a>( - state: &ExportState, +pub fn export_modal<'a, Message: From + Clone + 'a>( + state: &ImportExportState, error: Option<&'a Error>, - export_type: &str, + title: &str, + import_export_type: &ImportExportType, ) -> Element<'a, Message> { - let button = match state { - ExportState::Started | ExportState::Progress(_) => { - Some(button::secondary(None, "Cancel").on_press(ExportMessage::UserStop.into())) + let cancel_close = match state { + ImportExportState::Started | ImportExportState::Progress(_) => { + Some(button::secondary(None, "Cancel").on_press(ImportExportMessage::UserStop.into())) } - ExportState::Ended | ExportState::TimedOut | ExportState::Aborted => { - Some(button::secondary(None, "Close").on_press(ExportMessage::Close.into())) + ImportExportState::Ended | ImportExportState::TimedOut | ImportExportState::Aborted => { + Some(button::secondary(None, "Close").on_press(ImportExportMessage::Close.into())) } _ => None, - }; + } + .map(Container::new); + let msg = if let Some(error) = error { - format!("{:?}", error) + format!("{}", error) } else { match state { - ExportState::Init => "".to_string(), - ExportState::ChoosePath => { + ImportExportState::Init => "".to_string(), + ImportExportState::ChoosePath => { "Select the path you want to export in the popup window...".into() } - ExportState::Path(_) => "".into(), - ExportState::Started => "Starting export...".into(), - ExportState::Progress(p) => format!("Progress: {}%", p.round()), - ExportState::TimedOut => "Export failed: timeout".into(), - ExportState::Aborted => "Export canceled".into(), - ExportState::Ended => "Export successful!".into(), - ExportState::Closed => "".into(), + ImportExportState::Path(_) => "".into(), + ImportExportState::Started => "Starting export...".into(), + ImportExportState::Progress(p) => format!("Progress: {}%", p.round()), + ImportExportState::TimedOut => "Export failed: timeout".into(), + ImportExportState::Aborted => "Export canceled".into(), + ImportExportState::Ended => import_export_type.end_message().into(), + ImportExportState::Closed => "".into(), } }; - let p = match state { - ExportState::Init => 0.0, - ExportState::ChoosePath | ExportState::Path(_) | ExportState::Started => 5.0, - ExportState::Progress(p) => *p, - ExportState::TimedOut | ExportState::Aborted | ExportState::Ended | ExportState::Closed => { - 100.0 - } + let labels_btn = ( + "Labels conflict, what do you want to do?".to_string(), + Some(Container::new( + Row::new() + .push( + button::secondary(None, "Overwrite") + .on_press(ImportExportMessage::Overwrite.into()), + ) + .push(Space::with_width(30)) + .push( + button::secondary(None, "Ignore").on_press(ImportExportMessage::Ignore.into()), + ), + )), + ); + let aliases_btn = ( + "Aliases conflict, what do you want to do?".to_string(), + Some(Container::new( + Row::new() + .push( + button::secondary(None, "Overwrite") + .on_press(ImportExportMessage::Overwrite.into()), + ) + .push(Space::with_width(30)) + .push( + button::secondary(None, "Ignore").on_press(ImportExportMessage::Ignore.into()), + ), + )), + ); + let (msg, button) = match import_export_type { + ImportExportType::ImportBackup(labels, aliases) => match (labels, aliases) { + (Some(_), _) => labels_btn, + + (_, Some(_)) => aliases_btn, + _ => (msg, cancel_close), + }, + _ => (msg, cancel_close), }; + let button = button.map(|b| { + Container::new(b) + .align_x(Horizontal::Center) + .width(Length::Fill) + }); + + let mut p = match state { + ImportExportState::Init => 0.0, + ImportExportState::ChoosePath | ImportExportState::Path(_) | ImportExportState::Started => { + 5.0 + } + ImportExportState::Progress(p) => *p, + ImportExportState::TimedOut + | ImportExportState::Aborted + | ImportExportState::Ended + | ImportExportState::Closed => 100.0, + }; + // keep progress bar visible + if p == 0.0 { + p += 2.5; + } let progress_bar_row = Row::new() .push(Space::with_width(30)) .push(progress_bar(0.0..=100.0, p)) @@ -61,20 +114,16 @@ pub fn export_modal<'a>( card::simple( Column::new() .spacing(10) - .push(Container::new(h4_bold(format!("Export {export_type}"))).width(Length::Fill)) + .push(Container::new(h4_bold(title)).width(Length::Fill)) .push(Space::with_height(Length::Fill)) .push(progress_bar_row) .push(Space::with_height(Length::Fill)) .push(Row::new().push(text(msg))) .push(Space::with_height(Length::Fill)) - .push_maybe(button.map(|b| { - Container::new(b) - .align_x(Horizontal::Right) - .width(Length::Fill) - })) + .push_maybe(button) .push(Space::with_height(5)), ) .width(Length::Fixed(500.0)) - .height(Length::Fixed(220.0)) + .height(Length::Fixed(250.0)) .into() } diff --git a/liana-gui/src/app/view/message.rs b/liana-gui/src/app/view/message.rs index 345dd1dd..e5c46436 100644 --- a/liana-gui/src/app/view/message.rs +++ b/liana-gui/src/app/view/message.rs @@ -1,6 +1,10 @@ -use crate::{app::menu::Menu, export::ExportMessage, node::bitcoind::RpcAuthType}; +use crate::{app::menu::Menu, backup, export::ImportExportMessage, node::bitcoind::RpcAuthType}; use liana::miniscript::bitcoin::{bip32::Fingerprint, OutPoint}; +pub trait Close { + fn close() -> Self; +} + #[derive(Debug, Clone)] pub enum Message { Reload, @@ -19,7 +23,13 @@ pub enum Message { SelectHardwareWallet(usize), CreateRbf(CreateRbfMessage), ShowQrCode(usize), - Export(ExportMessage), + ImportExport(ImportExportMessage), +} + +impl Close for Message { + fn close() -> Self { + Self::Close + } } #[derive(Debug, Clone)] @@ -70,9 +80,17 @@ pub enum SettingsMessage { BitcoindSettings(SettingsEditMessage), ElectrumSettings(SettingsEditMessage), RescanSettings(SettingsEditMessage), + ImportExport(ImportExportMessage), EditRemoteBackendSettings, RemoteBackendSettings(RemoteBackendSettingsMessage), EditWalletSettings, + ImportExportSection, + ExportDescriptor, + ExportTransactions, + ExportLabels, + ExportWallet, + ExportBackup(Result), + ImportWallet, AboutSection, RegisterWallet, FingerprintAliasEdited(Fingerprint, String), diff --git a/liana-gui/src/app/view/settings.rs b/liana-gui/src/app/view/settings.rs index ec0d2627..6de7a004 100644 --- a/liana-gui/src/app/view/settings.rs +++ b/liana-gui/src/app/view/settings.rs @@ -106,6 +106,13 @@ pub fn list(cache: &Cache, is_remote_backend: bool) -> Element { Message::Settings(SettingsMessage::EditWalletSettings), ); + let import_export = settings_section( + "Import/export", + None, + icon::wallet_icon(), + Message::Settings(SettingsMessage::ImportExportSection), + ); + let recovery = settings_section( "Recovery", Some("In case of loss of the main key, the recovery key can move the funds after a certain time."), @@ -130,6 +137,7 @@ pub fn list(cache: &Cache, is_remote_backend: bool) -> Element { .push(header) .push(if !is_remote_backend { node } else { backend }) .push(wallet) + .push(import_export) .push(recovery) .push(about), ) @@ -153,6 +161,60 @@ pub fn bitcoind_settings<'a>( ) } +pub fn import_export<'a>(cache: &'a Cache, warning: Option<&Error>) -> Element<'a, Message> { + let header = header("Import/Export", SettingsMessage::ImportExportSection); + + let export_descriptor = settings_section( + "Export descriptor", + None, + icon::backup_icon(), + Message::Settings(SettingsMessage::ExportDescriptor), + ); + + let export_transactions = settings_section( + "Export transactions", + None, + icon::backup_icon(), + Message::Settings(SettingsMessage::ExportTransactions), + ); + + let export_labels = settings_section( + "Export labels", + None, + icon::backup_icon(), + Message::Settings(SettingsMessage::ExportLabels), + ); + + let export_wallet = settings_section( + "Back Up Wallet", + None, + icon::backup_icon(), + Message::Settings(SettingsMessage::ExportWallet), + ); + + let import_wallet = settings_section( + "Restore wallet", + None, + icon::restore_icon(), + Message::Settings(SettingsMessage::ImportWallet), + ); + + dashboard( + &Menu::Settings, + cache, + warning, + Column::new() + .spacing(20) + .push(header) + .push(export_descriptor) + .push(export_transactions) + .push(export_labels) + .push(export_wallet) + .push(import_wallet) + .width(Length::Fill), + ) +} + pub fn about_section<'a>( cache: &'a Cache, warning: Option<&Error>, @@ -856,6 +918,18 @@ pub fn wallet_settings<'a>( ) -> Element<'a, Message> { let header = header("Wallet", SettingsMessage::EditWalletSettings); + let import_export = Row::new() + .push( + button::secondary(Some(icon::backup_icon()), "Backup") + .on_press(Message::Settings(SettingsMessage::ExportWallet)), + ) + .push(Space::with_width(10)) + .push( + button::secondary(Some(icon::restore_icon()), "Restore") + .on_press(Message::Settings(SettingsMessage::ImportWallet)), + ) + .push(Space::with_width(Length::Fill)); + let descr = card::simple( Column::new() .push(text("Wallet descriptor:").bold()) @@ -943,6 +1017,7 @@ pub fn wallet_settings<'a>( Column::new() .spacing(20) .push(header) + .push(import_export) .push(descr) .push( card::simple(display_policy( diff --git a/liana-gui/src/app/view/transactions.rs b/liana-gui/src/app/view/transactions.rs index 1694deb1..e9cee700 100644 --- a/liana-gui/src/app/view/transactions.rs +++ b/liana-gui/src/app/view/transactions.rs @@ -25,7 +25,7 @@ use crate::{ }, }, daemon::model::{HistoryTransaction, Txid}, - export::ExportMessage, + export::ImportExportMessage, }; pub fn transactions_view<'a>( @@ -44,7 +44,10 @@ pub fn transactions_view<'a>( Row::new() .push(Container::new(h3("Transactions"))) .push(Space::with_width(Length::Fill)) - .push(button::secondary(None, "Export").on_press(ExportMessage::Open.into())), + .push( + button::secondary(None, "Export") + .on_press(ImportExportMessage::Open.into()), + ), ) .push( Column::new() diff --git a/liana-gui/src/app/view/warning.rs b/liana-gui/src/app/view/warning.rs index 1496646a..1f371481 100644 --- a/liana-gui/src/app/view/warning.rs +++ b/liana-gui/src/app/view/warning.rs @@ -41,11 +41,16 @@ impl From<&Error> for WarningMessage { DaemonError::CoinSelectionError => { WarningMessage("Error when selecting coins for spend".to_string()) } + DaemonError::NotImplemented => { + WarningMessage("Feature not implemented for this backend".to_string()) + } }, Error::Unexpected(_) => WarningMessage("Unknown error".to_string()), Error::HardwareWallet(_) => WarningMessage("Hardware wallet error".to_string()), Error::Desc(e) => WarningMessage(format!("Descriptor analysis error: '{}'.", e)), Error::Spend(e) => WarningMessage(format!("Spend creation error: '{}'.", e)), + Error::ImportExport(e) => WarningMessage(format!("{e}")), + Error::RestoreBackup(e) => WarningMessage(format!("Fail to restore backup: {e}")), } } } diff --git a/liana-gui/src/app/wallet.rs b/liana-gui/src/app/wallet.rs index 9313e271..52a095f1 100644 --- a/liana-gui/src/app/wallet.rs +++ b/liana-gui/src/app/wallet.rs @@ -179,6 +179,28 @@ impl Wallet { Ok(self) } } + + pub fn keys(&self) -> HashMap { + let mut map = HashMap::new(); + self.keys_aliases.iter().for_each(|(fg, alias)| { + map.insert( + *fg, + settings::KeySetting { + name: alias.clone(), + master_fingerprint: *fg, + provider_key: None, + }, + ); + }); + + self.provider_keys.iter().for_each(|(fg, key)| { + if let Some(entry) = map.get_mut(fg) { + entry.provider_key = Some(key.clone()) + } + }); + + map + } } #[allow(clippy::large_enum_variant)] diff --git a/liana-gui/src/backup.rs b/liana-gui/src/backup.rs new file mode 100644 index 00000000..cab64de3 --- /dev/null +++ b/liana-gui/src/backup.rs @@ -0,0 +1,545 @@ +use chrono::{Duration, Utc}; +use liana::miniscript::{ + self, + bitcoin::{bip32::Fingerprint, Network, Txid}, +}; +use lianad::{ + bip329, + commands::{CoinStatus, ListCoinsEntry}, +}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::{ + collections::{BTreeMap, HashMap}, + fmt::{Debug, Display}, + path::PathBuf, + sync::Arc, + time::{SystemTime, UNIX_EPOCH}, +}; + +use crate::{ + app::{settings::Settings, wallet::Wallet, Config}, + daemon::{model::HistoryTransaction, Daemon, DaemonBackend, DaemonError}, + installer::{ + extract_daemon_config, extract_local_gui_settings, extract_remote_gui_settings, Context, + RemoteBackend, + }, + lianalite::client::backend::api::DEFAULT_LIMIT, + VERSION, +}; + +const CONFIG_KEY: &str = "config"; +const SETTINGS_KEY: &str = "settings"; +const LIANA_VERSION_KEY: &str = "liana_version"; + +pub fn liana_version() -> String { + format!("{}.{}.{}", VERSION.major, VERSION.minor, VERSION.patch) +} + +fn now() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("cannot fail") + .as_secs() +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct Backup { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, + pub accounts: Vec, + pub network: Network, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub date: Option, + /// App proprietary metadata (settings, configuration, etc..) + #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")] + pub proprietary: serde_json::Map, + #[serde(default = "default_version")] + pub version: u32, +} + +fn default_version() -> u32 { + 0 +} + +#[derive(Debug, Clone)] +pub enum Error { + DescriptorMissing, + NotSingleWallet, + Json, + SettingsFromFile, + Daemon(String), + TxTimeMissing, +} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::DescriptorMissing => write!(f, "Backup: descriptor missing"), + Error::NotSingleWallet => write!(f, "Backup: Zero or several wallets"), + Error::Json => write!(f, "Backup: json error"), + Error::SettingsFromFile => write!(f, "Backup: fail to parse setting from file"), + Error::Daemon(e) => write!(f, "Backup daemon error: {e}"), + Error::TxTimeMissing => write!(f, "Backup: transaction block height missing"), + } + } +} + +impl From for Error { + fn from(value: DaemonError) -> Self { + Error::Daemon(value.to_string()) + } +} + +impl Display for Backup { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = serde_json::to_string(self).map_err(|_| std::fmt::Error)?; + write!(f, "{str}") + } +} + +impl Debug for Backup { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = serde_json::to_string_pretty(self).map_err(|_| std::fmt::Error)?; + write!(f, "{str}") + } +} + +impl Backup { + /// Create a Backup from the Installer context + /// + /// # Arguments + /// * `ctx` - the installer context + /// * `timestamp` - whether to record the current timestamp as wallet creation time + /// (we should want to set timestamp = false for a wallet import for instance) + pub async fn from_installer(ctx: Context, timestamp: bool) -> Result { + let descriptor = ctx + .descriptor + .clone() + .ok_or(Error::DescriptorMissing)? + .to_string(); + + let now = now(); + + let mut account = Account::new(descriptor); + + let mut proprietary = serde_json::Map::new(); + proprietary.insert(LIANA_VERSION_KEY.to_string(), liana_version().into()); + + let config = extract_daemon_config(&ctx); + if let Ok(config) = serde_json::to_value(config) { + proprietary.insert(CONFIG_KEY.to_string(), config); + } + let settings = if ctx.bitcoin_backend.is_some() { + Some(extract_local_gui_settings(&ctx)) + } else { + match &ctx.remote_backend { + RemoteBackend::WithWallet(backend) => { + Some(extract_remote_gui_settings(&ctx, backend).await) + } + _ => None, + } + }; + + let name = if let Some(settings) = settings { + assert_eq!(settings.wallets.len(), 1); + if settings.wallets.len() != 1 { + return Err(Error::NotSingleWallet); + } + let settings = settings.wallets.first().expect("only one wallet"); + let name = settings.name.clone(); + if let Ok(settings) = serde_json::to_value(settings) { + proprietary.insert(SETTINGS_KEY.to_string(), settings); + } + Some(name) + } else { + None + }; + + ctx.keys.iter().for_each(|(k, s)| { + account.keys.insert(*k, s.to_backup()); + }); + + account.proprietary = proprietary; + account.name = name.clone(); + if timestamp { + account.timestamp = Some(now); + } + + Ok(Backup { + name, + accounts: vec![account], + network: ctx.network, + proprietary: serde_json::Map::new(), + date: Some(now), + version: 0, + }) + } + + /// Create a Backup from the Liana App context + pub async fn from_app( + datadir: PathBuf, + network: Network, + config: Arc, + wallet: Arc, + daemon: Arc, + ) -> Result { + let mut proprietary = serde_json::Map::new(); + proprietary.insert(LIANA_VERSION_KEY.to_string(), liana_version().into()); + + let name = wallet.name.clone(); + let descriptor = wallet.main_descriptor.to_string(); + let keys = wallet.keys(); + + let settings = + Settings::from_file(datadir, network).map_err(|_| Error::SettingsFromFile)?; + if settings.wallets.len() == 1 { + if let Ok(settings) = serde_json::to_value(settings.wallets[0].clone()) { + proprietary.insert(SETTINGS_KEY.to_string(), settings); + } + } + + if let Ok(config) = serde_json::to_value((*config).clone()) { + proprietary.insert(CONFIG_KEY.to_string(), config); + } + + let info = daemon.get_info().await?; + + let mut account = Account::new(descriptor); + + account.chain_tip = Some(ChainTip { + block_height: info.block_height, + block_hash: None, + }); + account.proprietary = proprietary; + account.name = Some(name.clone()); + account.timestamp = Some(info.timestamp as u64); + account.change_index = Some(info.change_index); + account.receive_index = Some(info.receive_index); + for (fg, setting) in keys { + account.keys.insert(fg, setting.to_backup()); + } + + const MAX_LABEL_BIP329: u32 = 100; + + let labels = { + let mut buff = Vec::new(); + let mut start = 0; + loop { + let mut fetched = daemon.get_labels_bip329(start, 100).await?.into_vec(); + + if fetched.len() < MAX_LABEL_BIP329 as usize { + buff.append(&mut fetched); + break; + } else { + buff.append(&mut fetched); + start += MAX_LABEL_BIP329; + } + } + bip329::Labels::new(buff) + }; + + account.labels = Some(labels); + account.transactions = get_transactions(&daemon) + .await? + .into_iter() + .map(|tx| miniscript::bitcoin::consensus::encode::serialize_hex(&tx.tx)) + .collect(); + account.psbts = daemon + .list_spend_transactions(None) + .await? + .into_iter() + .map(|tx| tx.psbt.to_string()) + .collect(); + + let statuses = [ + CoinStatus::Unconfirmed, + CoinStatus::Confirmed, + CoinStatus::Spending, + ]; + account.coins = daemon + .list_coins(&statuses, &[]) + .await? + .coins + .into_iter() + .map(|c| (c.outpoint.clone().to_string(), Coin::from(c))) + .collect(); + + Ok(Backup { + name: Some(name), + accounts: vec![account], + network, + proprietary: serde_json::Map::new(), + date: Some(now()), + version: 0, + }) + } + + fn account(&self) -> Result<&Account, Error> { + if self.accounts.len() != 1 { + Err(Error::NotSingleWallet) + } else { + Ok(self.accounts.first().expect("single account")) + } + } + + pub fn config(&self) -> Result, Error> { + let account = self.account()?; + if let Some(config) = account.proprietary.get(CONFIG_KEY) { + let config: Config = serde_json::from_value(config.clone()).map_err(|_| Error::Json)?; + Ok(Some(config)) + } else { + Ok(None) + } + } + + pub fn settings(&self) -> Result, Error> { + let account = self.account()?; + if let Some(settings) = account.proprietary.get(SETTINGS_KEY) { + let settings: Settings = + serde_json::from_value(settings.clone()).map_err(|_| Error::Json)?; + Ok(Some(settings)) + } else { + Ok(None) + } + } +} + +async fn get_transactions( + daemon: &Arc, +) -> Result, Error> { + let max = match daemon.backend() { + DaemonBackend::RemoteBackend => DEFAULT_LIMIT as u64, + _ => u32::MAX as u64, + }; + + // look 2 hour forward + // https://github.com/bitcoin/bitcoin/blob/62bd61de110b057cbfd6e31e4d0b727d93119c72/src/chain.h#L29 + let mut end = ((Utc::now() + Duration::hours(2)).timestamp()) as u32; + + // store txs in a map to avoid duplicates + let mut map = HashMap::::new(); + let mut limit = max; + + loop { + let history_txs = daemon.list_history_txs(0, end, limit).await?; + // all txs have been fetched + if history_txs.is_empty() { + return Ok(Vec::new()); + } + + if history_txs.len() == limit as usize { + let first = if let Some(t) = history_txs.first().expect("checked").time { + t + } else { + return Err(Error::TxTimeMissing); + }; + + let last = if let Some(t) = history_txs.last().expect("checked").time { + t + } else { + return Err(Error::TxTimeMissing); + }; + + // limit too low, all tx are in the same timestamp + // we must increase limit and retry + if first == last { + limit += DEFAULT_LIMIT as u64; + continue; + } else { + // add txs to map + for tx in history_txs { + let txid = tx.txid; + map.insert(txid, tx); + } + limit = max; + end = first.min(last); + continue; + } + } else + /* history_txs.len() < limit */ + { + // add txs to map + for tx in history_txs { + let txid = tx.txid; + map.insert(txid, tx); + } + break; + } + } + let vec: Vec<_> = map.into_values().collect(); + Ok(vec) +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct Account { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub name: Option, + pub descriptor: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub receive_index: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub change_index: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub timestamp: Option, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub keys: BTreeMap, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub labels: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub transactions: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub psbts: Vec, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub coins: BTreeMap, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub chain_tip: Option, + #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")] + pub proprietary: serde_json::Map, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct ChainTip { + pub block_height: i32, + pub block_hash: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct Coin { + amount: u64, + outpoint: String, + address: String, + block_height: Option, + account: u32, + derivation_index: u32, + is_coinbase: Option, + is_from_self: Option, +} + +impl From for Coin { + fn from(value: ListCoinsEntry) -> Self { + Self { + amount: value.amount.to_sat(), + outpoint: value.outpoint.to_string(), + address: value.address.to_string(), + block_height: value.block_height, + account: if value.is_change { 1 } else { 0 }, + derivation_index: value.derivation_index.into(), + is_coinbase: if value.is_immature { Some(true) } else { None }, + is_from_self: Some(value.is_from_self), + } + } +} + +impl Account { + pub fn new(descriptor: String) -> Self { + Self { + name: None, + descriptor, + receive_index: None, + change_index: None, + timestamp: None, + keys: BTreeMap::new(), + labels: None, + transactions: Vec::new(), + psbts: Vec::new(), + coins: BTreeMap::new(), + proprietary: serde_json::Map::new(), + chain_tip: None, + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +pub struct Key { + pub key: Fingerprint, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub alias: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub role: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub key_type: Option, + #[serde(default, skip_serializing_if = "Value::is_null")] + pub proprietary: Value, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] +pub enum KeyRole { + /// Key to be used in normal spending condition + Main, + /// Key that will be used for recover in case loss of main key(s) + Recovery, + /// Key that wil inherit coins if main user disapear + Inheritance, + /// Key that will cosign a spend in order to enforce some policy + Cosigning, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] +pub enum KeyType { + /// Main user + Internal, + /// Heirs or friends + External, + /// Service the user pay for + ThirdParty, +} + +#[cfg(test)] +mod test { + use super::*; + + fn round_trip(backup: &Backup) -> bool { + let serialized = serde_json::to_string(backup).unwrap(); + let parsed: Backup = serde_json::from_str(&serialized).unwrap(); + *backup == parsed + } + + #[test] + fn backup_serde() { + let mut backup = Backup { + name: None, + accounts: Vec::new(), + network: Network::Signet, + date: Some(0), + proprietary: serde_json::Map::new(), + version: 0, + }; + let serialized = serde_json::to_string(&backup).unwrap(); + let expected = r#"{"accounts":[],"network":"signet","date":0,"version":0}"#; + assert_eq!(expected, serialized); + assert!(round_trip(&backup)); + + backup.name = Some("Liana".into()); + + let serialized = serde_json::to_string(&backup).unwrap(); + let expected = r#"{"name":"Liana","accounts":[],"network":"signet","date":0,"version":0}"#; + assert_eq!(expected, serialized); + assert!(round_trip(&backup)); + + let descr_str = r#"wsh(or_d(pk([19608592/48'/1'/0'/2']tpubDEjf1AbrUjxnw8jg6Gi12CunPqnCobLP6Ktoy4Hd52pa65d6QRPg5CSkdFrqPDjJ8BAUuMEDVDRQVjtuWWksMqBeZCqyABFucN9ErQq8oVX/<0;1>/*),and_v(v:pkh([19608592/48'/1'/0'/2']tpubDEjf1AbrUjxnw8jg6Gi12CunPqnCobLP6Ktoy4Hd52pa65d6QRPg5CSkdFrqPDjJ8BAUuMEDVDRQVjtuWWksMqBeZCqyABFucN9ErQq8oVX/<2;3>/*),older(52596))))#x6u6lmej"#.to_string(); + + let account = Account::new(descr_str); + backup.accounts.push(account); + + let serialized = serde_json::to_string(&backup).unwrap(); + println!("{serialized}"); + let expected = r#"{"name":"Liana","accounts":[{"descriptor":"wsh(or_d(pk([19608592/48'/1'/0'/2']tpubDEjf1AbrUjxnw8jg6Gi12CunPqnCobLP6Ktoy4Hd52pa65d6QRPg5CSkdFrqPDjJ8BAUuMEDVDRQVjtuWWksMqBeZCqyABFucN9ErQq8oVX/<0;1>/*),and_v(v:pkh([19608592/48'/1'/0'/2']tpubDEjf1AbrUjxnw8jg6Gi12CunPqnCobLP6Ktoy4Hd52pa65d6QRPg5CSkdFrqPDjJ8BAUuMEDVDRQVjtuWWksMqBeZCqyABFucN9ErQq8oVX/<2;3>/*),older(52596))))#x6u6lmej"}],"network":"signet","date":0,"version":0}"#; + assert_eq!(expected, serialized); + assert!(round_trip(&backup)); + + // if there is no version, the default is 0 + let no_version = r#"{"name":"Liana","accounts":[{"descriptor":"wsh(or_d(pk([19608592/48'/1'/0'/2']tpubDEjf1AbrUjxnw8jg6Gi12CunPqnCobLP6Ktoy4Hd52pa65d6QRPg5CSkdFrqPDjJ8BAUuMEDVDRQVjtuWWksMqBeZCqyABFucN9ErQq8oVX/<0;1>/*),and_v(v:pkh([19608592/48'/1'/0'/2']tpubDEjf1AbrUjxnw8jg6Gi12CunPqnCobLP6Ktoy4Hd52pa65d6QRPg5CSkdFrqPDjJ8BAUuMEDVDRQVjtuWWksMqBeZCqyABFucN9ErQq8oVX/<2;3>/*),older(52596))))#x6u6lmej"}],"network":"signet","date":0}"#; + let parsed: Backup = serde_json::from_str(no_version).unwrap(); + assert_eq!(parsed.version, 0); + + // Network is mandatory for an account + let no_network = r#"{"name":"Liana","accounts":[{"descriptor":"wsh(or_d(pk([19608592/48'/1'/0'/2']tpubDEjf1AbrUjxnw8jg6Gi12CunPqnCobLP6Ktoy4Hd52pa65d6QRPg5CSkdFrqPDjJ8BAUuMEDVDRQVjtuWWksMqBeZCqyABFucN9ErQq8oVX/<0;1>/*),and_v(v:pkh([19608592/48'/1'/0'/2']tpubDEjf1AbrUjxnw8jg6Gi12CunPqnCobLP6Ktoy4Hd52pa65d6QRPg5CSkdFrqPDjJ8BAUuMEDVDRQVjtuWWksMqBeZCqyABFucN9ErQq8oVX/<2;3>/*),older(52596))))#x6u6lmej"}],"date":0,"version":0}"#; + let parsed: Result = serde_json::from_str(no_network); + assert!(parsed.is_err()); + + // But it's the only mandatory field, w/ accounts array + let minimal = r#"{"network":"signet","accounts":[]}"#; + let _parsed: Backup = serde_json::from_str(minimal).unwrap(); + } +} diff --git a/liana-gui/src/daemon/client/mod.rs b/liana-gui/src/daemon/client/mod.rs index ec6abe03..53651d36 100644 --- a/liana-gui/src/daemon/client/mod.rs +++ b/liana-gui/src/daemon/client/mod.rs @@ -4,6 +4,8 @@ use std::iter::FromIterator; use std::path::Path; use async_trait::async_trait; +use lianad::bip329::Labels; +use lianad::commands::{GetLabelsBip329Result, UpdateDerivIndexesResult}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -81,6 +83,14 @@ impl Daemon for Lianad { self.call("getnewaddress", Option::::None) } + async fn update_deriv_indexes( + &self, + receive: Option, + change: Option, + ) -> Result { + self.call("updatederivationindexes", Some(vec![receive, change])) + } + async fn list_coins( &self, statuses: &[CoinStatus], @@ -205,6 +215,12 @@ impl Daemon for Lianad { let _res: serde_json::value::Value = self.call("updatelabels", Some(vec![labels]))?; Ok(()) } + + async fn get_labels_bip329(&self, offset: u32, limit: u32) -> Result { + let res: GetLabelsBip329Result = + self.call("getlabelsbip329", Some(vec![json!(offset), json!(limit)]))?; + Ok(res.labels) + } } #[derive(Debug, Clone, Deserialize, Serialize)] diff --git a/liana-gui/src/daemon/embedded.rs b/liana-gui/src/daemon/embedded.rs index 06318b03..94935ab3 100644 --- a/liana-gui/src/daemon/embedded.rs +++ b/liana-gui/src/daemon/embedded.rs @@ -1,3 +1,5 @@ +use lianad::bip329::Labels; +use lianad::commands::UpdateDerivIndexesResult; use std::collections::{HashMap, HashSet}; use std::path::Path; use tokio::sync::Mutex; @@ -97,6 +99,19 @@ impl Daemon for EmbeddedDaemon { self.command(|daemon| Ok(daemon.get_new_address())).await } + async fn update_deriv_indexes( + &self, + receive: Option, + change: Option, + ) -> Result { + self.command(|daemon| { + daemon + .update_deriv_indexes(receive, change) + .map_err(|e| DaemonError::Unexpected(e.to_string())) + }) + .await + } + async fn list_coins( &self, statuses: &[CoinStatus], @@ -227,4 +242,9 @@ impl Daemon for EmbeddedDaemon { }) .await } + + async fn get_labels_bip329(&self, offset: u32, limit: u32) -> Result { + self.command(|daemon| Ok(daemon.get_labels_bip329(offset, limit).labels)) + .await + } } diff --git a/liana-gui/src/daemon/mod.rs b/liana-gui/src/daemon/mod.rs index 3f0211fc..5a9e9755 100644 --- a/liana-gui/src/daemon/mod.rs +++ b/liana-gui/src/daemon/mod.rs @@ -14,12 +14,15 @@ use async_trait::async_trait; use liana::miniscript::bitcoin::{ address, bip32::Fingerprint, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid, }; +use lianad::bip329::Labels; +use lianad::commands::UpdateDerivIndexesResult; use lianad::{ commands::{CoinStatus, LabelItem, TransactionInfo}, config::Config, StartupError, }; +use crate::app::settings::Settings; use crate::{hw::HardwareWalletConfig, node}; #[derive(Debug)] @@ -42,6 +45,8 @@ pub enum DaemonError { ClientNotSupported, /// Error when selecting coins for spend. CoinSelectionError, + /// Not implemented feature + NotImplemented, } impl std::fmt::Display for DaemonError { @@ -56,6 +61,7 @@ impl std::fmt::Display for DaemonError { Self::Start(e) => write!(f, "Daemon did not start: {}", e), Self::ClientNotSupported => write!(f, "Daemon communication is not supported"), Self::CoinSelectionError => write!(f, "Coin selection error"), + Self::NotImplemented => write!(f, "This feature is not implemented for this backend"), } } } @@ -81,6 +87,11 @@ pub trait Daemon: Debug { async fn stop(&self) -> Result<(), DaemonError>; async fn get_info(&self) -> Result; async fn get_new_address(&self) -> Result; + async fn update_deriv_indexes( + &self, + receive: Option, + change: Option, + ) -> Result; async fn list_coins( &self, statuses: &[CoinStatus], @@ -125,6 +136,7 @@ pub trait Daemon: Debug { &self, labels: &HashMap>, ) -> Result<(), DaemonError>; + async fn get_labels_bip329(&self, offset: u32, limit: u32) -> Result; async fn send_wallet_invitation(&self, _email: &str) -> Result<(), DaemonError> { Ok(()) } @@ -354,12 +366,33 @@ pub trait Daemon: Debug { Ok(events) } - /// Implemented by LianaLite backend + /// Reimplemented by LianaLite backend async fn update_wallet_metadata( &self, - _fingerprint_aliases: &HashMap, + fingerprint_aliases: &HashMap, _hws: &[HardwareWalletConfig], ) -> Result<(), DaemonError> { + if let Some(datadir) = self + .config() + .ok_or(DaemonError::Unexpected("Config missing".into()))? + .data_dir() + { + let network = self.get_info().await?.network; + let mut settings = Settings::from_file(datadir, network) + .map_err(|_| DaemonError::Unexpected("Fail to read Settings from file".into()))?; + let wallet = if settings.wallets.len() == 1 { + settings.wallets.get_mut(0).expect("already checked") + } else { + return Err(DaemonError::Unexpected( + "Settings file contains more than one wallet".into(), + )); + }; + for fg in wallet.keys_aliases().keys() { + if fingerprint_aliases.contains_key(fg) { + wallet.update_alias(fg, fingerprint_aliases.get(fg).expect("checked")); + } + } + } Ok(()) } } diff --git a/liana-gui/src/export.rs b/liana-gui/src/export.rs index ef6090ce..fa8bc9aa 100644 --- a/liana-gui/src/export.rs +++ b/liana-gui/src/export.rs @@ -1,17 +1,27 @@ use std::{ collections::HashMap, + fmt::Display, fs::{self, File}, - io::Write, + io::{Read, Write}, path::PathBuf, + str::FromStr, sync::{ - mpsc::{channel, Receiver, Sender}, + mpsc::{channel, sync_channel, Receiver, Sender, SyncSender}, Arc, Mutex, }, - time::{self}, + time, }; +use async_hwi::bitbox::api::btc::Fingerprint; use chrono::{DateTime, Duration, Utc}; -use liana::miniscript::bitcoin::{Amount, Txid}; +use liana::{ + descriptors::LianaDescriptor, + miniscript::bitcoin::{Amount, Network, Psbt, Txid}, +}; +use lianad::{ + bip329::{error::ExportError, Labels}, + commands::LabelItem, +}; use tokio::{ task::{JoinError, JoinHandle}, time::sleep, @@ -20,58 +30,108 @@ use tokio::{ use iced::futures::{SinkExt, Stream}; use crate::{ - app::view, + app::{ + cache::Cache, + settings::{self, KeySetting, Settings}, + view, + wallet::Wallet, + Config, + }, + backup::{self, Backup}, daemon::{ model::{HistoryTransaction, Labelled}, Daemon, DaemonBackend, DaemonError, }, lianalite::client::backend::api::DEFAULT_LIMIT, + node::bitcoind::Bitcoind, }; +const DUMP_LABELS_LIMIT: u32 = 100; + macro_rules! send_error { ($sender:ident, $error:ident) => { - if let Err(e) = $sender.send(ExportProgress::Error(Error::$error)) { - tracing::error!("ExportState::start() fail to send msg: {}", e); + if let Err(e) = $sender.send(Progress::Error(Error::$error)) { + tracing::error!("Import/Export fail to send msg: {}", e); } }; ($sender:ident, $error:expr) => { - if let Err(e) = $sender.send(ExportProgress::Error($error)) { - tracing::error!("ExportState::start() fail to send msg: {}", e); + if let Err(e) = $sender.send(Progress::Error($error)) { + tracing::error!("Import/Export fail to send msg: {}", e); } }; } macro_rules! send_progress { ($sender:ident, $progress:ident) => { - if let Err(e) = $sender.send(ExportProgress::$progress) { - tracing::error!("ExportState::start() fail to send msg: {}", e); + if let Err(e) = $sender.send(Progress::$progress) { + tracing::error!("ImportExport fail to send msg: {}", e); } }; ($sender:ident, $progress:ident($val:expr)) => { - if let Err(e) = $sender.send(ExportProgress::$progress($val)) { - tracing::error!("ExportState::start() fail to send msg: {}", e); + if let Err(e) = $sender.send(Progress::$progress($val)) { + tracing::error!("ImportExport fail to send msg: {}", e); } }; } +macro_rules! open_file_write { + ($path:ident, $sender:ident) => {{ + let dir = match $path.parent() { + Some(dir) => dir, + None => { + send_error!($sender, NoParentDir); + return; + } + }; + if !dir.exists() { + if let Err(e) = fs::create_dir_all(dir) { + send_error!($sender, e.into()); + return; + } + } + match File::create($path.as_path()) { + Ok(f) => f, + Err(e) => { + send_error!($sender, e.into()); + return; + } + } + }}; +} + +macro_rules! open_file_read { + ($path:ident, $sender:ident) => {{ + match File::open($path.as_path()) { + Ok(f) => f, + Err(e) => { + send_error!($sender, e.into()); + return; + } + } + }}; +} + #[derive(Debug, Clone)] -pub enum ExportMessage { +pub enum ImportExportMessage { Open, - ExportProgress(ExportProgress), + Progress(Progress), TimedOut, UserStop, Path(Option), Close, + Overwrite, + Ignore, + UpdateAliases(HashMap), } -impl From for view::Message { - fn from(value: ExportMessage) -> Self { - Self::Export(value) +impl From for view::Message { + fn from(value: ImportExportMessage) -> Self { + Self::ImportExport(value) } } #[derive(Debug, PartialEq)] -pub enum ExportState { +pub enum ImportExportState { Init, ChoosePath, Path(PathBuf), @@ -93,6 +153,79 @@ pub enum Error { NoParentDir, Daemon(String), TxTimeMissing, + DaemonMissing, + ParsePsbt, + ParseDescriptor, + Bip329Export(String), + BackupImport(String), + Backup(backup::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io(e) => write!(f, "ImportExport Io Error: {e}"), + Error::HandleLost => write!(f, "ImportExport: subprocess handle lost"), + Error::UnexpectedEnd => write!(f, "ImportExport: unexpected end of the process"), + Error::JoinError(e) => write!(f, "ImportExport fail to handle.join(): {e} "), + Error::ChannelLost => write!(f, "ImportExport: the channel have been closed"), + Error::NoParentDir => write!(f, "ImportExport: there is no parent dir"), + Error::Daemon(e) => write!(f, "ImportExport daemon error: {e}"), + Error::TxTimeMissing => write!(f, "ImportExport: transaction block height missing"), + Error::DaemonMissing => write!(f, "ImportExport: the daemon is missing"), + Error::ParsePsbt => write!(f, "ImportExport: fail to parse PSBT"), + Error::ParseDescriptor => write!(f, "ImportExport: fail to parse descriptor"), + Error::Bip329Export(e) => write!(f, "Bip329Export: {e}"), + Error::BackupImport(e) => write!(f, "BackupImport: {e}"), + Error::Backup(e) => write!(f, "Backup: {e}"), + } + } +} + +#[derive(Debug, Clone)] +pub enum ImportExportType { + Transactions, + ExportPsbt(String), + ExportBackup(String), + ImportBackup( + Option>, /*overwrite_labels*/ + Option>, /*overwrite_aliases*/ + ), + WalletFromBackup, + Descriptor(LianaDescriptor), + ExportLabels, + ImportPsbt, + ImportDescriptor, +} + +impl ImportExportType { + pub fn end_message(&self) -> &str { + match self { + ImportExportType::Transactions + | ImportExportType::ExportPsbt(_) + | ImportExportType::ExportBackup(_) + | ImportExportType::Descriptor(_) + | ImportExportType::ExportLabels => "Export successful!", + ImportExportType::ImportBackup(_, _) + | ImportExportType::ImportPsbt + | ImportExportType::WalletFromBackup + | ImportExportType::ImportDescriptor => "Import successful", + } + } +} + +impl PartialEq for ImportExportType { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::ExportPsbt(l0), Self::ExportPsbt(r0)) => l0 == r0, + (Self::ExportBackup(l0), Self::ExportBackup(r0)) => l0 == r0, + (Self::ImportBackup(l0, l1), Self::ImportBackup(r0, r1)) => { + l0.is_some() == r0.is_some() && l1.is_some() == r1.is_some() + } + (Self::Descriptor(l0), Self::Descriptor(r0)) => l0 == r0, + _ => core::mem::discriminant(self) == core::mem::discriminant(other), + } + } } impl From for Error { @@ -113,6 +246,12 @@ impl From for Error { } } +impl From for Error { + fn from(value: ExportError) -> Self { + Error::Bip329Export(format!("{:?}", value)) + } +} + #[derive(Debug)] pub enum Status { Init, @@ -121,212 +260,86 @@ pub enum Status { } #[derive(Debug, Clone)] -pub enum ExportProgress { +pub enum Progress { Started(Arc>>), Progress(f32), Ended, Finished, Error(Error), None, + Psbt(Psbt), + Descriptor(LianaDescriptor), + LabelsConflict(SyncSender), + KeyAliasesConflict(SyncSender), + UpdateAliases(HashMap), + WalletFromBackup( + ( + LianaDescriptor, + Network, + HashMap, + Backup, + ), + ), } -pub struct State { - pub receiver: Receiver, - pub sender: Option>, +pub struct Export { + pub receiver: Receiver, + pub sender: Option>, pub handle: Option>>>, - pub daemon: Arc, + pub daemon: Option>, pub path: Box, + pub export_type: ImportExportType, } -impl State { - pub fn new(daemon: Arc, path: Box) -> Self { +impl Export { + pub fn new( + daemon: Option>, + path: Box, + export_type: ImportExportType, + ) -> Self { let (sender, receiver) = channel(); - State { + Export { receiver, sender: Some(sender), handle: None, daemon, path, + export_type, } } + pub async fn export_logic( + export_type: ImportExportType, + sender: Sender, + daemon: Option>, + path: PathBuf, + ) { + match export_type { + ImportExportType::Transactions => export_transactions(sender, daemon, path).await, + ImportExportType::ExportPsbt(str) => export_string(sender, path, str), + ImportExportType::Descriptor(descriptor) => export_descriptor(sender, path, descriptor), + ImportExportType::ExportLabels => export_labels(sender, daemon, path).await, + ImportExportType::ImportPsbt => import_psbt(sender, path), + ImportExportType::ImportDescriptor => import_descriptor(sender, path), + ImportExportType::ExportBackup(str) => export_string(sender, path, str), + ImportExportType::ImportBackup(..) => import_backup(sender, path, daemon).await, + ImportExportType::WalletFromBackup => wallet_from_backup(sender, path).await, + }; + } + pub async fn start(&mut self) { if let (true, Some(sender)) = (self.handle.is_none(), self.sender.take()) { let daemon = self.daemon.clone(); let path = self.path.clone(); let cloned_sender = sender.clone(); + let export_type = self.export_type.clone(); let handle = tokio::spawn(async move { - let dir = match path.parent() { - Some(dir) => dir, - None => { - send_error!(sender, NoParentDir); - return; - } - }; - if !dir.exists() { - if let Err(e) = fs::create_dir_all(dir) { - send_error!(sender, e.into()); - return; - } - } - let mut file = match File::create(path.as_path()) { - Ok(f) => f, - Err(e) => { - send_error!(sender, e.into()); - return; - } - }; - - let header = "Date,Label,Value,Fee,Txid,Block\n".to_string(); - if let Err(e) = file.write_all(header.as_bytes()) { - send_error!(sender, e.into()); - return; - } - - // look 2 hour forward - // https://github.com/bitcoin/bitcoin/blob/62bd61de110b057cbfd6e31e4d0b727d93119c72/src/chain.h#L29 - let mut end = ((Utc::now() + Duration::hours(2)).timestamp()) as u32; - let total_txs = daemon.list_confirmed_txs(0, end, u32::MAX as u64).await; - let total_txs = match total_txs { - Ok(r) => r.transactions.len(), - Err(e) => { - send_error!(sender, e.into()); - return; - } - }; - - if total_txs == 0 { - send_progress!(sender, Ended); - } else { - send_progress!(sender, Progress(5.0)); - } - - let max = match daemon.backend() { - DaemonBackend::RemoteBackend => DEFAULT_LIMIT as u64, - _ => u32::MAX as u64, - }; - - // store txs in a map to avoid duplicates - let mut map = HashMap::::new(); - let mut limit = max; - - loop { - let history = daemon.list_history_txs(0, end, limit).await; - let history_txs = match history { - Ok(h) => h, - Err(e) => { - send_error!(sender, e.into()); - return; - } - }; - let dl = map.len() + history_txs.len(); - if dl > 0 { - let progress = (dl as f32) / (total_txs as f32) * 80.0; - send_progress!(sender, Progress(progress)); - } - // all txs have been fetched - if history_txs.is_empty() { - break; - } - if history_txs.len() == limit as usize { - let first = if let Some(t) = history_txs.first().expect("checked").time { - t - } else { - send_error!(sender, TxTimeMissing); - return; - }; - let last = if let Some(t) = history_txs.last().expect("checked").time { - t - } else { - send_error!(sender, TxTimeMissing); - return; - }; - // limit too low, all tx are in the same timestamp - // we must increase limit and retry - if first == last { - limit += DEFAULT_LIMIT as u64; - continue; - } else { - // add txs to map - for tx in history_txs { - let txid = tx.txid; - map.insert(txid, tx); - } - limit = max; - end = first.min(last); - continue; - } - } else - /* history_txs.len() < limit */ - { - // add txs to map - for tx in history_txs { - let txid = tx.txid; - map.insert(txid, tx); - } - break; - } - } - - let mut txs: Vec<_> = map.into_values().collect(); - txs.sort_by(|a, b| b.compare(a)); - - for mut tx in txs { - let date_time = tx - .time - .map(|t| { - let mut str = DateTime::from_timestamp(t as i64, 0) - .expect("bitcoin timestamp") - .to_rfc3339(); - //str has the form `1996-12-19T16:39:57-08:00` - // ^ ^^^^^^ - // replace `T` by ` `| | drop this part - str = str.replace("T", " "); - str[0..(str.len() - 6)].to_string() - }) - .unwrap_or("".to_string()); - - let txid = tx.txid.clone().to_string(); - let txid_label = tx.labels().get(&txid).cloned(); - let mut label = if let Some(txid) = txid_label { - txid - } else { - "".to_string() - }; - if !label.is_empty() { - label = format!("\"{}\"", label); - } - let txid = tx.txid.to_string(); - let fee = tx.fee_amount.unwrap_or(Amount::ZERO).to_sat() as i128; - let mut inputs_amount = 0; - tx.coins.iter().for_each(|(_, coin)| { - inputs_amount += coin.amount.to_sat() as i128; - }); - let value = tx.incoming_amount.to_sat() as i128 - inputs_amount; - let value = value as f64 / 100_000_000.0; - let fee = fee as f64 / 100_000_000.0; - let block = tx.height.map(|h| h.to_string()).unwrap_or("".to_string()); - let fee = if fee != 0.0 { - fee.to_string() - } else { - "".into() - }; - - let line = format!( - "{},{},{},{},{},{}\n", - date_time, label, value, fee, txid, block - ); - if let Err(e) = file.write_all(line.as_bytes()) { - send_error!(sender, e.into()); - return; - } - } - send_progress!(sender, Progress(100.0)); - send_progress!(sender, Ended); + Self::export_logic(export_type, cloned_sender, daemon, *path).await; }); let handle = Arc::new(Mutex::new(handle)); + let cloned_sender = sender.clone(); // we send the handle to the GUI so we can kill the thread on timeout // or user cancel action send_progress!(cloned_sender, Started(handle.clone())); @@ -346,11 +359,12 @@ impl State { } pub fn export_subscription( - daemon: Arc, + daemon: Option>, path: PathBuf, -) -> impl Stream { + export_type: ImportExportType, +) -> impl Stream { iced::stream::channel(100, move |mut output| async move { - let mut state = State::new(daemon, Box::new(path)); + let mut state = Export::new(daemon, Box::new(path), export_type); loop { match state.state() { Status::Init => { @@ -378,7 +392,7 @@ pub fn export_subscription( let handle = match state.handle.take() { Some(h) => h, None => { - if let Err(e) = output.send(ExportProgress::Error(Error::HandleLost)).await { + if let Err(e) = output.send(Progress::Error(Error::HandleLost)).await { tracing::error!("export_subscription() fail to send message: {}", e); } continue; @@ -387,9 +401,9 @@ pub fn export_subscription( let msg = { let h = handle.lock().expect("should not fail"); if h.is_finished() { - Some(ExportProgress::Finished) + Some(Progress::Finished) } else if disconnected { - Some(ExportProgress::Error(Error::ChannelLost)) + Some(Progress::Error(Error::ChannelLost)) } else { None } @@ -407,13 +421,869 @@ pub fn export_subscription( }) } -pub async fn get_path() -> Option { - let date = chrono::Local::now().format("%Y-%m-%dT%H-%M-%S"); - let file_name = format!("liana-txs-{date}.csv"); - rfd::AsyncFileDialog::new() - .set_title("Choose a location to export...") - .set_file_name(file_name) - .save_file() - .await - .map(|fh| fh.path().to_path_buf()) +pub async fn export_transactions( + sender: Sender, + daemon: Option>, + path: PathBuf, +) { + async move { + let daemon = match daemon { + Some(d) => d, + None => { + send_error!(sender, Error::DaemonMissing); + return; + } + }; + let mut file = open_file_write!(path, sender); + + let header = "Date,Label,Value,Fee,Txid,Block\n".to_string(); + if let Err(e) = file.write_all(header.as_bytes()) { + send_error!(sender, e.into()); + return; + } + + // look 2 hour forward + // https://github.com/bitcoin/bitcoin/blob/62bd61de110b057cbfd6e31e4d0b727d93119c72/src/chain.h#L29 + let mut end = ((Utc::now() + Duration::hours(2)).timestamp()) as u32; + let total_txs = daemon.list_confirmed_txs(0, end, u32::MAX as u64).await; + let total_txs = match total_txs { + Ok(r) => r.transactions.len(), + Err(e) => { + send_error!(sender, e.into()); + return; + } + }; + + if total_txs == 0 { + send_progress!(sender, Ended); + } else { + send_progress!(sender, Progress(5.0)); + } + + let max = match daemon.backend() { + DaemonBackend::RemoteBackend => DEFAULT_LIMIT as u64, + _ => u32::MAX as u64, + }; + + // store txs in a map to avoid duplicates + let mut map = HashMap::::new(); + let mut limit = max; + + loop { + let history = daemon.list_history_txs(0, end, limit).await; + let history_txs = match history { + Ok(h) => h, + Err(e) => { + send_error!(sender, e.into()); + return; + } + }; + let dl = map.len() + history_txs.len(); + if dl > 0 { + let progress = (dl as f32) / (total_txs as f32) * 80.0; + send_progress!(sender, Progress(progress)); + } + // all txs have been fetched + if history_txs.is_empty() { + break; + } + if history_txs.len() == limit as usize { + let first = if let Some(t) = history_txs.first().expect("checked").time { + t + } else { + send_error!(sender, TxTimeMissing); + return; + }; + let last = if let Some(t) = history_txs.last().expect("checked").time { + t + } else { + send_error!(sender, TxTimeMissing); + return; + }; + // limit too low, all tx are in the same timestamp + // we must increase limit and retry + if first == last { + limit += DEFAULT_LIMIT as u64; + continue; + } else { + // add txs to map + for tx in history_txs { + let txid = tx.txid; + map.insert(txid, tx); + } + limit = max; + end = first.min(last); + continue; + } + } else + /* history_txs.len() < limit */ + { + // add txs to map + for tx in history_txs { + let txid = tx.txid; + map.insert(txid, tx); + } + break; + } + } + + let mut txs: Vec<_> = map.into_values().collect(); + txs.sort_by(|a, b| b.compare(a)); + + for mut tx in txs { + let date_time = tx + .time + .map(|t| { + let mut str = DateTime::from_timestamp(t as i64, 0) + .expect("bitcoin timestamp") + .to_rfc3339(); + //str has the form `1996-12-19T16:39:57-08:00` + // ^ ^^^^^^ + // replace `T` by ` `| | drop this part + str = str.replace("T", " "); + str[0..(str.len() - 6)].to_string() + }) + .unwrap_or("".to_string()); + + let txid = tx.txid.clone().to_string(); + let txid_label = tx.labels().get(&txid).cloned(); + let mut label = if let Some(txid) = txid_label { + txid + } else { + "".to_string() + }; + if !label.is_empty() { + label = format!("\"{}\"", label); + } + let txid = tx.txid.to_string(); + let fee = tx.fee_amount.unwrap_or(Amount::ZERO).to_sat() as i128; + let mut inputs_amount = 0; + tx.coins.iter().for_each(|(_, coin)| { + inputs_amount += coin.amount.to_sat() as i128; + }); + let value = tx.incoming_amount.to_sat() as i128 - inputs_amount; + let value = value as f64 / 100_000_000.0; + let fee = fee as f64 / 100_000_000.0; + let block = tx.height.map(|h| h.to_string()).unwrap_or("".to_string()); + let fee = if fee != 0.0 { + fee.to_string() + } else { + "".into() + }; + + let line = format!( + "{},{},{},{},{},{}\n", + date_time, label, value, fee, txid, block + ); + if let Err(e) = file.write_all(line.as_bytes()) { + send_error!(sender, e.into()); + return; + } + } + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Ended); + } + .await; +} + +pub fn export_descriptor(sender: Sender, path: PathBuf, descriptor: LianaDescriptor) { + let mut file = open_file_write!(path, sender); + + let descr_string = descriptor.to_string(); + if let Err(e) = file.write_all(descr_string.as_bytes()) { + send_error!(sender, e.into()); + return; + } + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Ended); +} + +pub fn export_string(sender: Sender, path: PathBuf, psbt: String) { + let mut file = open_file_write!(path, sender); + + if let Err(e) = file.write_all(psbt.as_bytes()) { + send_error!(sender, e.into()); + return; + } + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Ended); +} + +pub fn import_psbt(sender: Sender, path: PathBuf) { + let mut file = open_file_read!(path, sender); + + let mut psbt_str = String::new(); + if let Err(e) = file.read_to_string(&mut psbt_str) { + send_error!(sender, e.into()); + return; + } + + let psbt = match Psbt::from_str(&psbt_str) { + Ok(psbt) => psbt, + Err(_) => { + send_error!(sender, Error::ParsePsbt); + return; + } + }; + + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Psbt(psbt)); +} + +pub fn import_descriptor(sender: Sender, path: PathBuf) { + let mut file = open_file_read!(path, sender); + + let mut descr_str = String::new(); + if let Err(e) = file.read_to_string(&mut descr_str) { + send_error!(sender, e.into()); + return; + } + + let descriptor = match LianaDescriptor::from_str(&descr_str) { + Ok(psbt) => psbt, + Err(_) => { + send_error!(sender, Error::ParseDescriptor); + return; + } + }; + + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Descriptor(descriptor)); +} + +/// Import a backup in an already existing wallet: +/// - Load backup from file +/// - check if networks matches +/// - check if descriptors matches +/// - check if labels can be imported w/o conflict, if conflic ask user to ACK +/// - check if aliases can be imported w/o conflict, if conflict ask user to ACK +/// - update receive and change indexes +/// - parse psbt from backup +/// - import PSBTs +/// - import labels if no conflict or user ACK +/// - update aliases if no conflict or user ACK +pub async fn import_backup( + sender: Sender, + path: PathBuf, + daemon: Option>, +) { + let daemon = match daemon { + Some(d) => d, + None => { + send_error!(sender, Error::DaemonMissing); + return; + } + }; + + // TODO: drop after support for restore to liana-connect + if matches!(daemon.backend(), DaemonBackend::RemoteBackend) { + send_error!( + sender, + Error::BackupImport("Restore to a Liana-connect backend is not yet supported!".into()) + ); + return; + } + + // Load backup from file + let mut file = open_file_read!(path, sender); + + let mut backup_str = String::new(); + if let Err(e) = file.read_to_string(&mut backup_str) { + send_error!(sender, e.into()); + return; + } + + let backup: Result = serde_json::from_str(&backup_str); + let backup = match backup { + Ok(psbt) => psbt, + Err(e) => { + send_error!(sender, Error::BackupImport(format!("{:?}", e))); + return; + } + }; + + // get backend info + let info = match daemon.get_info().await { + Ok(info) => info, + Err(e) => { + send_error!(sender, Error::Daemon(format!("{e:?}"))); + return; + } + }; + + // check if networks matches + let network = info.network; + if backup.network != network { + send_error!( + sender, + Error::BackupImport("The network of the backup don't match the wallet network!".into()) + ); + return; + } + + // check if descriptors matches + let descriptor = info.descriptors.main; + let account = match backup.accounts.len() { + 0 => { + send_error!( + sender, + Error::BackupImport("There is no account in the backup!".into()) + ); + return; + } + 1 => backup.accounts.first().expect("already checked"), + _ => { + send_error!( + sender, + Error::BackupImport( + "Liana is actually not supporting import of backup with several accounts!" + .into() + ) + ); + return; + } + }; + + let backup_descriptor = match LianaDescriptor::from_str(&account.descriptor) { + Ok(d) => d, + Err(_) => { + send_error!( + sender, + Error::BackupImport( + "The backup descriptor is not a valid Liana descriptor!".into() + ) + ); + return; + } + }; + + if backup_descriptor != descriptor { + send_error!( + sender, + Error::BackupImport("The backup descriptor do not match this wallet!".into()) + ); + return; + } + + // TODO: check if timestamp matches? + + // check if labels can be imported w/o conflict + let mut write_labels = true; + let backup_labels = if let Some(labels) = account.labels.clone() { + let db_labels = match daemon.get_labels_bip329(0, u32::MAX).await { + Ok(l) => l, + Err(_) => { + send_error!(sender, Error::BackupImport("Fail to dump DB labels".into())); + return; + } + }; + + let labels_map = db_labels.clone().into_map(); + let backup_labels_map = labels.clone().into_map(); + + // if there is a conflict, we ask user to ACK before overwrite + let (ack_sender, ack_receiver) = sync_channel(0); + let mut conflict = false; + for (k, l) in &backup_labels_map { + if let Some(lab) = labels_map.get(k) { + if lab != l { + send_progress!(sender, LabelsConflict(ack_sender)); + conflict = true; + break; + } + } + } + if conflict { + write_labels = match ack_receiver.recv() { + Ok(b) => b, + Err(_) => { + send_error!( + sender, + Error::BackupImport("Fail to receive labels ACK".into()) + ); + return; + } + } + } + + labels.into_vec() + } else { + Vec::new() + }; + + let datadir = match daemon.config() { + Some(c) => match &c.data_dir { + Some(dd) => dd, + None => { + send_error!( + sender, + Error::BackupImport("Fail to get Daemon config".into()) + ); + return; + } + }, + None => { + send_error!( + sender, + Error::BackupImport("Fail to get Daemon config".into()) + ); + return; + } + }; + + // check if key aliases can be imported w/o conflict + let mut write_aliases = true; + let settings = if !account.keys.is_empty() { + let settings = match Settings::from_file(datadir.to_path_buf(), network) { + Ok(s) => s, + Err(_) => { + send_error!( + sender, + Error::BackupImport("Fail to get App Settings".into()) + ); + return; + } + }; + + let settings_aliases: HashMap<_, _> = match settings.wallets.len() { + 1 => settings + .wallets + .first() + .expect("already checked") + .keys + .clone() + .into_iter() + .map(|s| (s.master_fingerprint, s)) + .collect(), + _ => { + send_error!( + sender, + Error::BackupImport("Settings.wallets.len() is not 1".into()) + ); + return; + } + }; + + let (ack_sender, ack_receiver) = sync_channel(0); + let mut conflict = false; + for (fg, key) in &account.keys { + if let Some(k) = settings_aliases.get(fg) { + let ks = k.to_backup(); + if ks != *key { + send_progress!(sender, KeyAliasesConflict(ack_sender)); + conflict = true; + break; + } + } + } + if conflict { + // wait for the user ACK/NACK + write_aliases = match ack_receiver.recv() { + Ok(a) => a, + Err(_) => { + send_error!( + sender, + Error::BackupImport("Fail to receive aliases ACK".into()) + ); + return; + } + }; + } + + Some((settings, settings_aliases)) + } else { + None + }; + + // update receive & change index + let db_receive = info.receive_index; + let i = account.receive_index.unwrap_or(0); + let receive = if db_receive < i { Some(i) } else { None }; + + let db_change = info.change_index; + let i = account.change_index.unwrap_or(0); + let change = if db_change < i { Some(i) } else { None }; + + if daemon.update_deriv_indexes(receive, change).await.is_err() { + send_error!( + sender, + Error::BackupImport("Fail to update derivation indexes".into()) + ); + return; + } + + // parse PSBTs + let mut psbts = Vec::new(); + for psbt_str in &account.psbts { + match Psbt::from_str(psbt_str) { + Ok(p) => { + psbts.push(p); + } + Err(_) => { + send_error!(sender, Error::BackupImport("Fail to parse PSBT".into())); + return; + } + } + } + + // import PSBTs + for psbt in psbts { + if daemon.update_spend_tx(&psbt).await.is_err() { + send_error!(sender, Error::BackupImport("Fail to store PSBT".into())); + return; + } + } + + // import labels if no conflict or user ACK + if write_labels { + let labels: HashMap> = backup_labels + .into_iter() + .filter_map(|l| { + if let Some((item, label)) = LabelItem::from_bip329(&l, network) { + Some((item, Some(label))) + } else { + None + } + }) + .collect(); + if daemon.update_labels(&labels).await.is_err() { + send_error!(sender, Error::BackupImport("Fail to import labels".into())); + return; + } + } + + // update aliases if no conflict or user ACK + if let (true, Some((mut settings, mut settings_aliases))) = (write_aliases, settings) { + for (k, v) in &account.keys { + if let Some(ks) = KeySetting::from_backup( + v.alias.clone().unwrap_or("".into()), + *k, + v.role, + v.key_type, + v.proprietary.clone(), + ) { + settings_aliases.insert(*k, ks); + } + } + + settings.wallets.get_mut(0).expect("already checked").keys = + settings_aliases.clone().into_values().collect(); + if settings.to_file(datadir.to_path_buf(), network).is_err() { + send_error!( + sender, + Error::BackupImport("Fail to import keys aliases".into()) + ); + return; + } else { + // Update wallet state + send_progress!(sender, UpdateAliases(settings_aliases)); + } + } + + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Ended); +} + +#[derive(Debug)] +pub enum RestoreBackupError { + Daemon(DaemonError), + Network, + InvalidDescriptor, + WrongDescriptor, + NoAccount, + SeveralAccounts, + LianaConnectNotSupported, + GetLabels, + LabelsNotEmpty, + InvalidPsbt, +} + +impl Display for RestoreBackupError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RestoreBackupError::Daemon(e) => write!(f, "Daemon error during restore process: {e}"), + RestoreBackupError::Network => write!(f, "Backup & wallet network don't matches"), + RestoreBackupError::InvalidDescriptor => write!(f, "The backup descriptor is invalid"), + RestoreBackupError::WrongDescriptor => { + write!(f, "Backup & wallet descriptor don't matches") + } + RestoreBackupError::NoAccount => write!(f, "There is no account in the backup"), + RestoreBackupError::SeveralAccounts => { + write!(f, "There is several accounts in the backup") + } + RestoreBackupError::LianaConnectNotSupported => { + write!(f, "Restore a backup to Liana-connect is not yet supported") + } + RestoreBackupError::GetLabels => write!(f, "Fails to get labels during backup restore"), + RestoreBackupError::LabelsNotEmpty => write!( + f, + "Cannot load labels: there is already labels into the database" + ), + RestoreBackupError::InvalidPsbt => write!(f, "Psbt is invalid"), + } + } +} + +impl From for RestoreBackupError { + fn from(value: DaemonError) -> Self { + Self::Daemon(value) + } +} + +/// Create a wallet from a backup +/// - load backup from file +/// - extract descriptor +/// - extract network +/// - extract aliases +pub async fn wallet_from_backup(sender: Sender, path: PathBuf) { + // Load backup from file + let mut file = open_file_read!(path, sender); + + let mut backup_str = String::new(); + if let Err(e) = file.read_to_string(&mut backup_str) { + send_error!(sender, e.into()); + return; + } + + let backup: Result = serde_json::from_str(&backup_str); + let backup = match backup { + Ok(psbt) => psbt, + Err(e) => { + send_error!(sender, Error::BackupImport(format!("{:?}", e))); + return; + } + }; + + let network = backup.network; + + let account = match backup.accounts.len() { + 0 => { + send_error!( + sender, + Error::BackupImport("There is no account in the backup!".into()) + ); + return; + } + 1 => backup.accounts.first().expect("already checked"), + _ => { + send_error!( + sender, + Error::BackupImport( + "Liana is actually not supporting import of backup with several accounts!" + .into() + ) + ); + return; + } + }; + + let descriptor = match LianaDescriptor::from_str(&account.descriptor) { + Ok(d) => d, + Err(_) => { + send_error!( + sender, + Error::BackupImport( + "The backup descriptor is not a valid Liana descriptor!".into() + ) + ); + return; + } + }; + + let mut aliases: HashMap = HashMap::new(); + for (k, v) in &account.keys { + if let Some(ks) = KeySetting::from_backup( + v.alias.clone().unwrap_or("".into()), + *k, + v.role, + v.key_type, + v.proprietary.clone(), + ) { + aliases.insert(*k, ks); + } + } + + send_progress!( + sender, + WalletFromBackup((descriptor, network, aliases, backup)) + ); + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Ended); +} + +#[allow(unused)] +/// Import backup data if wallet created from a backup +/// - check if networks matches +/// - check if descriptors matches +/// - check if labels are empty +/// - update receive and change indexes +/// - parse psbt from backup +/// - import PSBTs +/// - import labels +pub async fn import_backup_at_launch( + cache: Cache, + wallet: Arc, + config: Config, + daemon: Arc, + datadir: PathBuf, + internal_bitcoind: Option, + backup: Backup, +) -> Result< + ( + Cache, + Arc, + Config, + Arc, + PathBuf, + Option, + ), + RestoreBackupError, +> { + // TODO: drop after support for restore to liana-connect + if matches!(daemon.backend(), DaemonBackend::RemoteBackend) { + return Err(RestoreBackupError::LianaConnectNotSupported); + } + + // get backend info + let info = daemon.get_info().await?; + + // check if networks matches + let network = info.network; + if backup.network != network { + return Err(RestoreBackupError::Network); + } + + // check if descriptors matches + let descriptor = info.descriptors.main; + let account = match backup.accounts.len() { + 0 => return Err(RestoreBackupError::NoAccount), + 1 => backup.accounts.first().expect("already checked"), + _ => return Err(RestoreBackupError::SeveralAccounts), + }; + + let backup_descriptor = LianaDescriptor::from_str(&account.descriptor) + .map_err(|_| RestoreBackupError::InvalidDescriptor)?; + + if backup_descriptor != descriptor { + return Err(RestoreBackupError::WrongDescriptor); + } + + // check there is no labels in DB + if account.labels.is_some() + && !daemon + .get_labels_bip329(0, u32::MAX) + .await + .map_err(|_| RestoreBackupError::GetLabels)? + .to_vec() + .is_empty() + { + return Err(RestoreBackupError::LabelsNotEmpty); + } + + // parse PSBTs + let mut psbts = Vec::new(); + for psbt_str in &account.psbts { + psbts.push(Psbt::from_str(psbt_str).map_err(|_| RestoreBackupError::InvalidPsbt)?); + } + + // update receive & change index + let db_receive = info.receive_index; + let i = account.receive_index.unwrap_or(0); + let receive = if db_receive < i { Some(i) } else { None }; + + let db_change = info.change_index; + let i = account.change_index.unwrap_or(0); + let change = if db_change < i { Some(i) } else { None }; + + daemon.update_deriv_indexes(receive, change).await?; + + // import labels + if let Some(labels) = account.labels.clone().map(|l| l.into_vec()) { + let labels: HashMap> = labels + .into_iter() + .filter_map(|l| { + if let Some((item, label)) = LabelItem::from_bip329(&l, network) { + Some((item, Some(label))) + } else { + None + } + }) + .collect(); + daemon.update_labels(&labels).await?; + } + + // import PSBTs + for psbt in psbts { + if let Err(e) = daemon.update_spend_tx(&psbt).await { + tracing::error!("Fail to restore PSBT: {e}") + } + } + + Ok((cache, wallet, config, daemon, datadir, internal_bitcoind)) +} + +pub async fn export_labels( + sender: Sender, + daemon: Option>, + path: PathBuf, +) { + let daemon = match daemon { + Some(d) => d, + None => { + send_error!(sender, Error::DaemonMissing); + return; + } + }; + let mut labels = Labels::new(Vec::new()); + let mut offset = 0u32; + loop { + let mut fetched = match daemon.get_labels_bip329(offset, DUMP_LABELS_LIMIT).await { + Ok(l) => l, + Err(e) => { + send_error!(sender, e.into()); + return; + } + } + .into_vec(); + let fetch_len = fetched.len() as u32; + labels.append(&mut fetched); + if fetch_len < DUMP_LABELS_LIMIT { + break; + } else { + offset += DUMP_LABELS_LIMIT; + } + } + let json = match labels.export() { + Ok(j) => j, + Err(e) => { + send_error!(sender, e.into()); + return; + } + }; + let mut file = open_file_write!(path, sender); + + if let Err(e) = file.write_all(json.as_bytes()) { + send_error!(sender, e.into()); + return; + } + send_progress!(sender, Progress(100.0)); + send_progress!(sender, Ended); +} + +pub async fn get_path(filename: String, write: bool) -> Option { + if write { + rfd::AsyncFileDialog::new() + .set_title("Choose a location to export...") + .set_file_name(filename) + .save_file() + .await + .map(|fh| fh.path().to_path_buf()) + } else { + rfd::AsyncFileDialog::new() + .set_title("Choose a file to import...") + .set_file_name(filename) + .pick_file() + .await + .map(|fh| fh.path().to_path_buf()) + } } diff --git a/liana-gui/src/installer/context.rs b/liana-gui/src/installer/context.rs index c1071ff1..025349bc 100644 --- a/liana-gui/src/installer/context.rs +++ b/liana-gui/src/installer/context.rs @@ -5,6 +5,7 @@ use std::time::Duration; use crate::{ app::settings::KeySetting, + backup::Backup, lianalite::client::backend::{BackendClient, BackendWalletClient}, node::bitcoind::{Bitcoind, InternalBitcoindConfig}, signer::Signer, @@ -69,6 +70,7 @@ pub struct Context { pub internal_bitcoind_config: Option, pub internal_bitcoind: Option, pub remote_backend: RemoteBackend, + pub backup: Option, } impl Context { @@ -95,6 +97,7 @@ impl Context { internal_bitcoind_config: None, internal_bitcoind: None, remote_backend, + backup: None, } } } diff --git a/liana-gui/src/installer/message.rs b/liana-gui/src/installer/message.rs index 56828ee6..efe5dc2f 100644 --- a/liana-gui/src/installer/message.rs +++ b/liana-gui/src/installer/message.rs @@ -2,12 +2,17 @@ use liana::miniscript::{ bitcoin::{bip32::Fingerprint, Network}, DescriptorPublicKey, }; -use std::path::PathBuf; +use std::{collections::HashMap, path::PathBuf}; use super::{context, Error}; use crate::{ - app::settings::ProviderKey, + app::{ + settings::{self, ProviderKey}, + view::Close, + }, + backup::{self, Backup}, download::{DownloadError, Progress}, + export::ImportExportMessage, hw::HardwareWalletMessage, installer::descriptor::{Key, PathKind}, lianalite::client::{auth::AuthClient, backend::api}, @@ -49,6 +54,23 @@ pub enum Message { RedeemNextKey, KeyRedeemed(ProviderKey, Result<(), services::Error>), AllKeysRedeemed, + BackupWallet, + ExportWallet(Result), + ImportExport(ImportExportMessage), + ImportBackup, + WalletFromBackup((HashMap, Backup)), +} + +impl Close for Message { + fn close() -> Self { + Self::Close + } +} + +impl From for Message { + fn from(value: ImportExportMessage) -> Self { + Message::ImportExport(value) + } } #[derive(Debug, Clone)] diff --git a/liana-gui/src/installer/mod.rs b/liana-gui/src/installer/mod.rs index 9c1fa3c5..245121cf 100644 --- a/liana-gui/src/installer/mod.rs +++ b/liana-gui/src/installer/mod.rs @@ -5,6 +5,7 @@ mod prompt; mod step; mod view; +pub use context::{Context, RemoteBackend}; use iced::{clipboard, Subscription, Task}; use liana::miniscript::bitcoin::{self, Network}; use liana_ui::{ @@ -14,8 +15,6 @@ use liana_ui::{ use lianad::config::Config; use tracing::{error, info, warn}; -use context::{Context, RemoteBackend}; - use std::io::Write; use std::path::PathBuf; use std::sync::{Arc, Mutex}; @@ -26,6 +25,7 @@ use crate::{ settings::{self as gui_settings, AuthConfig, Settings, SettingsError, WalletSetting}, wallet::wallet_name, }, + backup, daemon::DaemonError, datadir::create_directory, hw::{HardwareWalletConfig, HardwareWallets}, @@ -66,7 +66,7 @@ pub struct Installer { signer: Arc>, /// Context is data passed through each step. - context: Context, + pub context: Context, } impl Installer { @@ -292,6 +292,11 @@ impl Installer { .expect("There is always a step") .update(&mut self.hws, Message::Installed(Err(e))) } + Message::WalletFromBackup((ks, backup)) => { + self.context.keys = ks; + self.context.backup = Some(backup); + Task::none() + } _ => self .steps .get_mut(self.current) @@ -429,7 +434,7 @@ pub async fn install_local_wallet( info!("Gui configuration file created"); // create liana GUI settings file - let settings: gui_settings::Settings = extract_local_gui_settings(&ctx).await; + let settings: gui_settings::Settings = extract_local_gui_settings(&ctx); create_and_write_file( network_datadir_path, gui_settings::DEFAULT_FILE_NAME, @@ -669,7 +674,7 @@ pub async fn extract_remote_gui_settings(ctx: &Context, backend: &BackendWalletC } } -pub async fn extract_local_gui_settings(ctx: &Context) -> Settings { +pub fn extract_local_gui_settings(ctx: &Context) -> Settings { let descriptor = ctx .descriptor .as_ref() @@ -731,6 +736,7 @@ pub enum Error { CannotGetAvailablePort(String), Unexpected(String), HardwareWallet(async_hwi::Error), + Backup(backup::Error), } impl From for Error { @@ -784,6 +790,7 @@ impl std::fmt::Display for Error { Self::CannotCreateFile(e) => write!(f, "Failed to create file: {}", e), Self::Unexpected(e) => write!(f, "Unexpected: {}", e), Self::HardwareWallet(e) => write!(f, "Hardware Wallet: {}", e), + Self::Backup(e) => write!(f, "Backup: {:?}", e), } } } diff --git a/liana-gui/src/installer/prompt.rs b/liana-gui/src/installer/prompt.rs index de1c44e1..bf4f2c79 100644 --- a/liana-gui/src/installer/prompt.rs +++ b/liana-gui/src/installer/prompt.rs @@ -1,5 +1,5 @@ -pub const BACKUP_DESCRIPTOR_MESSAGE: &str = "The descriptor is necessary to recover your funds. The backup of your key (via mnemonics, sometimes called 'seed words') is not enough. Please make sure you have backed up both your private key and your descriptor."; -pub const BACKUP_DESCRIPTOR_HELP: &str = "In Bitcoin, the coins are locked using a Script (related to the 'address'). In order to recover your funds you need to know both the Scripts you have participated in (your 'addresses'), and be able to sign a transaction that spends from those. For the ability to sign, you back up your private key, this is your mnemonic ('seed words'). For finding the coins that belong to you, you back up a template of your Script (your 'addresses'), this is your descriptor. However, note the descriptor need not be stored as securely as the private key. A thief who steals your descriptor but not your private key cannot steal your funds."; +pub const BACKUP_DESCRIPTOR_MESSAGE: &str = "A backup of your wallet configuration is necessary to recover your funds. Please make sure to store your Wallet backup file (or alternatively to copy and paste the descriptor string) in one or more secure and accessible locations. You still need to back up your seed phrases too, since they are not included in the file."; +pub const BACKUP_DESCRIPTOR_HELP: &str = "In Bitcoin, the coins are locked using a Script (related to the 'address'). In order to recover your funds you need to know both the Scripts you have participated in (your 'addresses'), and be able to sign a transaction that spends from those. For the ability to sign, you back up your private key, this is your mnemonic ('seed words'). For finding the coins that belong to you, you back up a template of your Script (your 'addresses'), this is your descriptor, included in your wallet backup file. However, note the descriptor need not be stored as securely as the private key. A thief who steals your descriptor but not your private key cannot steal your funds."; pub const DEFINE_DESCRIPTOR_FINGERPRINT_TOOLTIP: &str = "The alias is applied on all the keys derived from the same seed"; pub const REGISTER_DESCRIPTOR_HELP: &str = "To be used with the wallet, a signing device needs the descriptor. If the descriptor contains one or more keys imported from an external signing device, the descriptor must be registered on it. Registration confirms that the device is able to handle the policy. Registration on a device is not a substitute for backing up the descriptor."; diff --git a/liana-gui/src/installer/step/descriptor/mod.rs b/liana-gui/src/installer/step/descriptor/mod.rs index e98463b9..866fb447 100644 --- a/liana-gui/src/installer/step/descriptor/mod.rs +++ b/liana-gui/src/installer/step/descriptor/mod.rs @@ -16,7 +16,9 @@ use liana_ui::{component::form, widget::Element}; use async_hwi::DeviceKind; use crate::{ - app::{settings::KeySetting, wallet::wallet_name}, + app::{settings::KeySetting, state::export::ExportModal, wallet::wallet_name}, + backup::{self, Backup}, + export::{ImportExportMessage, ImportExportType, Progress}, hw::{HardwareWallet, HardwareWallets}, installer::{ message::{self, Message}, @@ -30,6 +32,8 @@ pub struct ImportDescriptor { imported_descriptor: form::Value, wrong_network: bool, error: Option, + modal: Option, + imported_backup: bool, } impl ImportDescriptor { @@ -39,6 +43,8 @@ impl ImportDescriptor { imported_descriptor: form::Value::default(), wrong_network: false, error: None, + modal: None, + imported_backup: false, } } @@ -75,14 +81,55 @@ impl Step for ImportDescriptor { fn skip(&self, ctx: &Context) -> bool { ctx.remote_backend.is_some() } - // form value is set as valid each time it is edited. - // Verification of the values is happening when the user click on Next button. + + fn subscription(&self, _hws: &HardwareWallets) -> Subscription { + if let Some(modal) = &self.modal { + if let Some(sub) = modal.subscription() { + sub.map(|m| Message::ImportExport(ImportExportMessage::Progress(m))) + } else { + Subscription::none() + } + } else { + Subscription::none() + } + } + fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Task { - if let Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(desc)) = - message - { - self.imported_descriptor.value = desc; - self.check_descriptor(self.network); + match message { + Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(desc)) => { + self.imported_descriptor.value = desc; + self.check_descriptor(self.network); + } + Message::ImportExport(ImportExportMessage::Close) => { + self.modal = None; + } + Message::ImportBackup => { + if !self.imported_backup { + let modal = ExportModal::new(None, ImportExportType::WalletFromBackup); + let launch = modal.launch(false); + self.modal = Some(modal); + return launch; + } + } + Message::ImportExport(ImportExportMessage::Progress(Progress::WalletFromBackup(r))) => { + let (descriptor, network, aliases, backup) = r; + if self.network == network { + self.imported_backup = true; + self.imported_descriptor.value = descriptor.to_string(); + return Task::perform(async move { (aliases, backup) }, |(a, b)| { + Message::WalletFromBackup((a, b)) + }); + } else { + self.error = Some("Backup network do not match the selected network!".into()); + } + } + Message::ImportExport(m) => { + if let Some(modal) = self.modal.as_mut() { + let task: Task = modal.update(m); + return task; + }; + } + _ => {} } Task::none() } @@ -106,13 +153,19 @@ impl Step for ImportDescriptor { progress: (usize, usize), email: Option<&'a str>, ) -> Element { - view::import_descriptor( + let content = view::import_descriptor( progress, email, &self.imported_descriptor, + self.imported_backup, self.wrong_network, self.error.as_ref(), - ) + ); + if let Some(modal) = &self.modal { + modal.view(content) + } else { + content + } } } @@ -292,16 +345,71 @@ pub struct BackupDescriptor { done: bool, descriptor: Option, keys: HashMap, + modal: Option, + error: Option, + context: Option, } impl Step for BackupDescriptor { + fn subscription(&self, _hws: &HardwareWallets) -> Subscription { + if let Some(modal) = &self.modal { + if let Some(sub) = modal.subscription() { + sub.map(|m| Message::ImportExport(ImportExportMessage::Progress(m))) + } else { + Subscription::none() + } + } else { + Subscription::none() + } + } fn update(&mut self, _hws: &mut HardwareWallets, message: Message) -> Task { - if let Message::UserActionDone(done) = message { - self.done = done; + match message { + Message::ImportExport(ImportExportMessage::Close) => { + self.modal = None; + } + Message::ImportExport(m) => { + if let Some(modal) = self.modal.as_mut() { + let task: Task = modal.update(m); + return task; + }; + } + Message::BackupWallet => { + if let (None, Some(ctx)) = (&self.modal, self.context.as_ref()) { + let ctx = ctx.clone(); + return Task::perform( + async move { + let backup = Backup::from_installer(ctx, true).await?; + serde_json::to_string_pretty(&backup).map_err(|_| backup::Error::Json) + }, + Message::ExportWallet, + ); + } + } + Message::ExportWallet(str) => { + if self.modal.is_none() { + let str = match str { + Ok(s) => s, + Err(e) => { + tracing::error!("{e:?}"); + self.error = Some(Error::Backup(e)); + return Task::none(); + } + }; + let modal = ExportModal::new(None, ImportExportType::ExportBackup(str)); + let launch = modal.launch(true); + self.modal = Some(modal); + return launch; + } + } + Message::UserActionDone(done) => { + self.done = done; + } + _ => {} } Task::none() } fn load_context(&mut self, ctx: &Context) { + self.context = Some(ctx.clone()); if self.descriptor != ctx.descriptor { self.descriptor.clone_from(&ctx.descriptor); self.done = false; @@ -318,13 +426,19 @@ impl Step for BackupDescriptor { progress: (usize, usize), email: Option<&'a str>, ) -> Element { - view::backup_descriptor( + let content = view::backup_descriptor( progress, email, self.descriptor.as_ref().expect("Must be a descriptor"), &self.keys, + self.error.as_ref(), self.done, - ) + ); + if let Some(modal) = &self.modal { + modal.view(content) + } else { + content + } } } diff --git a/liana-gui/src/installer/view/mod.rs b/liana-gui/src/installer/view/mod.rs index c85a542e..7aea026e 100644 --- a/liana-gui/src/installer/view/mod.rs +++ b/liana-gui/src/installer/view/mod.rs @@ -264,11 +264,15 @@ pub fn import_descriptor<'a>( progress: (usize, usize), email: Option<&'a str>, imported_descriptor: &form::Value, + imported_backup: bool, wrong_network: bool, error: Option<&String>, ) -> Element<'a, Message> { + let valid = !imported_descriptor.value.is_empty() && imported_descriptor.valid; + let col_descriptor = Column::new() .push(text("Descriptor:").bold()) + .push(Space::with_height(10)) .push( form::Form::new_trimmed("Descriptor", imported_descriptor, |msg| { Message::DefineDescriptor(message::DefineDescriptor::ImportDescriptor(msg)) @@ -280,21 +284,65 @@ pub fn import_descriptor<'a>( }) .size(text::P1_SIZE) .padding(10), + ); + + let descriptor = if imported_backup { + None + } else { + Some(col_descriptor) + }; + + let or = if !valid && !imported_backup { + Some( + Row::new() + .push(text("or").bold()) + .push(Space::with_width(Length::Fill)), ) - .spacing(10); + } else { + None + }; + + let import_backup = if !valid && !imported_backup { + Some( + Row::new() + .push(button::secondary(None, "Import backup").on_press(Message::ImportBackup)) + .push(Space::with_width(Length::Fill)), + ) + } else { + None + }; + + let backup_imported = if imported_backup { + Some( + Row::new() + .push(text("Backup successfuly imported!").bold()) + .push(Space::with_width(Length::Fill)), + ) + } else { + None + }; + layout( progress, email, "Import the wallet", Column::new() - .push(Column::new().spacing(20).push(col_descriptor).push(text( - "If you are using a Bitcoin Core node, \ + .push( + Column::new() + .spacing(20) + .push_maybe(descriptor) + .push_maybe(or) + .push_maybe(import_backup) + .push_maybe(backup_imported) + .push(text( + "If you are using a Bitcoin Core node, \ you will need to perform a rescan of \ the blockchain after creating the wallet \ in order to see your coins and past \ transactions. This can be done in \ Settings > Node.", - ))) + )), + ) .push( if imported_descriptor.value.is_empty() || !imported_descriptor.valid { button::secondary(None, "Next").width(Length::Fixed(200.0)) @@ -689,12 +737,20 @@ pub fn backup_descriptor<'a>( email: Option<&'a str>, descriptor: &'a LianaDescriptor, keys: &'a HashMap, + error: Option<&Error>, done: bool, ) -> Element<'a, Message> { + let backup_button = if done { + button::secondary(Some(icon::backup_icon()), "Back Up Wallet") + .on_press(Message::BackupWallet) + } else { + button::primary(Some(icon::backup_icon()), "Back Up Wallet").on_press(Message::BackupWallet) + }; + layout( progress, email, - "Backup your wallet descriptor", + "Back Up your wallet", Column::new() .push( Column::new() @@ -724,6 +780,7 @@ pub fn backup_descriptor<'a>( )) .max_width(1000), ) + .push_maybe(error.map(|e| card::error("Failed to export backup", e.to_string()))) .push( card::simple( Column::new() @@ -741,10 +798,14 @@ pub fn backup_descriptor<'a>( ), ) .push( - Row::new().push(Column::new().width(Length::Fill)).push( - button::secondary(Some(icon::clipboard_icon()), "Copy") - .on_press(Message::Clibpboard(descriptor.to_string())), - ), + Row::new() + .push(Space::with_width(Length::Fill)) + .push(backup_button) + .push(Space::with_width(10)) + .push( + button::secondary(Some(icon::clipboard_icon()), "Copy") + .on_press(Message::Clibpboard(descriptor.to_string())), + ), ) .spacing(10), ) @@ -756,10 +817,11 @@ pub fn backup_descriptor<'a>( .max_width(1500), ) .push( - checkbox("I have backed up my descriptor", done).on_toggle(Message::UserActionDone), + checkbox("I have backed up my wallet/descriptor", done) + .on_toggle(Message::UserActionDone), ) .push(if done { - button::secondary(None, "Next") + button::primary(None, "Next") .on_press(Message::Next) .width(Length::Fixed(200.0)) } else { diff --git a/liana-gui/src/lianalite/client/backend/api.rs b/liana-gui/src/lianalite/client/backend/api.rs index 2c45c2ba..131e04c9 100644 --- a/liana-gui/src/lianalite/client/backend/api.rs +++ b/liana-gui/src/lianalite/client/backend/api.rs @@ -105,6 +105,8 @@ pub struct Wallet { pub name: String, #[serde(deserialize_with = "deser_fromstr")] pub descriptor: LianaDescriptor, + pub deposit_derivation_index: u32, + pub change_derivation_index: u32, pub recovery_paths: Vec, pub biggest_remaining_sequence: Option, pub smallest_remaining_sequence: Option, @@ -333,6 +335,11 @@ pub struct ListPsbts { pub psbts: Vec, } +#[derive(Deserialize)] +pub struct Labels { + pub labels: lianad::bip329::Labels, +} + #[derive(Deserialize)] pub struct Address { #[serde(deserialize_with = "deser_addr_assume_checked")] diff --git a/liana-gui/src/lianalite/client/backend/mod.rs b/liana-gui/src/lianalite/client/backend/mod.rs index 7b8eca2a..7b1ffaf2 100644 --- a/liana-gui/src/lianalite/client/backend/mod.rs +++ b/liana-gui/src/lianalite/client/backend/mod.rs @@ -13,7 +13,8 @@ use liana::{ miniscript::bitcoin::{address, psbt::Psbt, Address, Network, OutPoint, Txid}, }; use lianad::{ - commands::{CoinStatus, GetInfoDescriptors, LCSpendInfo, LabelItem}, + bip329::Labels, + commands::{CoinStatus, GetInfoDescriptors, LCSpendInfo, LabelItem, UpdateDerivIndexesResult}, config::Config, }; use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response}; @@ -593,6 +594,8 @@ impl Daemon for BackendWalletClient { timestamp: wallet.created_at as u32, // We can ignore this field for remote backend as the wallet should remain synced. last_poll_timestamp: None, + receive_index: wallet.deposit_derivation_index, + change_index: wallet.change_derivation_index, }) } @@ -624,6 +627,14 @@ impl Daemon for BackendWalletClient { }) } + async fn update_deriv_indexes( + &self, + _receive: Option, + _change: Option, + ) -> Result { + Err(DaemonError::NotImplemented) + } + /// Spent coins are not returned if statuses is empty, unless their outpoints are specified. async fn list_coins( &self, @@ -1115,6 +1126,24 @@ impl Daemon for BackendWalletClient { Ok(()) } + + async fn get_labels_bip329(&self, offset: u32, limit: u32) -> Result { + let response: Response = self + .inner + .request( + Method::GET, + &format!( + "{}/v1/wallets/{}/labels/bip329?offset={}&limit={}", + self.inner.url, self.wallet_uuid, offset, limit + ), + ) + .await + .send() + .await?; + + let res: api::Labels = response.json().await?; + Ok(res.labels) + } } fn history_tx_from_api(value: api::Transaction, network: Network) -> HistoryTransaction { diff --git a/liana-gui/src/lib.rs b/liana-gui/src/lib.rs index f0160fa1..411db157 100644 --- a/liana-gui/src/lib.rs +++ b/liana-gui/src/lib.rs @@ -1,4 +1,5 @@ pub mod app; +pub mod backup; pub mod daemon; pub mod datadir; pub mod download; diff --git a/liana-gui/src/loader.rs b/liana-gui/src/loader.rs index 72f0a1c9..e785f721 100644 --- a/liana-gui/src/loader.rs +++ b/liana-gui/src/loader.rs @@ -23,6 +23,9 @@ use lianad::{ StartupError, }; +use crate::app; +use crate::backup::Backup; +use crate::export::RestoreBackupError; use crate::{ app::{ cache::Cache, @@ -56,7 +59,7 @@ pub struct Loader { pub daemon_started: bool, pub internal_bitcoind: Option, pub waiting_daemon_bitcoind: bool, - + pub backup: Option, step: Step, } @@ -83,6 +86,20 @@ pub enum Message { Cache, Arc, Option, + Option, + ), + Error, + >, + ), + App( + Result< + ( + Cache, + Arc, + app::Config, + Arc, + PathBuf, + Option, ), Error, >, @@ -100,6 +117,7 @@ impl Loader { gui_config: GUIConfig, network: bitcoin::Network, internal_bitcoind: Option, + backup: Option, ) -> (Self, Task) { let path = gui_config .daemon_rpc_path @@ -114,6 +132,7 @@ impl Loader { daemon_started: false, internal_bitcoind, waiting_daemon_bitcoind: false, + backup, }, Task::perform(connect(path), Message::Loaded), ) @@ -134,6 +153,7 @@ impl Loader { self.datadir_path.clone(), self.network, self.internal_bitcoind.clone(), + self.backup.clone(), ), Message::Synced, ); @@ -232,6 +252,7 @@ impl Loader { self.datadir_path.clone(), self.network, self.internal_bitcoind.clone(), + self.backup.clone(), ), Message::Synced, ); @@ -290,6 +311,7 @@ impl Loader { self.gui_config.clone(), self.network, self.internal_bitcoind.clone(), + self.backup.clone(), ); *self = loader; cmd @@ -391,12 +413,14 @@ pub async fn load_application( datadir_path: PathBuf, network: bitcoin::Network, internal_bitcoind: Option, + backup: Option, ) -> Result< ( Arc, Cache, Arc, Option, + Option, ), Error, > { @@ -421,7 +445,7 @@ pub async fn load_application( ..Default::default() }; - Ok((Arc::new(wallet), cache, daemon, internal_bitcoind)) + Ok((Arc::new(wallet), cache, daemon, internal_bitcoind, backup)) } #[derive(Clone, Debug)] @@ -582,6 +606,7 @@ pub enum Error { Daemon(DaemonError), Bitcoind(StartInternalBitcoindError), BitcoindLogs(std::io::Error), + RestoreBackup(RestoreBackupError), } impl std::fmt::Display for Error { @@ -592,6 +617,7 @@ impl std::fmt::Display for Error { Self::Daemon(e) => write!(f, "Liana daemon error: {}", e), Self::Bitcoind(e) => write!(f, "Bitcoind error: {}", e), Self::BitcoindLogs(e) => write!(f, "Bitcoind logs error: {}", e), + Self::RestoreBackup(e) => write!(f, "Restore backup: {e}"), } } } diff --git a/liana-gui/src/main.rs b/liana-gui/src/main.rs index 72ef2814..476d3ca6 100644 --- a/liana-gui/src/main.rs +++ b/liana-gui/src/main.rs @@ -24,11 +24,12 @@ use lianad::config::Config as DaemonConfig; use liana_gui::{ app::{self, cache::Cache, config::default_datadir, wallet::Wallet, App}, datadir, + export::import_backup_at_launch, hw::HardwareWalletConfig, installer::{self, Installer}, launcher::{self, Launcher}, lianalite::{ - client::{backend::api, backend::BackendWalletClient}, + client::backend::{api, BackendWalletClient}, login, }, loader::{self, Loader}, @@ -162,7 +163,7 @@ impl GUI { network, log_level.unwrap_or_else(|| cfg.log_level().unwrap_or(LevelFilter::INFO)), ); - let (loader, command) = Loader::new(datadir_path, cfg, network, None); + let (loader, command) = Loader::new(datadir_path, cfg, network, None, None); cmds.push(command.map(|msg| Message::Load(Box::new(msg)))); State::Loader(Box::new(loader)) } @@ -242,12 +243,13 @@ impl GUI { self.state = State::Login(Box::new(login)); command.map(|msg| Message::Login(Box::new(msg))) } else { - let (loader, command) = Loader::new(datadir_path, cfg, network, None); + let (loader, command) = + Loader::new(datadir_path, cfg, network, None, None); self.state = State::Loader(Box::new(loader)); command.map(|msg| Message::Load(Box::new(msg))) } } else { - let (loader, command) = Loader::new(datadir_path, cfg, network, None); + let (loader, command) = Loader::new(datadir_path, cfg, network, None, None); self.state = State::Loader(Box::new(loader)); command.map(|msg| Message::Load(Box::new(msg))) } @@ -334,6 +336,7 @@ impl GUI { cfg, daemon_cfg.bitcoin_config.network, internal_bitcoind, + i.context.backup.take(), ); self.state = State::Loader(Box::new(loader)); command.map(|msg| Message::Load(Box::new(msg))) @@ -353,18 +356,45 @@ impl GUI { self.state = State::Launcher(Box::new(launcher)); command.map(|msg| Message::Launch(Box::new(msg))) } - loader::Message::Synced(Ok((wallet, cache, daemon, bitcoind))) => { - let (app, command) = App::new( - cache, - wallet, - loader.gui_config.clone(), - daemon, - loader.datadir_path.clone(), - bitcoind, - ); + loader::Message::Synced(Ok((wallet, cache, daemon, bitcoind, backup))) => { + if let Some(backup) = backup { + let config = loader.gui_config.clone(); + let datadir = loader.datadir_path.clone(); + Task::perform( + async move { + import_backup_at_launch( + cache, wallet, config, daemon, datadir, bitcoind, backup, + ) + .await + }, + |r| { + let r = r.map_err(loader::Error::RestoreBackup); + Message::Load(Box::new(loader::Message::App(r))) + }, + ) + } else { + let (app, command) = App::new( + cache, + wallet, + loader.gui_config.clone(), + daemon, + loader.datadir_path.clone(), + bitcoind, + ); + self.state = State::App(app); + command.map(|msg| Message::Run(Box::new(msg))) + } + } + loader::Message::App(Ok((cache, wallet, config, daemon, datadir, bitcoind))) => { + let (app, command) = App::new(cache, wallet, config, daemon, datadir, bitcoind); self.state = State::App(app); command.map(|msg| Message::Run(Box::new(msg))) } + loader::Message::App(Err(e)) => { + tracing::error!("Fail to import backup: {e}"); + Task::none() + } + _ => loader.update(*msg).map(|msg| Message::Load(Box::new(msg))), }, (State::App(i), Message::Run(msg)) => { @@ -574,6 +604,7 @@ fn main() -> Result<(), Box> { fonts: font::load(), }; + #[allow(unused_mut)] let mut window_settings = iced::window::Settings { icon: Some(image::liana_app_icon()), position: iced::window::Position::Default, diff --git a/liana-gui/src/node/bitcoind.rs b/liana-gui/src/node/bitcoind.rs index 955c5898..59831507 100644 --- a/liana-gui/src/node/bitcoind.rs +++ b/liana-gui/src/node/bitcoind.rs @@ -6,6 +6,7 @@ use liana::{ }; use liana_ui::component::form; use lianad::config::BitcoindConfig; +use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; use std::fmt; use std::path::{Path, PathBuf}; @@ -154,7 +155,7 @@ impl std::fmt::Display for RpcAuthParseError { } /// Represents RPC auth credentials as stored in bitcoin.conf. -#[derive(PartialEq, Eq, Debug, Clone)] +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] pub struct RpcAuth { pub user: String, salt: String, @@ -209,7 +210,7 @@ impl std::str::FromStr for RpcAuth { } /// Represents section for a single network in `bitcoin.conf` file. -#[derive(PartialEq, Eq, Debug, Clone)] +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] pub struct InternalBitcoindNetworkConfig { pub rpc_port: u16, pub p2p_port: u16, diff --git a/liana-ui/src/icon.rs b/liana-ui/src/icon.rs index c733734a..9a8dd1c5 100644 --- a/liana-ui/src/icon.rs +++ b/liana-ui/src/icon.rs @@ -123,6 +123,14 @@ pub fn round_key_icon() -> Text<'static> { bootstrap_icon('\u{F44E}') } +pub fn backup_icon() -> Text<'static> { + bootstrap_icon('\u{F356}') +} + +pub fn restore_icon() -> Text<'static> { + bootstrap_icon('\u{F358}') +} + const ICONEX_ICONS: Font = Font::with_name("Untitled1"); fn iconex_icon(unicode: char) -> Text<'static> { diff --git a/lianad/Cargo.toml b/lianad/Cargo.toml index bbe83b2e..d39b37a0 100644 --- a/lianad/Cargo.toml +++ b/lianad/Cargo.toml @@ -55,3 +55,6 @@ rusqlite = { version = "0.30", features = ["bundled", "unlock_notify"] } # To talk to bitcoind jsonrpc = { version = "0.17", features = ["minreq_http"], default-features = false } + +# import/export labels +bip329 = "0.3.0" diff --git a/lianad/src/commands/mod.rs b/lianad/src/commands/mod.rs index 6f04f1ed..2417f246 100644 --- a/lianad/src/commands/mod.rs +++ b/lianad/src/commands/mod.rs @@ -36,7 +36,11 @@ use std::{ }; use miniscript::{ - bitcoin::{self, address, bip32, psbt::Psbt}, + bitcoin::{ + self, address, + bip32::{self, ChildNumber}, + psbt::Psbt, + }, psbt::PsbtExt, }; use serde::{Deserialize, Serialize}; @@ -314,6 +318,8 @@ impl DaemonControl { let mut db_conn = self.db.connection(); let block_height = db_conn.chain_tip().map(|tip| tip.height).unwrap_or(0); let wallet = db_conn.wallet(); + let receive_index: u32 = db_conn.receive_index().into(); + let change_index: u32 = db_conn.change_index().into(); let rescan_progress = wallet .rescan_timestamp .map(|_| self.bitcoin.rescan_progress().unwrap_or(1.0)); @@ -328,6 +334,8 @@ impl DaemonControl { rescan_progress, timestamp: wallet.timestamp, last_poll_timestamp: wallet.last_poll_timestamp, + receive_index, + change_index, } } @@ -349,6 +357,60 @@ impl DaemonControl { GetAddressResult::new(address, index) } + /// Update derivation indexes + pub fn update_deriv_indexes( + &self, + receive: Option, + change: Option, + ) -> Result { + let mut db_conn = self.db.connection(); + + const MAX_INCREMENT_GAP: u32 = 1_000; + + let db_receive = db_conn.receive_index().into(); + let mut final_receive = db_receive; + + let db_change = db_conn.change_index().into(); + let mut final_change = db_change; + + if let Some(index) = receive { + ChildNumber::from_normal_idx(index) + .map_err(|_| CommandError::InvalidDerivationIndex)?; + if index > db_receive { + let delta = (index - db_receive).min(MAX_INCREMENT_GAP); + let index = db_receive + delta; + final_receive = index; + match ChildNumber::from_normal_idx(index) { + Ok(i) => { + db_conn.set_receive_index(i, &self.secp); + } + Err(_) => return Err(CommandError::InvalidDerivationIndex), + }; + } + } + + if let Some(index) = change { + ChildNumber::from_normal_idx(index) + .map_err(|_| CommandError::InvalidDerivationIndex)?; + if index > db_change { + let delta = (index - db_change).min(MAX_INCREMENT_GAP); + let index = db_change + delta; + final_change = index; + match ChildNumber::from_normal_idx(index) { + Ok(i) => { + db_conn.set_change_index(i, &self.secp); + } + Err(_) => return Err(CommandError::InvalidDerivationIndex), + }; + } + } + + Ok(UpdateDerivIndexesResult { + receive: final_receive, + change: final_change, + }) + } + /// list addresses pub fn list_addresses( &self, @@ -687,6 +749,13 @@ impl DaemonControl { } } + pub fn get_labels_bip329(&self, offset: u32, limit: u32) -> GetLabelsBip329Result { + let mut db_conn = self.db.connection(); + GetLabelsBip329Result { + labels: db_conn.get_labels_bip329(offset, limit), + } + } + pub fn list_spend( &self, txids: Option>, @@ -1161,6 +1230,16 @@ pub struct GetInfoResult { pub timestamp: u32, /// Timestamp of last poll, if any. pub last_poll_timestamp: Option, + /// Last index used to generate a receive address + pub receive_index: u32, + /// Last index used to generate a change address + pub change_index: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateDerivIndexesResult { + pub receive: u32, + pub change: u32, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -1184,6 +1263,11 @@ pub struct GetLabelsResult { pub labels: HashMap, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetLabelsBip329Result { + pub labels: crate::bip329::Labels, +} + #[derive(Debug, Clone, Serialize, PartialEq, Eq)] pub struct AddressInfo { index: u32, diff --git a/lianad/src/database/mod.rs b/lianad/src/database/mod.rs index c8f13ca5..cd1bd22b 100644 --- a/lianad/src/database/mod.rs +++ b/lianad/src/database/mod.rs @@ -20,7 +20,8 @@ use std::{ sync, }; -use miniscript::bitcoin::{self, bip32, psbt::Psbt, secp256k1}; +use bip329::Labels; +use miniscript::bitcoin::{self, bip32, psbt::Psbt, secp256k1, Address, Network, OutPoint, Txid}; /// Information about the wallet. /// @@ -190,6 +191,9 @@ pub trait DatabaseConnection { &mut self, txids: &[bitcoin::Txid], ) -> Vec<(bitcoin::Transaction, Option, Option)>; + + /// Dump all labels + fn get_labels_bip329(&mut self, offset: u32, limit: u32) -> Labels; } impl DatabaseConnection for SqliteConn { @@ -367,6 +371,15 @@ impl DatabaseConnection for SqliteConn { HashMap::from_iter(labels.into_iter().map(|label| (label.item, label.value))) } + fn get_labels_bip329(&mut self, offset: u32, limit: u32) -> Labels { + let labels = self + .labels_bip329(offset, limit) + .into_iter() + .map(|l| l.into()) + .collect(); + Labels::new(labels) + } + fn rollback_tip(&mut self, new_tip: &BlockChainTip) { self.rollback_tip(new_tip) } @@ -558,6 +571,47 @@ impl LabelItem { None } } + + pub fn from_bip329(label: &bip329::Label, network: Network) -> Option<(Self, String)> { + match label { + bip329::Label::Transaction(tx_record) => { + if let (Some(txid), Some(label)) = ( + Txid::from_str(&tx_record.ref_.to_string()).ok(), + tx_record.label.clone(), + ) { + Some((Self::Txid(txid), label)) + } else { + None + } + } + bip329::Label::Address(address_record) => { + if let (Some(addr), Some(label)) = ( + Address::from_str(&address_record.ref_.clone().assume_checked().to_string()) + .ok(), + address_record.label.clone(), + ) { + if addr.is_valid_for_network(network) { + Some((Self::Address(addr.assume_checked()), label)) + } else { + None + } + } else { + None + } + } + bip329::Label::Output(output_record) => { + if let (Some(outpoint), Some(label)) = ( + OutPoint::from_str(&output_record.ref_.to_string()).ok(), + output_record.label.clone(), + ) { + Some((Self::OutPoint(outpoint), label)) + } else { + None + } + } + _ => None, + } + } } #[cfg(test)] diff --git a/lianad/src/database/sqlite/mod.rs b/lianad/src/database/sqlite/mod.rs index 5ec1462e..5f6876ee 100644 --- a/lianad/src/database/sqlite/mod.rs +++ b/lianad/src/database/sqlite/mod.rs @@ -678,6 +678,18 @@ impl SqliteConn { .expect("Db must not fail") } + pub fn labels_bip329(&mut self, offset: u32, limit: u32) -> Vec { + db_query( + &mut self.conn, + "SELECT * FROM labels \ + ORDER BY id \ + LIMIT ?1 OFFSET ?2", + rusqlite::params![limit, offset], + |row| row.try_into(), + ) + .expect("Db must not fail") + } + /// Retrieves a limited and ordered list of transactions ids that happened during the given /// range. pub fn db_list_txids(&mut self, start: u32, end: u32, limit: u64) -> Vec { diff --git a/lianad/src/database/sqlite/schema.rs b/lianad/src/database/sqlite/schema.rs index 1e6a7610..34eb5fd2 100644 --- a/lianad/src/database/sqlite/schema.rs +++ b/lianad/src/database/sqlite/schema.rs @@ -1,8 +1,16 @@ +use bip329::Label; use liana::descriptors::LianaDescriptor; use std::{convert::TryFrom, str::FromStr}; -use miniscript::bitcoin::{self, address, bip32, consensus::encode, psbt::Psbt}; +use miniscript::bitcoin::{ + self, + address::{self, NetworkUnchecked}, + bip32, + consensus::encode, + psbt::Psbt, + Address, OutPoint, Txid, +}; // Due to limitations of Sqlite's ALTER TABLE command and in order not to recreate // tables during migration: @@ -366,6 +374,40 @@ impl From for DbLabelledKind { } } +impl From for Label { + fn from(value: DbLabel) -> Self { + let mut ref_ = value.item; + if value.item_kind == DbLabelledKind::Txid { + let frontward: Txid = bitcoin::consensus::encode::deserialize_hex(&ref_).unwrap(); + ref_ = frontward.to_string(); + } + let label = if value.value.is_empty() { + None + } else { + Some(value.value) + }; + match value.item_kind { + DbLabelledKind::Address => Label::Address(bip329::AddressRecord { + ref_: Address::::from_str(&ref_) + .expect("db contains valid adresses"), + label, + }), + DbLabelledKind::OutPoint => Label::Output(bip329::OutputRecord { + ref_: OutPoint::from_str(&ref_).expect(" db contais valid outpoints"), + label, + spendable: true, + }), + DbLabelledKind::Txid => Label::Transaction(bip329::TransactionRecord { + ref_: bitcoin::consensus::encode::deserialize_hex(&ref_) + .expect("db contains valid txid"), + label, + // FIXME: "Optional key origin information referencing the wallet associated with the label" + origin: None, + }), + } + } +} + impl TryFrom<&rusqlite::Row<'_>> for DbLabel { type Error = rusqlite::Error; diff --git a/lianad/src/jsonrpc/api.rs b/lianad/src/jsonrpc/api.rs index d27a7f1d..808156a1 100644 --- a/lianad/src/jsonrpc/api.rs +++ b/lianad/src/jsonrpc/api.rs @@ -199,6 +199,50 @@ fn list_addresses( Ok(serde_json::json!(&res)) } +fn update_deriv_indexes( + control: &DaemonControl, + params: Params, +) -> Result { + let receive = params.get(0, "receive"); + let change = params.get(1, "change"); + + if receive.is_none() && change.is_none() { + return Err(Error::invalid_params( + "Missing 'receive' or 'change' parameter", + )); + } + + let receive = match receive { + Some(i) => { + let res = i.as_i64().ok_or(Error::invalid_params( + "Invalid value for 'receive' param".to_string(), + ))?; + let res = res + .try_into() + .map_err(|_| Error::invalid_params("Invalid value for 'receive' param"))?; + Some(res) + } + None => None, + }; + + let change = match change { + Some(i) => { + let res = i.as_i64().ok_or(Error::invalid_params( + "Invalid value for 'change' param".to_string(), + ))?; + let res = res + .try_into() + .map_err(|_| Error::invalid_params("Invalid value for 'change' param"))?; + Some(res) + } + None => None, + }; + + Ok(serde_json::json!( + control.update_deriv_indexes(receive, change)? + )) +} + fn list_confirmed(control: &DaemonControl, params: Params) -> Result { let start: u32 = params .get(0, "start") @@ -364,6 +408,22 @@ fn get_labels(control: &DaemonControl, params: Params) -> Result Result { + let offset: u32 = params + .get(0, "offset") + .ok_or_else(|| Error::invalid_params("Missing 'offset' parameter."))? + .as_u64() + .and_then(|t| t.try_into().ok()) + .ok_or_else(|| Error::invalid_params("Invalid 'offset' parameter."))?; + let limit: u32 = params + .get(1, "limit") + .ok_or_else(|| Error::invalid_params("Missing 'limit' parameter."))? + .as_u64() + .and_then(|t| t.try_into().ok()) + .ok_or_else(|| Error::invalid_params("Invalid 'limit' parameter."))?; + Ok(serde_json::json!(control.get_labels_bip329(offset, limit))) +} + /// Handle an incoming JSONRPC2 request. pub fn handle_request(control: &mut DaemonControl, req: Request) -> Result { let result = match req.method.as_str() { @@ -401,6 +461,12 @@ pub fn handle_request(control: &mut DaemonControl, req: Request) -> Result serde_json::json!(&control.get_info()), "getnewaddress" => serde_json::json!(&control.get_new_address()), + "updatederivationindexes" => { + let params = req.params.ok_or_else(|| { + Error::invalid_params("Missing 'receive' or 'change' parameters.") + })?; + update_deriv_indexes(control, params)? + } "listcoins" => { let params = req.params; list_coins(control, params)? @@ -451,6 +517,12 @@ pub fn handle_request(control: &mut DaemonControl, req: Request) -> Result { + let params = req + .params + .ok_or_else(|| Error::invalid_params("Missing 'offset' and 'limit' parameters."))?; + get_labels_bip329(control, params)? + } _ => { return Err(Error::method_not_found()); } diff --git a/lianad/src/lib.rs b/lianad/src/lib.rs index 7ad1333e..ea40ac46 100644 --- a/lianad/src/lib.rs +++ b/lianad/src/lib.rs @@ -7,6 +7,7 @@ mod jsonrpc; mod testutils; pub use bdk_electrum::electrum_client; +pub use bip329; use bitcoin::electrum; pub use miniscript; diff --git a/lianad/src/testutils.rs b/lianad/src/testutils.rs index aacf64a3..dd05f716 100644 --- a/lianad/src/testutils.rs +++ b/lianad/src/testutils.rs @@ -523,6 +523,10 @@ impl DatabaseConnection for DummyDatabase { } wallet_txs } + + fn get_labels_bip329(&mut self, _offset: u32, _limit: u32) -> bip329::Labels { + todo!() + } } pub struct DummyLiana { diff --git a/tests/test_rpc.py b/tests/test_rpc.py index 4ba57726..39ea5b82 100644 --- a/tests/test_rpc.py +++ b/tests/test_rpc.py @@ -20,6 +20,8 @@ from test_framework.utils import ( USE_TAPROOT, ) +MAX_DERIV = 2**31 - 1 + def test_getinfo(lianad): res = lianad.rpc.getinfo() @@ -36,6 +38,129 @@ def test_getinfo(lianad): time.sleep(lianad.poll_interval_secs + 1) res = lianad.rpc.getinfo() assert res["last_poll_timestamp"] > last_poll_timestamp + assert res["receive_index"] == 0 + assert res["change_index"] == 0 + + +def test_update_derivation_indexes(lianad): + info = lianad.rpc.getinfo() + assert info["receive_index"] == 0 + assert info["change_index"] == 0 + + ret = lianad.rpc.updatederivationindexes(0, 0) + info = lianad.rpc.getinfo() + assert info["receive_index"] == 0 + assert info["change_index"] == 0 + assert ret["receive"] == 0 + assert ret["change"] == 0 + + ret = lianad.rpc.updatederivationindexes(receive=3) + info = lianad.rpc.getinfo() + assert info["receive_index"] == 3 + assert info["change_index"] == 0 + assert ret["receive"] == 3 + assert ret["change"] == 0 + + ret = lianad.rpc.updatederivationindexes(change=4) + info = lianad.rpc.getinfo() + assert info["receive_index"] == 3 + assert info["change_index"] == 4 + assert ret["receive"] == 3 + assert ret["change"] == 4 + + ret = lianad.rpc.updatederivationindexes(receive=1, change=2) + info = lianad.rpc.getinfo() + assert info["receive_index"] == 3 + assert info["change_index"] == 4 + assert ret["receive"] == 3 + assert ret["change"] == 4 + + ret = lianad.rpc.updatederivationindexes(5, 6) + info = lianad.rpc.getinfo() + assert info["receive_index"] == 5 + assert info["change_index"] == 6 + assert ret["receive"] == 5 + assert ret["change"] == 6 + + ret = lianad.rpc.updatederivationindexes(0, 0) + info = lianad.rpc.getinfo() + assert info["receive_index"] == 5 + assert info["change_index"] == 6 + assert ret["receive"] == 5 + assert ret["change"] == 6 + + # Will explicitly error on invalid indexes + with pytest.raises( + RpcError, + match=re.escape( + "Invalid params: Invalid value for \'receive\' param" + ), + ): + lianad.rpc.updatederivationindexes(-1) + + with pytest.raises( + RpcError, + match=re.escape( + "Invalid params: Invalid value for \'change\' param" + ), + ): + lianad.rpc.updatederivationindexes(0, -1) + + with pytest.raises( + RpcError, + match=re.escape( + "Unhardened or overflowing BIP32 derivation index." + ), + ): + lianad.rpc.updatederivationindexes(MAX_DERIV + 1, 2) + + with pytest.raises( + RpcError, + match=re.escape( + "Unhardened or overflowing BIP32 derivation index." + ), + ): + lianad.rpc.updatederivationindexes(0, MAX_DERIV + 1) + + with pytest.raises( + RpcError, + match=re.escape( + "Unhardened or overflowing BIP32 derivation index." + ), + ): + lianad.rpc.updatederivationindexes(receive=(MAX_DERIV+1)) + + with pytest.raises( + RpcError, + match=re.escape( + "Unhardened or overflowing BIP32 derivation index." + ), + ): + lianad.rpc.updatederivationindexes(change=(MAX_DERIV+1)) + + with pytest.raises( + RpcError, + match=re.escape( + "Invalid params: Missing \'receive\' or \'change\' parameter" + ), + ): + lianad.rpc.updatederivationindexes() + + last_derivs = lianad.rpc.updatederivationindexes(0, 0) + last_receive = last_derivs["receive"] + last_change = last_derivs["change"] + + ret = lianad.rpc.updatederivationindexes(0, (MAX_DERIV - 1)) + assert ret["receive"] == last_receive + assert ret["change"] == last_change + 1000 + + last_derivs = lianad.rpc.updatederivationindexes(0, 0) + last_receive = last_derivs["receive"] + last_change = last_derivs["change"] + + ret = lianad.rpc.updatederivationindexes((MAX_DERIV -1 ), 0) + assert ret["receive"] == last_receive + 1000 + assert ret["change"] == last_change def test_getaddress(lianad): @@ -45,6 +170,9 @@ def test_getaddress(lianad): assert res["address"] != lianad.rpc.getnewaddress()["address"] # new address has derivation_index higher than the previous one assert lianad.rpc.getnewaddress()["derivation_index"] == res["derivation_index"] + 2 + info = lianad.rpc.getinfo() + assert info["receive_index"] == res["derivation_index"] + 3 + assert info["change_index"] == 0 def test_listaddresses(lianad): @@ -420,6 +548,8 @@ def test_create_spend(lianad, bitcoind): assert len(spend_psbt.o) == 4 assert len(spend_psbt.tx.vout) == 4 + assert lianad.rpc.getinfo()["change_index"] == 15 + # The transaction must contain the spent transaction for each input for P2WSH. But not for Taproot. # We don't make assumptions about the ordering of PSBT inputs. if USE_TAPROOT: @@ -1081,6 +1211,76 @@ def test_labels(lianad, bitcoind): assert res[random_address] == "this address is random" +def test_labels_bip329(lianad, bitcoind): + # Label 5 addresses + addresses = [] + for i in range(0,5): + addr = lianad.rpc.getnewaddress()["address"] + addresses.append(addr) + lianad.rpc.updatelabels({addr: f"addr{i}"}) + + # Label 5 coin + txids = [] + for i in range(0,5): + addr = lianad.rpc.getnewaddress()["address"] + txid = bitcoind.rpc.sendtoaddress(addr, 1) + txids.append(txid) + wait_for(lambda: len(lianad.rpc.listcoins()["coins"]) == i+1 ) + + coins = lianad.rpc.listcoins()["coins"] + for i in range(0,5): + coin = coins[i] + lianad.rpc.updatelabels({coin["outpoint"]: f"coin{i}"}) + + # Label 5 transactions + for i, txid in enumerate(txids): + lianad.rpc.updatelabels({txid: f"tx{i}"}) + + # Get Bip-0329 labels + bip329_labels = lianad.rpc.getlabelsbip329(0,100)["labels"] + assert len(bip329_labels) == 15 + + def label_found(name, labels): + for label in labels: + if label["label"] == name: + return True + return False + + # All transactions are labelled + for i in range(0, len(txids)): + assert label_found(f"tx{i}", bip329_labels) + + # All adresses are labelled + for i in range(0, len(addresses)): + assert label_found(f"addr{i}", bip329_labels) + + # All coins are labelled + for i in range(0, len(coins)): + assert label_found(f"coin{i}", bip329_labels) + + # There is no conflict between batches + batch1 = lianad.rpc.getlabelsbip329(0,5)["labels"] + assert len(batch1) == 5 + + batch2 = lianad.rpc.getlabelsbip329(5,5)["labels"] + assert len(batch2) == 5 + + batch3 = lianad.rpc.getlabelsbip329(10,5)["labels"] + assert len(batch3) == 5 + + for label in batch1: + print(label) + name = label["label"] + + assert not label_found(name, batch2) + assert not label_found(name, batch3) + + for label in batch2: + name = label["label"] + assert not label_found(name, batch1) + assert not label_found(name, batch3) + + def test_rbfpsbt_bump_fee(lianad, bitcoind): """Test the use of RBF to bump the fee of a transaction."""