Merge #1496: gui: tx export feature
22f48753a367187a1d08bc34d20e5b75bdfc01b8 gui: add and export transactions feature (pythcoiner)
Pull request description:
This PR add a feature to export transactions:
Done:
- [x] Subscription to run the feature in a detached thread w/ possibility to send update about the ongoing progress.
- [x] Let user choose the path using `rfd` crate
- [x] Add a modal that show progress of the process
- [x] estimate the progress
- [x] cancel feature
ACKs for top commit:
jp1ac4:
Tested ACK 22f48753a367187a1d08bc34d20e5b75bdfc01b8.
Tree-SHA512: 5cf271d52878c4845347c5951a562e08e7f7efea08f0dc702d0500e41d6ad8eab7cb31f7e8a7b4edba48916648759ce97a9f591d1bcb4564a0c7067d5274fa08
This commit is contained in:
commit
cfe15b360d
469
Cargo.lock
generated
469
Cargo.lock
generated
@ -195,6 +195,76 @@ dependencies = [
|
||||
"libloading 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9c39d707614dbcc6bed00015539f488d8e3fe3e66ed60961efc0c90f4b380b3"
|
||||
dependencies = [
|
||||
"async-fs",
|
||||
"async-net",
|
||||
"enumflags2",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"rand",
|
||||
"raw-window-handle",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"url",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols 0.32.5",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener-strategy",
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"fastrand",
|
||||
"futures-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-fs"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-hwi"
|
||||
version = "0.0.24"
|
||||
@ -221,6 +291,101 @@ dependencies = [
|
||||
"tokio-serial",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"parking",
|
||||
"polling",
|
||||
"rustix",
|
||||
"slab",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"event-listener-strategy",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-net"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-signal",
|
||||
"async-task",
|
||||
"blocking",
|
||||
"cfg-if",
|
||||
"event-listener",
|
||||
"futures-lite",
|
||||
"rustix",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-signal"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"atomic-waker",
|
||||
"cfg-if",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"rustix",
|
||||
"signal-hook-registry",
|
||||
"slab",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.83"
|
||||
@ -583,6 +748,19 @@ dependencies = [
|
||||
"objc2 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blocking"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-task",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.16.0"
|
||||
@ -1403,6 +1581,33 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2_derive"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
@ -1444,6 +1649,27 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener-strategy"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exr"
|
||||
version = "1.73.0"
|
||||
@ -1683,6 +1909,19 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
@ -2845,6 +3084,7 @@ dependencies = [
|
||||
"lianad",
|
||||
"log",
|
||||
"reqwest",
|
||||
"rfd",
|
||||
"rust-ini",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -3114,6 +3354,15 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "metal"
|
||||
version = "0.27.0"
|
||||
@ -3200,7 +3449,7 @@ checksum = "20a4c60ca5c9c0e114b3bd66ff4aa5f9b2b175442be51ca6c4365d687a97a2ac"
|
||||
dependencies = [
|
||||
"log",
|
||||
"mio 0.8.11",
|
||||
"nix",
|
||||
"nix 0.26.4",
|
||||
"serialport",
|
||||
"winapi",
|
||||
]
|
||||
@ -3276,10 +3525,23 @@ dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memoffset",
|
||||
"memoffset 0.7.1",
|
||||
"pin-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases 0.2.1",
|
||||
"libc",
|
||||
"memoffset 0.9.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "no-std-compat"
|
||||
version = "0.4.1"
|
||||
@ -3476,6 +3738,7 @@ checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"block2 0.5.1",
|
||||
"dispatch",
|
||||
"libc",
|
||||
"objc2 0.5.2",
|
||||
]
|
||||
@ -3560,6 +3823,16 @@ dependencies = [
|
||||
"hashbrown 0.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ouroboros"
|
||||
version = "0.18.4"
|
||||
@ -3624,6 +3897,12 @@ dependencies = [
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.11.2"
|
||||
@ -3754,6 +4033,17 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "piper"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"fastrand",
|
||||
"futures-io",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs8"
|
||||
version = "0.10.2"
|
||||
@ -3798,6 +4088,12 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pollster"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2"
|
||||
|
||||
[[package]]
|
||||
name = "poly1305"
|
||||
version = "0.8.0"
|
||||
@ -4226,6 +4522,28 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46f6f80a9b882647d9014673ca9925d30ffc9750f2eed2b4490e189eaebd01e8"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"block2 0.5.1",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2 0.5.2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"pollster",
|
||||
"raw-window-handle",
|
||||
"urlencoding",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rgb"
|
||||
version = "0.8.50"
|
||||
@ -4554,6 +4872,17 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
@ -4579,7 +4908,7 @@ dependencies = [
|
||||
"io-kit-sys",
|
||||
"libudev",
|
||||
"mach2",
|
||||
"nix",
|
||||
"nix 0.26.4",
|
||||
"scopeguard",
|
||||
"unescaper",
|
||||
"winapi",
|
||||
@ -5286,6 +5615,17 @@ version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9"
|
||||
dependencies = [
|
||||
"memoffset 0.9.1",
|
||||
"tempfile",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unescaper"
|
||||
version = "0.1.5"
|
||||
@ -5395,8 +5735,15 @@ dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "usvg"
|
||||
version = "0.36.0"
|
||||
@ -6292,6 +6639,16 @@ version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ef33da6b1660b4ddbfb3aef0ade110c8b8a781a3b6382fa5f2b5b040fd55f61"
|
||||
|
||||
[[package]]
|
||||
name = "xdg-home"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xkbcommon-dl"
|
||||
version = "0.4.2"
|
||||
@ -6371,6 +6728,69 @@ dependencies = [
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb67eadba43784b6fb14857eba0d8fc518686d3ee537066eb6086dc318e2c8a1"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-executor",
|
||||
"async-fs",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-process",
|
||||
"async-recursion",
|
||||
"async-task",
|
||||
"async-trait",
|
||||
"blocking",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"nix 0.29.0",
|
||||
"ordered-stream",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"static_assertions",
|
||||
"tracing",
|
||||
"uds_windows",
|
||||
"windows-sys 0.59.0",
|
||||
"winnow",
|
||||
"xdg-home",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c9d49ebc960ceb660f2abe40a5904da975de6986f2af0d7884b39eec6528c57"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_names"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "856b7a38811f71846fd47856ceee8bccaec8399ff53fb370247e66081ace647b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"winnow",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeno"
|
||||
version = "0.2.3"
|
||||
@ -6482,3 +6902,46 @@ checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "5.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1200ee6ac32f1e5a312e455a949a4794855515d34f9909f4a3e082d14e1a56f"
|
||||
dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"url",
|
||||
"winnow",
|
||||
"zvariant_derive",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_derive"
|
||||
version = "5.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "687e3b97fae6c9104fbbd36c73d27d149abf04fb874e2efbd84838763daa8916"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_utils"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20d1d011a38f12360e5fcccceeff5e2c42a8eb7f27f0dcba97a0862ede05c9c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"syn 2.0.87",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
@ -50,6 +50,7 @@ base64 = "0.21"
|
||||
bitcoin_hashes = "0.12"
|
||||
reqwest = { version = "0.11", default-features=false, features = ["json", "rustls-tls"] }
|
||||
rust-ini = "0.19.0"
|
||||
rfd = "0.15.1"
|
||||
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
|
||||
@ -11,6 +11,7 @@ use lianad::config::Config as DaemonConfig;
|
||||
use crate::{
|
||||
app::{cache::Cache, error::Error, view, wallet::Wallet},
|
||||
daemon::model::*,
|
||||
export::ExportMessage,
|
||||
hw::HardwareWalletMessage,
|
||||
};
|
||||
|
||||
@ -46,4 +47,5 @@ pub enum Message {
|
||||
LabelsUpdated(Result<HashMap<String, Option<String>>, Error>),
|
||||
BroadcastModal(Result<HashSet<Txid>, Error>),
|
||||
RbfModal(Box<HistoryTransaction>, bool, Result<HashSet<Txid>, Error>),
|
||||
Export(ExportMessage),
|
||||
}
|
||||
|
||||
130
liana-gui/src/app/state/export.rs
Normal file
130
liana-gui/src/app/state/export.rs
Normal file
@ -0,0 +1,130 @@
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use iced::{Command, Subscription};
|
||||
use liana_ui::{component::modal::Modal, widget::Element};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
message::Message,
|
||||
view::{self, export::export_modal},
|
||||
},
|
||||
daemon::Daemon,
|
||||
export::{self, get_path, ExportMessage, ExportProgress, ExportState},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ExportModal {
|
||||
path: Option<PathBuf>,
|
||||
handle: Option<Arc<Mutex<JoinHandle<()>>>>,
|
||||
state: ExportState,
|
||||
error: Option<export::Error>,
|
||||
daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
}
|
||||
|
||||
impl ExportModal {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new(daemon: Arc<dyn Daemon + Sync + Send>) -> Self {
|
||||
Self {
|
||||
path: None,
|
||||
handle: None,
|
||||
state: ExportState::Init,
|
||||
error: None,
|
||||
daemon,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn launch(&self) -> Command<Message> {
|
||||
Command::perform(get_path(), |m| {
|
||||
Message::View(view::Message::Export(ExportMessage::Path(m)))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: Message) -> Command<Message> {
|
||||
if let Message::View(view::Message::Export(m)) = message {
|
||||
match m {
|
||||
ExportMessage::ExportProgress(m) => match m {
|
||||
ExportProgress::Started(handle) => {
|
||||
self.handle = Some(handle);
|
||||
self.state = ExportState::Progress(0.0);
|
||||
}
|
||||
ExportProgress::Progress(p) => {
|
||||
if let ExportState::Progress(_) = self.state {
|
||||
self.state = ExportState::Progress(p);
|
||||
}
|
||||
}
|
||||
ExportProgress::Finished | ExportProgress::Ended => {
|
||||
self.state = ExportState::Ended
|
||||
}
|
||||
ExportProgress::Error(e) => self.error = Some(e),
|
||||
ExportProgress::None => {}
|
||||
},
|
||||
ExportMessage::TimedOut => {
|
||||
self.stop(ExportState::TimedOut);
|
||||
}
|
||||
ExportMessage::UserStop => {
|
||||
self.stop(ExportState::Aborted);
|
||||
}
|
||||
ExportMessage::Path(p) => {
|
||||
if let Some(path) = p {
|
||||
self.path = Some(path);
|
||||
self.start();
|
||||
} else {
|
||||
return Command::perform(async {}, |_| {
|
||||
Message::View(view::Message::Export(ExportMessage::Close))
|
||||
});
|
||||
}
|
||||
}
|
||||
ExportMessage::Close | ExportMessage::Open => { /* unreachable */ }
|
||||
}
|
||||
Command::none()
|
||||
} else {
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
pub fn view<'a>(&'a self, content: Element<'a, view::Message>) -> Element<view::Message> {
|
||||
let modal = Modal::new(
|
||||
content,
|
||||
export_modal(&self.state, self.error.as_ref(), "Transactions"),
|
||||
);
|
||||
match self.state {
|
||||
ExportState::TimedOut
|
||||
| ExportState::Aborted
|
||||
| ExportState::Ended
|
||||
| ExportState::Closed => modal.on_blur(Some(view::Message::Close)),
|
||||
_ => modal,
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
self.state = ExportState::Started;
|
||||
}
|
||||
|
||||
pub fn stop(&mut self, state: ExportState) {
|
||||
if let Some(handle) = self.handle.take() {
|
||||
handle.lock().expect("poisoned").abort();
|
||||
self.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn subscription(&self) -> Option<Subscription<export::ExportProgress>> {
|
||||
if let Some(path) = &self.path {
|
||||
match &self.state {
|
||||
ExportState::Started | ExportState::Progress(_) => {
|
||||
Some(iced::subscription::unfold(
|
||||
"transactions",
|
||||
export::State::new(self.daemon.clone(), Box::new(path.to_path_buf())),
|
||||
export::export_subscription,
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
mod coins;
|
||||
mod export;
|
||||
mod label;
|
||||
mod psbt;
|
||||
mod psbts;
|
||||
|
||||
@ -28,6 +28,7 @@ use crate::{
|
||||
wallet::Wallet,
|
||||
},
|
||||
daemon::model::{self, LabelsLoader},
|
||||
export::ExportMessage,
|
||||
};
|
||||
|
||||
use crate::daemon::{
|
||||
@ -35,13 +36,22 @@ use crate::daemon::{
|
||||
Daemon,
|
||||
};
|
||||
|
||||
use super::export::ExportModal;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum TransactionsModal {
|
||||
CreateRbf(CreateRbfModal),
|
||||
Export(ExportModal),
|
||||
None,
|
||||
}
|
||||
|
||||
pub struct TransactionsPanel {
|
||||
wallet: Arc<Wallet>,
|
||||
txs: Vec<HistoryTransaction>,
|
||||
labels_edited: LabelsEdited,
|
||||
selected_tx: Option<HistoryTransaction>,
|
||||
warning: Option<Error>,
|
||||
create_rbf_modal: Option<CreateRbfModal>,
|
||||
modal: TransactionsModal,
|
||||
is_last_page: bool,
|
||||
processing: bool,
|
||||
}
|
||||
@ -54,7 +64,7 @@ impl TransactionsPanel {
|
||||
txs: Vec::new(),
|
||||
labels_edited: LabelsEdited::default(),
|
||||
warning: None,
|
||||
create_rbf_modal: None,
|
||||
modal: TransactionsModal::None,
|
||||
is_last_page: false,
|
||||
processing: false,
|
||||
}
|
||||
@ -63,7 +73,7 @@ impl TransactionsPanel {
|
||||
pub fn preselect(&mut self, tx: HistoryTransaction) {
|
||||
self.selected_tx = Some(tx);
|
||||
self.warning = None;
|
||||
self.create_rbf_modal = None;
|
||||
self.modal = TransactionsModal::None;
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,19 +86,22 @@ impl State for TransactionsPanel {
|
||||
self.labels_edited.cache(),
|
||||
self.warning.as_ref(),
|
||||
);
|
||||
if let Some(modal) = &self.create_rbf_modal {
|
||||
modal.view(content)
|
||||
} else {
|
||||
content
|
||||
match &self.modal {
|
||||
TransactionsModal::CreateRbf(rbf) => rbf.view(content),
|
||||
_ => content,
|
||||
}
|
||||
} else {
|
||||
view::transactions::transactions_view(
|
||||
let content = view::transactions::transactions_view(
|
||||
cache,
|
||||
&self.txs,
|
||||
self.warning.as_ref(),
|
||||
self.is_last_page,
|
||||
self.processing,
|
||||
)
|
||||
);
|
||||
match &self.modal {
|
||||
TransactionsModal::Export(export) => export.view(content),
|
||||
_ => content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,7 +147,7 @@ impl State for TransactionsPanel {
|
||||
Message::RbfModal(tx, is_cancel, res) => match res {
|
||||
Ok(descendant_txids) => {
|
||||
let modal = CreateRbfModal::new(*tx, is_cancel, descendant_txids);
|
||||
self.create_rbf_modal = Some(modal);
|
||||
self.modal = TransactionsModal::CreateRbf(modal);
|
||||
}
|
||||
Err(e) => {
|
||||
self.warning = e.into();
|
||||
@ -146,16 +159,16 @@ impl State for TransactionsPanel {
|
||||
Message::View(view::Message::Select(i)) => {
|
||||
self.selected_tx = self.txs.get(i).cloned();
|
||||
// Clear modal if it's for a different tx.
|
||||
if let Some(modal) = &self.create_rbf_modal {
|
||||
if let TransactionsModal::CreateRbf(modal) = &self.modal {
|
||||
if Some(modal.tx.tx.txid())
|
||||
!= self.selected_tx.as_ref().map(|selected| selected.tx.txid())
|
||||
{
|
||||
self.create_rbf_modal = None;
|
||||
self.modal = TransactionsModal::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::View(view::Message::CreateRbf(view::CreateRbfMessage::Cancel)) => {
|
||||
self.create_rbf_modal = None;
|
||||
self.modal = TransactionsModal::None;
|
||||
}
|
||||
Message::View(view::Message::CreateRbf(view::CreateRbfMessage::New(is_cancel))) => {
|
||||
if let Some(tx) = &self.selected_tx {
|
||||
@ -249,11 +262,26 @@ impl State for TransactionsPanel {
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Some(modal) = &mut self.create_rbf_modal {
|
||||
return modal.update(daemon, _cache, message);
|
||||
Message::View(view::Message::Export(ExportMessage::Open)) => {
|
||||
if let TransactionsModal::None = &self.modal {
|
||||
self.modal = TransactionsModal::Export(ExportModal::new(daemon));
|
||||
if let TransactionsModal::Export(m) = &self.modal {
|
||||
return m.launch();
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::View(view::Message::Export(ExportMessage::Close)) => {
|
||||
if let TransactionsModal::Export(_) = &self.modal {
|
||||
self.modal = TransactionsModal::None;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return match &mut self.modal {
|
||||
TransactionsModal::CreateRbf(modal) => modal.update(daemon, _cache, message),
|
||||
TransactionsModal::Export(modal) => modal.update(message),
|
||||
TransactionsModal::None => Command::none(),
|
||||
};
|
||||
}
|
||||
};
|
||||
Command::none()
|
||||
}
|
||||
@ -284,6 +312,17 @@ impl State for TransactionsPanel {
|
||||
Message::HistoryTransactions,
|
||||
)])
|
||||
}
|
||||
|
||||
fn subscription(&self) -> iced::Subscription<Message> {
|
||||
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)))
|
||||
});
|
||||
}
|
||||
}
|
||||
iced::Subscription::none()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TransactionsPanel> for Box<dyn State> {
|
||||
@ -292,6 +331,7 @@ impl From<TransactionsPanel> for Box<dyn State> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CreateRbfModal {
|
||||
/// Transaction to replace.
|
||||
tx: model::HistoryTransaction,
|
||||
|
||||
88
liana-gui/src/app/view/export.rs
Normal file
88
liana-gui/src/app/view/export.rs
Normal file
@ -0,0 +1,88 @@
|
||||
use iced::{
|
||||
widget::{progress_bar, Button, Column, Container, Row, Space},
|
||||
Length,
|
||||
};
|
||||
use liana_ui::{
|
||||
component::{
|
||||
card,
|
||||
text::{h4_bold, text},
|
||||
},
|
||||
theme,
|
||||
widget::Element,
|
||||
};
|
||||
|
||||
use crate::export::{Error, ExportMessage};
|
||||
use crate::{app::view::message::Message, export::ExportState};
|
||||
|
||||
/// Return the modal view for an export task
|
||||
pub fn export_modal<'a>(
|
||||
state: &ExportState,
|
||||
error: Option<&'a Error>,
|
||||
export_type: &str,
|
||||
) -> Element<'a, Message> {
|
||||
let button = match state {
|
||||
ExportState::Started | ExportState::Progress(_) => {
|
||||
Some(Button::new("Cancel").on_press(ExportMessage::UserStop.into()))
|
||||
}
|
||||
ExportState::Ended | ExportState::TimedOut | ExportState::Aborted => {
|
||||
Some(Button::new("Close").on_press(ExportMessage::Close.into()))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
.map(|b| b.height(32).style(theme::Button::Primary));
|
||||
let msg = if let Some(error) = error {
|
||||
format!("{:?}", error)
|
||||
} else {
|
||||
match state {
|
||||
ExportState::Init => "".to_string(),
|
||||
ExportState::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(),
|
||||
}
|
||||
};
|
||||
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 progress_bar_row = Row::new()
|
||||
.push(Space::with_width(30))
|
||||
.push(progress_bar(0.0..=100.0, p))
|
||||
.push(Space::with_width(30));
|
||||
let button_row = button.map(|b| {
|
||||
Row::new()
|
||||
.push(Space::with_width(Length::Fill))
|
||||
.push(b)
|
||||
.push(Space::with_width(Length::Fill))
|
||||
});
|
||||
card::simple(
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
.push(Container::new(h4_bold(format!("Export {export_type}"))).width(Length::Fill))
|
||||
.push(Space::with_height(Length::Fill))
|
||||
.push(progress_bar_row)
|
||||
.push(Space::with_height(Length::Fill))
|
||||
.push(
|
||||
Row::new()
|
||||
.push(Space::with_width(Length::Fill))
|
||||
.push(text(msg))
|
||||
.push(Space::with_width(Length::Fill)),
|
||||
)
|
||||
.push(Space::with_height(Length::Fill))
|
||||
.push_maybe(button_row)
|
||||
.push(Space::with_height(5)),
|
||||
)
|
||||
.width(Length::Fixed(500.0))
|
||||
.height(Length::Fixed(220.0))
|
||||
.into()
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::{app::menu::Menu, node::bitcoind::RpcAuthType};
|
||||
use crate::{app::menu::Menu, export::ExportMessage, node::bitcoind::RpcAuthType};
|
||||
use liana::miniscript::bitcoin::{bip32::Fingerprint, OutPoint};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -19,6 +19,7 @@ pub enum Message {
|
||||
SelectHardwareWallet(usize),
|
||||
CreateRbf(CreateRbfMessage),
|
||||
ShowQrCode(usize),
|
||||
Export(ExportMessage),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@ -3,6 +3,7 @@ mod message;
|
||||
mod warning;
|
||||
|
||||
pub mod coins;
|
||||
pub mod export;
|
||||
pub mod home;
|
||||
pub mod hw;
|
||||
pub mod psbt;
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use chrono::{DateTime, Local, Utc};
|
||||
use iced::{alignment, widget::tooltip, Alignment, Length};
|
||||
use iced::{
|
||||
alignment,
|
||||
widget::{tooltip, Space},
|
||||
Alignment, Length,
|
||||
};
|
||||
|
||||
use liana_ui::{
|
||||
color,
|
||||
@ -15,9 +19,14 @@ use crate::{
|
||||
cache::Cache,
|
||||
error::Error,
|
||||
menu::Menu,
|
||||
view::{dashboard, label, message::CreateRbfMessage, message::Message, warning::warn},
|
||||
view::{
|
||||
dashboard, label,
|
||||
message::{CreateRbfMessage, Message},
|
||||
warning::warn,
|
||||
},
|
||||
},
|
||||
daemon::model::{HistoryTransaction, Txid},
|
||||
export::ExportMessage,
|
||||
};
|
||||
|
||||
pub fn transactions_view<'a>(
|
||||
@ -32,7 +41,16 @@ pub fn transactions_view<'a>(
|
||||
cache,
|
||||
warning,
|
||||
Column::new()
|
||||
.push(Container::new(h3("Transactions")).width(Length::Fill))
|
||||
.push(
|
||||
Row::new()
|
||||
.push(Container::new(h3("Transactions")))
|
||||
.push(Space::with_width(Length::Fill))
|
||||
.push(
|
||||
Button::new("Export")
|
||||
.on_press(ExportMessage::Open.into())
|
||||
.style(theme::Button::Secondary),
|
||||
),
|
||||
)
|
||||
.push(
|
||||
Column::new()
|
||||
.spacing(10)
|
||||
|
||||
386
liana-gui/src/export.rs
Normal file
386
liana-gui/src/export.rs
Normal file
@ -0,0 +1,386 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{self, File},
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
mpsc::{channel, Receiver, Sender},
|
||||
Arc, Mutex,
|
||||
},
|
||||
time::{self},
|
||||
};
|
||||
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use liana::miniscript::bitcoin::{Amount, Txid};
|
||||
use tokio::{
|
||||
task::{JoinError, JoinHandle},
|
||||
time::sleep,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::view,
|
||||
daemon::{
|
||||
model::{HistoryTransaction, Labelled},
|
||||
Daemon, DaemonBackend, DaemonError,
|
||||
},
|
||||
lianalite::client::backend::api::DEFAULT_LIMIT,
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
($sender:ident, $error:expr) => {
|
||||
if let Err(e) = $sender.send(ExportProgress::Error($error)) {
|
||||
tracing::error!("ExportState::start() 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);
|
||||
}
|
||||
};
|
||||
($sender:ident, $progress:ident($val:expr)) => {
|
||||
if let Err(e) = $sender.send(ExportProgress::$progress($val)) {
|
||||
tracing::error!("ExportState::start() fail to send msg: {}", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExportMessage {
|
||||
Open,
|
||||
ExportProgress(ExportProgress),
|
||||
TimedOut,
|
||||
UserStop,
|
||||
Path(Option<PathBuf>),
|
||||
Close,
|
||||
}
|
||||
|
||||
impl From<ExportMessage> for view::Message {
|
||||
fn from(value: ExportMessage) -> Self {
|
||||
Self::Export(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ExportState {
|
||||
Init,
|
||||
ChoosePath,
|
||||
Path(PathBuf),
|
||||
Started,
|
||||
Progress(f32),
|
||||
TimedOut,
|
||||
Aborted,
|
||||
Ended,
|
||||
Closed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Error {
|
||||
Io(String),
|
||||
HandleLost,
|
||||
UnexpectedEnd,
|
||||
JoinError(String),
|
||||
ChannelLost,
|
||||
NoParentDir,
|
||||
Daemon(String),
|
||||
TxTimeMissing,
|
||||
}
|
||||
|
||||
impl From<JoinError> for Error {
|
||||
fn from(value: JoinError) -> Self {
|
||||
Error::JoinError(format!("{:?}", value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Error::Io(format!("{:?}", value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DaemonError> for Error {
|
||||
fn from(value: DaemonError) -> Self {
|
||||
Error::Daemon(format!("{:?}", value))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Status {
|
||||
Init,
|
||||
Running,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExportProgress {
|
||||
Started(Arc<Mutex<JoinHandle<()>>>),
|
||||
Progress(f32),
|
||||
Ended,
|
||||
Finished,
|
||||
Error(Error),
|
||||
None,
|
||||
}
|
||||
|
||||
pub struct State {
|
||||
pub receiver: Receiver<ExportProgress>,
|
||||
pub sender: Option<Sender<ExportProgress>>,
|
||||
pub handle: Option<Arc<Mutex<JoinHandle<()>>>>,
|
||||
pub daemon: Arc<dyn Daemon + Sync + Send>,
|
||||
pub path: Box<PathBuf>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new(daemon: Arc<dyn Daemon + Sync + Send>, path: Box<PathBuf>) -> Self {
|
||||
let (sender, receiver) = channel();
|
||||
State {
|
||||
receiver,
|
||||
sender: Some(sender),
|
||||
handle: None,
|
||||
daemon,
|
||||
path,
|
||||
}
|
||||
}
|
||||
|
||||
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 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::<Txid, HistoryTransaction>::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| a.compare(b));
|
||||
|
||||
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 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);
|
||||
});
|
||||
let handle = Arc::new(Mutex::new(handle));
|
||||
|
||||
// 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()));
|
||||
self.handle = Some(handle);
|
||||
} else {
|
||||
tracing::error!("ExportState can start only once!");
|
||||
}
|
||||
}
|
||||
pub fn state(&self) -> Status {
|
||||
match (&self.sender, &self.handle) {
|
||||
(Some(_), None) => Status::Init,
|
||||
(None, Some(_)) => Status::Running,
|
||||
(None, None) => Status::Stopped,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn export_subscription(mut state: State) -> (ExportProgress, State) {
|
||||
match state.state() {
|
||||
Status::Init => {
|
||||
state.start().await;
|
||||
}
|
||||
Status::Stopped => {
|
||||
sleep(time::Duration::from_millis(1000)).await;
|
||||
return (ExportProgress::None, state);
|
||||
}
|
||||
Status::Running => { /* continue */ }
|
||||
}
|
||||
let msg = state.receiver.try_recv();
|
||||
let disconnected = match msg {
|
||||
Ok(m) => return (m, state),
|
||||
Err(e) => match e {
|
||||
std::sync::mpsc::TryRecvError::Empty => false,
|
||||
std::sync::mpsc::TryRecvError::Disconnected => true,
|
||||
},
|
||||
};
|
||||
|
||||
let handle = match state.handle.take() {
|
||||
Some(h) => h,
|
||||
None => return (ExportProgress::Error(Error::HandleLost), state),
|
||||
};
|
||||
{
|
||||
let h = handle.lock().expect("should not fail");
|
||||
if h.is_finished() {
|
||||
return (ExportProgress::Finished, state);
|
||||
} else if disconnected {
|
||||
return (ExportProgress::Error(Error::ChannelLost), state);
|
||||
}
|
||||
} // => release handle lock
|
||||
state.handle = Some(handle);
|
||||
|
||||
sleep(time::Duration::from_millis(100)).await;
|
||||
(ExportProgress::None, state)
|
||||
}
|
||||
|
||||
pub async fn get_path() -> Option<PathBuf> {
|
||||
rfd::AsyncFileDialog::new()
|
||||
.set_title("Choose a location to export...")
|
||||
.set_file_name("liana.csv")
|
||||
.save_file()
|
||||
.await
|
||||
.map(|fh| fh.path().to_path_buf())
|
||||
}
|
||||
@ -2,6 +2,7 @@ pub mod app;
|
||||
pub mod daemon;
|
||||
pub mod datadir;
|
||||
pub mod download;
|
||||
pub mod export;
|
||||
pub mod hw;
|
||||
pub mod installer;
|
||||
pub mod launcher;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user